[NOI2012] 骑行川藏
一、题目
二、解法
据说导数的相关内容是选择性必修 \(2\) 的,考虑到我也没有学过,那我就简单的讲一讲吧,我只讲怎么求函数 \(y=ax^n\) 的导数:
因为最后包含 \(\Delta x\) 的项可以视而不见,我们用二项式展开可以得到唯一剩下的项是 \(anx^{n-1}\) ,而他就是导数。如果是若干个上述形式的函数加起来,那么不难得到他们的导数也相加。
提示:必然存在一种最优的体力方案满足:蛋蛋在每段路上都采用匀速骑行的方式。
首先考虑一些消耗能量最少的走法:如果 \(v[i]\) 大于 \(0\) ,那么我们就使 \(v=v[i]\) ;如果 \(v[i]\leq0\) ,那么我们使得 \(v=0\) 。这样显然不是最优的,但是我们可以慢慢的调整使其达到最优,这种方法叫做 微调法 。
但是这道题是在实数范围内的最优问题,我们无法用最小单位来微调。那么我们不妨用极限的思想,我们把无限小的能量用于给某段路减少时间,那么这道题就必须要引入 导数 的概念。即每段路都拥有一个导数,我们每次取导数最大的微调。
我们可以想象一个 \(E-t\) 的图像 ,这个图像的导数是越来越大的(导数永远是负数,后面投入的相同能量会产生更小的效益),因为微调的过程是不可能实现的,我们可以利用这个性质来达到微调的效果。可以二分一个导数值 \(x\) ,最后小于等于 \(x\) 的初始导数都会等于 \(x\) ,那么这个取法才是最优的。
如果算出来消耗的能量大于已经有的能量,那么就把导数值调小,否者就把导数值调大。怎么算需要消耗的能量呢?首先我们要知道每段路的导数表达式,那么我们就可以反解出速度。现在我们来推导导数 \(d\) 的表达式:
我们知道 \(t=\frac{s}{v},E=sk(v-v')^2\),发现 \(t\) 和 \(E\) 好像是没有关联的,所以上面的 \(\Delta t\) 就不是很好求,那么我们就用 \(v\) 把他们关联起来,也就是求出 \(v\) 变化很小的时候 \(t\) 和 \(E\) 的变化量,也就是 \(t-v\) 和 \(E-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);
}