Luogu P2179 [NOI2012] 骑行川藏 (拉格朗日乘数法)(黑题祭!!)
首先:第一道手撕的黑题祭!2022.4.20
题意:
在满足 $\sum_{i=1}^{n} {k_{i} \times s_{i} \times (v_{i} - v_{i}{}' )^2} \le E_{U} $ 的条件下,
使得 $\sum_{i=1}^{n} {\frac{s_{i}}{v{i}} } $ 最小,
其中 $k_{i},s_{i},v_{i}{}'$ 为给定的常数,未知量就是 $v_{i}$。
分析:
其实学过拉格朗日乘数法的已经能看出来了,求这种多元函数的极值,有了给定的约束条件,直接拉乘就可以了。
那么拉格朗日乘数法是什么呢?
首先你得学会导数,只需要记住几个基本函数的求导法则就行了,比如幂函数,指数函数,对数函数,$sin$,$cos$ ,再学会函数加和、乘积、作比的求导法则,基本够用了。
其实从高等数学的角度来讲,我不会,如果好奇可以去这里。
所以从通俗角度来讲,我们只需要掌握套路,知道这个东西可以求极值就行了。
那么套路是什么呢?
首先题中给出了约束条件,也就是未知量的一个等式,在本题中,我们可以贪心的想:
我们消耗的体力越多,肯定跑的越快,也就是那个式子越小。
所以我们不妨令约束条件为 $\sum_{i=1}^{n} {k_{i} \times s_{i} \times (v_{i} - v_{i}{}' )^2} = E_{U}$。
接下来引出拉格朗日乘数法最核心的部分:
我们要求上面式子的极值,就要引入一个 $\lambda $,
然后得到这样的一个函数:
$\sum_{i=1}^{n} {\frac{s_{i}}{v{i}} } + \lambda (\sum_{i=1}^{n} {k_{i} \times s_{i} \times (v_{i} - v_{i}{}' )^2} - E_{U})$
观察一下是怎么得来的?
首先把要求极值的式子抄过来,再写一个 $\lambda$,再用它乘上约束条件的量都移项到左边的样子。
接下来我们要对这个函数中的每一个变量,也就是我们要求的 $v_{i}$ 和 $\lambda$,求偏导,
偏导是什么?我不会啊啊啊!!!
不,你会!
偏导其实就是只考虑当前一个变量,把其他所有的无关变量以及原有的常量统一认为是常量,然后求导。
所以我们推导一下:
对于 $v_{i}$:
首先忽略与他无关的常数式子,得到:
${\frac{s_{i}}{v_{i}} } + \lambda \times {k_{i} \times s_{i} \times (v_{i} - v_{i}{}' )^2}$
此时进行求导,可以得到:
$-\frac{s_{i}}{v_{i}^2} + 2\times \lambda \times {k_{i} \times s_{i} \times (v_{i} - v_{i}{}' )}$
注意这里有个小技巧就是
如果 $f(x) = (x + b)^2$ 那么 $f{}’(x) = 2(x + b)$,手推一下就知道了。
接下来对 $\lambda$ 求导就没什么了,把系数抄下来就OK:
$\sum_{i=1}^{n} {k_{i} \times s_{i} \times (v_{i} - v_{i}{}' )^2} - E_{U}$
众所周知,函数求极值在导函数的零点处取得,所以我们要让所有的 $v_{i}$ 还有 $\lambda$ 都使得他们的导函数为 $0$,也就是说我们所求的式子的极值,在我们求偏导得出的所有式子同时为 $0$ 时取得。
接下来考虑怎么求:
首先我们发现,直接找每一个 $v_{i}$,如同我们脱裤子放屁,是不可行的。
我们看看对 $v_{i}$ 导数的式子可以得到什么?
$2\times \lambda \times {k_{i} \times {v_{i}^2} \times (v_{i} - v_{i}{}' )} = 1$
非常滴amazing啊,这不反比例吗?
当然不全是,至少我们肯定能看出来随着 $\lambda$ 递增,$v_{i}$ 肯定递减,且这个时候的 $v_{i}$ 最便于确定。
好!那就二分 $\lambda$ 吧!
然后在二分答案的时候,在里面二分 $v_{i}$ 就好了,因为内外都有单调性,都可以二分,这样我们肯定能逼近到所有
方程都为 $0$ 也就是取到极值的时候啦。
代码部分,其实和之前的神犇好像都差不多啦,重在理解学习!
#include<cstdio> #include<queue> #include<iostream> #include<cstring> #include<algorithm> #include<cmath> #include<cctype> #include<vector> #include<string> #include<climits> #include<stack> using namespace std; template <typename T> inline void read(T &x){ x=0;char ch=getchar();bool f=0; while(ch<'0'||ch>'9'){if(ch=='-')f=1;ch=getchar();} while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); if(f)x=-x; } template <typename T,typename ...Args> inline void read(T &tmp,Args &...tmps){read(tmp);read(tmps...);} const double eps = 1e-12; const int N = 1e4 + 5; int n; double E,ans; double s[N],k[N],v[N],u[N]; inline bool check(double lambda,double v,double k,double u){ return 2 * lambda * k * v * v * (v - u) <= 1;//检查关于v[i]的导数是否符合条件 } inline double po(double x){return x * x;} inline bool check1(double lambda){ double res = 0; for(int i=1;i<=n;++i){ //puts("1"); double l = max(u[i],0.0),r = 1e5; while(l + eps <= r){//二分v[i] double mid = (l + r) / 2; if(check(lambda,mid,k[i],u[i]))l = mid; else r = mid; } v[i] = l; res += k[i] * po(v[i] - u[i]) * s[i]; } return res <= E;//这是关于λ的导数方程的检验 } signed main(){ read(n); scanf("%lf",&E); for(int i=1;i<=n;++i)scanf("%lf%lf%lf",&s[i],&k[i],&u[i]);//别问为什么v'用u,问就是图论做多了 double l = 0,r = 1e5; while(l + eps <= r){//二分λ double mid = (l + r) / 2; if(check1(mid))r = mid; else l = mid; } for(int i=1;i<=n;++i)ans += s[i] / v[i]; printf("%.8lf",ans); }