题目
思路
用微分的思想,先把某一条路上 v v v 不同的部分拆开。这样我们就改成了讨论很多很多段,每一段内的 v v v 都是相同的。
如果你做过函数这道题,或许你会有点思路。可以考虑成,把能量分配给每一段路,使得它们对应算出的时间最小。
我们试着算一算导数行吧?然而不是很好算。你会发现 t t t 和 E E E 没有直接关联,而是用 v v v 串联在了一起。所以有一个技巧,我们以 v v v 为自变量,计算 t t t 和 E E E 的导数,然后作比。感性的式子是
d t d E = d t d v ÷ d E d v \frac{dt}{dE}=\frac{dt}{dv}\div\frac{dE}{dv} dEdt=dvdt÷dvdE
颇有些洛必达的味道。
然后就很好算了,因为 E ( v ) = k s ( v − v ′ ) 2 ⇒ E ′ ( v ) = 2 k s ( v − v ′ ) E(v)=ks(v-v')^2\;\Rightarrow\;E'(v)=2ks(v-v') E(v)=ks(v−v′)2⇒E′(v)=2ks(v−v′) 且 t ( v ) = s ⋅ v − 1 ⇒ t ′ ( v ) = − s ⋅ v − 2 t(v)=s\cdot v^{-1}\;\Rightarrow\;t'(v)=-s\cdot v^{-2} t(v)=s⋅v−1⇒t′(v)=−s⋅v−2 于是
t ′ ( v ) E ′ ( v ) = − s ⋅ v − 2 2 k s ( v − v ′ ) = − 1 2 k v 2 ( v − v ′ ) \frac{t'(v)}{E'(v)}=\frac{-s\cdot v^{-2}}{2ks(v-v')}=\frac{-1}{2kv^2(v-v')} E′(v)t′(v)=2ks(v−v′)−s⋅v−2=2kv2(v−v′)−1
默认大家都会复合函数求导啥的。
因为 v > v ′ v>v' v>v′ ,所以该斜率单增(但始终为负数)。既然斜率是单增的,就容易证明 最终每一段路的导数相等。当然,在这道题中,你可以大致把 t − E t-E t−E 函数想象为反比例函数在第一象限的图像。
由于这个导数跟 s s s 无关,所以同一段路拆成多个小段(文章开头的操作)是没用的,它们最终的 v v v 相等。
二分这个斜率。计算所需能量。如果花费的能量偏少了,那么调大斜率(这个函数是反比例),否则调小。
然鹅,怎么解关于的
v
v
v 的方程
−
1
2
k
v
2
(
v
−
v
′
)
=
x
\frac{-1}{2kv^2(v-v')}=x
2kv2(v−v′)−1=x 呢?我们已经证明了它单调,二分就行。不会真的有人背得下来三次方程求根公式吧?
复杂度 O ( n log 2 Q ) \mathcal O(n\log^2 Q) O(nlog2Q) ,但是 Q Q Q 是什么呢?由于 E ≤ 1 0 8 E\le 10^8 E≤108 ,所以 v ≤ 1 0 4 + v ′ < 10100 v\le 10^4+v'<10100 v≤104+v′<10100 ;由于答案不超过 1 0 5 10^5 105 ,所以 v ≥ s 1 0 5 ≥ 1 0 − 5 v\ge\frac{s}{10^5}\ge 10^{-5} v≥105s≥10−5 。所以导数 x x x 的范围是
1 0 − 15 ≤ x ≤ 1 0 15 10^{-15}\le x\le 10^{15} 10−15≤x≤1015
这个范围比较粗略。但是这至少告诉你,二分的次数不能太少。我二分 100 100 100 次就会 W r o n g A n s w e r \rm Wrong\;Answer WrongAnswer 了。
代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 10005;
double s[MaxN], k[MaxN], v[MaxN];
double getV(int i,double x){
double L = v[i], R = L+pow(-1/(2*k[i]*x),0.333333);
R += 100; if(L < 0) L = 0; // v >= 0
for(int cs=60; cs; --cs){
double mid = (L+R)/2;
if(-1/(2*k[i]*x) < mid*mid*(mid-v[i]))
R = mid; // a bit too large
else L = mid; // a bit too small
}
return L;
}
double calc(const int &n,double x){
double sum = 0; // used energy
for(int i=1; i<=n; ++i){
double now = getV(i,x);
sum += s[i]*k[i]*(now-v[i])*(now-v[i]);
}
return sum;
}
int main(){
int n = readint();
double E; scanf("%lf",&E);
for(int i=1; i<=n; ++i)
scanf("%lf %lf %lf",s+i,k+i,v+i);
double L = -1e10, R = 0;
for(int cs=200; cs; --cs)
if(calc(n,(L+R)/2) <= E)
L = (L+R)/2;
else R = (L+R)/2;
double ans = 0;
for(int i=1; i<=n; ++i)
ans += s[i]/getV(i,L);
printf("%.10f\n",ans);
return 0;
}
后记
题解里有模拟退火的做法,宋队狂喜!