1. 程式人生 > 實用技巧 >[NOI2012] 騎行川藏

[NOI2012] 騎行川藏

一、題目

點此看題

二、解法

據說導數的相關內容是選擇性必修 \(2\) 的,考慮到我也沒有學過,那我就簡單的講一講吧,我只講怎麼求函式 \(y=ax^n\) 的導數:

\[d=\frac{a(x+\Delta x)^n-ax^n}{\Delta x}=?\Delta x^{?}x^{?}+anx^{n-1} \]

因為最後包含 \(\Delta x\) 的項可以視而不見,我們用二項式展開可以得到唯一剩下的項是 \(anx^{n-1}\) ,而他就是導數。如果是若干個上述形式的函式加起來,那麼不難得到他們的導數也相加。


提示:必然存在一種最優的體力方案滿足:蛋蛋在每段路上都採用勻速騎行的方式。

首先考慮一些消耗能量最少的走法:如果 \(v[i]\) 大於 \(0\) ,那麼我們就使 \(v=v[i]\) ;如果 \(v[i]\leq0\) ,那麼我們使得 \(v=0\) 。這樣顯然不是最優的,但是我們可以慢慢的調整使其達到最優,這種方法叫做 微調法

但是這道題是在實數範圍內的最優問題,我們無法用最小單位來微調。那麼我們不妨用極限的思想,我們把無限小的能量用於給某段路減少時間,那麼這道題就必須要引入 導數 的概念。即每段路都擁有一個導數,我們每次取導數最大的微調。

我們可以想象一個 \(E-t\) 的影象 ,這個影象的導數是越來越大的(導數永遠是負數,後面投入的相同能量會產生更小的效益),因為微調的過程是不可能實現的,我們可以利用這個性質來達到微調的效果。可以二分一個導數值 \(x\)

最後小於等於 \(x\) 的初始導數都會等於 \(x\) ,那麼這個取法才是最優的。

如果算出來消耗的能量大於已經有的能量,那麼就把導數值調小,否者就把導數值調大。怎麼算需要消耗的能量呢?首先我們要知道每段路的導數表示式,那麼我們就可以反解出速度。現在我們來推導導數 \(d\) 的表示式:

\[d=\frac{\Delta t}{\Delta E} \]

我們知道 \(t=\frac{s}{v},E=sk(v-v')^2\),發現 \(t\)\(E\) 好像是沒有關聯的,所以上面的 \(\Delta t\) 就不是很好求,那麼我們就用 \(v\) 把他們關聯起來,也就是求出 \(v\)

變化很小的時候 \(t\)\(E\) 的變化量,也就是 \(t-v\)\(E-v\) 影象的導數

\[d=\frac{\Delta t/\Delta v}{\Delta E/\Delta v}=\frac{-1sv^{-2}}{2sk(v-v')}=\frac{-1}{2kv^2(v-v')} \]

那麼可以用 \(d\) 反解出 \(v\) ,這是個一元三次方程,但是具有單調性,所以可以二分解決。

本題是二分套二分,方便卡時間我們不規定二分的精度而規定二分的次數,外層二分我做了 \(100\) 次,內層二分我做了 \(60\) 次,只要卡著時間你儘量多分幾次唄。

最後補充一點,你知不知道提示的勻速騎行的策略是怎麼來的。我們假設有一條路上不勻速騎行,那麼我們可以把它劃分成無限個勻速騎行的段,這些段的 \(k\)\(v'\) 都相同,最後達到的 \(d\) 也相同,那麼由於導數的單調性解出來的 \(v\) 也都是相同的,這與一開始的假設相悖。所以我們用反證法說明了這個結論。

#include <cstdio>
#include <iostream>
using namespace std;
#define db double
const int M = 10005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n;db E,s[M],k[M],v[M];
db getv(db x,int i)
{
	db l=max(0.0,v[i]),r=100000,mid;int cnt=60;
	while(cnt--)
	{
		mid=(l+r)/2;
		if(2*k[i]*x*mid*mid*(mid-v[i])>-1) l=mid;
		else r=mid;
	}
	return (l+r)/2;
}
signed main()
{
	n=read();scanf("%lf",&E);
	for(int i=1;i<=n;i++)
	{
		scanf("%lf %lf %lf",&s[i],&k[i],&v[i]);
	}
	db l=-1e10,r=0,mid,sum;int cnt=100;
	while(cnt--)
	{
		mid=(l+r)/2,sum=0;
		for(int i=1;i<=n;i++)
		{
			db x=getv(mid,i);
			sum+=s[i]*k[i]*(x-v[i])*(x-v[i]);
		}
		if(sum<=E) l=mid;
		else r=mid;
	}
	db ans=0;mid=(l+r)/2;
	for(int i=1;i<=n;i++)
	{
		ans+=s[i]/getv(mid,i);
	}
	printf("%.6f\n",ans);
}