1. 程式人生 > 其它 >Luogu P2179 [NOI2012] 騎行川藏 (拉格朗日乘數法)(黑題祭!!)

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