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); }