1. 程式人生 > 實用技巧 >Luogu P2179 [NOI2012]騎行川藏

Luogu P2179 [NOI2012]騎行川藏

題意

給定 \(n\) 個路段,每個路段用三個實數 \(s_i,k_i,v^\prime_i\) 描述,最小化

\[F(v_1,\cdots v_n)=\sum\limits_{i=1}^{n}\frac{s_i}{v_i} \]

其中 \(v_1,\cdots v_n\) 均為非負實數而且需要滿足

\[\varphi(v_1,\cdots,v_n)=\sum\limits_{i=0}^{n}k_i(v_i-v^{\prime}_i)^2s_i-E_U=0 \]

\(\texttt{Data Range:}1\leq n\leq 1000\)

題解

拉格朗日乘子法。

我們來形象的講一下這個東西到底是什麼,你可能需要一些關於多元函式微積分的知識。

假設我們現在有一個二元函式 \(F(x,y)=x^2+y^2\),需要求這個東西的最小值。

如果沒有限制的話,最小值就是 \(0\)。但是我如果需要讓 \(x,y\) 滿足 \(x^2y=3\) 的話呢?

一個很顯然的想法就是去逐一列舉一個最小值 \(r\),然後看這個 \(r\) 滿不滿足條件。於是我們只需要看看 \(x^2y=3\)\(x^2+y^2=r\) 是否相交。

注意到後者的影象是一個圓,從小到大列舉 \(r\) 的過程可以看做是圓的半徑逐漸擴大。如果擴大到某個 \(r\) 剛好與 \(x^2y=3\) 相交了的話就可以取這個 \(r\) 作為最小值。

這裡有一個 demo,拖動 \(r_0\)

的滑動條相當於是圓的半徑逐漸擴大。注意到隨著半徑的擴大,圓與曲線的位置關係是先相離再相切最後相交。所以說,在極值點,圓與曲線相切

注意到這些圓是可以看做 \(F(x,y)=x^2+y^2\) 的等高線的。注意到 \(\nabla F\)(也就是梯度)是等高線的法線。同時 另一個函式 \(G(x,y)=x^2y\) 的梯度向量 \(\nabla G\) 也會垂直於 \(x^2y=3\) 這條等高線。

因為梯度向量是等高線的法線,所以梯度與等高線的切線垂直。結合兩個加粗的條件我們可以知道在相切點,圓的梯度向量和曲線的梯度向量平行。這個時候我們可以列方程了:

\[\nabla F=\lambda\nabla G \]

也就是說

\[\begin{cases}\frac{\partial F}{\partial x}=\lambda\frac{\partial G}{\partial x}\\\frac{\partial F}{\partial y}=\lambda\frac{\partial G}{\partial y}\\x^2y=3\end{cases} \]

解出來即可。

這個時候我們可以考慮構造約束函式 \(\varphi(x,y)=x^2y-3\)。由於 \(3\) 是常數在對任何一個變數求偏導數的時候都會消去所以不會對上面兩個方程產生影響。

由於偏導數的可加性,我們增加一個新的變數 \(\lambda\) 並且將原來的函式寫成這樣:(也就是將等式的右邊移到了左邊)

\[F(x,y,\lambda)=F(x,y)+\lambda\varphi(x,y) \]

這個時候很容易看出對 \(F(x,y,\lambda)\) 求三個偏導數得到的方程與上面的方程組是一樣的。

對於這個題目來說,建構函式

\[F(v_1,\cdots,v_n,\lambda)=F(v_1,\cdots,v_n)+\lambda\varphi(v_1,\cdots,v_n)=\sum\limits_{i=1}^{n}\frac{s_i}{v_i}+\lambda\left(\sum\limits_{i=0}^{n}k_i(v_i-v^{\prime}_i)^2s_i-E_U\right) \]

根據對稱性我們可以很方便的求出這個東西對 \(v_i\)\(\lambda\) 的偏導數:

\[\frac{\partial F}{\partial v_i}=2\lambda k_i(v_i-v^\prime_i)s_i-\frac{s_i}{v_i^2} \]

\[\frac{\partial F}{\partial \lambda}=\sum\limits_{i=0}^{n}k_i(v_i-v^\prime_i)^2s_i-E_U \]

按照上面講的東西,這些偏導數都應該等於 \(0\) 的,所以得到兩個方程:

\[2\lambda k_iv_i^2(v_i-v^\prime_i)=1 \]

\[\sum k_i(v_i-v^{\prime})^2s_i=E_U \]

將第一個方程移項得到

\[v_i^2(v_i-v^\prime_i)=\frac{1}{2\lambda k_i} \]

左邊那個東西當 \(v_i\in [0,+\infty)\) 的時候通過對導數進行分析可以看出是單調遞增的。

\(\lambda\) 固定的時候,\(v_i\uparrow\) 則等式左邊 \(\uparrow\)。當 \(\lambda\uparrow\) 時,因為等式右邊 \(\downarrow\),所以 \(v_i\downarrow\)。因為 \(v_i>v^\prime_i\),所以第二個等式的左邊整體是 \(\downarrow\) 的。

於是我們可以考慮二分 \(\lambda\),然後二分解出 \(v_i\) 利用第二個等式再 check 即可。

程式碼

#include<bits/stdc++.h>
using namespace std;
typedef int ll;
typedef long long int li;
typedef long double db;
const ll MAXN=2e5+51;
const db eps=1e-12;
ll n;
db eu,l,r,mid,res;
db s[MAXN],kk[MAXN],v[MAXN],vl[MAXN];
inline ll read()
{
    register ll num=0,neg=1;
    register char ch=getchar();
    while(!isdigit(ch)&&ch!='-')
    {
        ch=getchar();
    }
    if(ch=='-')
    {
        neg=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        num=(num<<3)+(num<<1)+(ch-'0');
        ch=getchar();
    }
    return num*neg;
}
#define sqr(x) (x)*(x)
inline db calcDeriv(db lambda,db vel,ll x)
{
    return 2.0*lambda*kk[x]*sqr(vel)*(vel-v[x]);
}
inline ll check(db lambda)
{
    db e=0,l,r,mid;
    for(register int i=1;i<=n;i++)
    {
        l=max(v[i],0.0L),r=100000;
        while(l+eps<=r)
        {
            mid=(l+r)/2.0;
            calcDeriv(lambda,mid,i)<=1?l=mid:r=mid;
        }
        vl[i]=l,e+=kk[i]*sqr(vl[i]-v[i])*s[i];
    }
    return e<=eu;
}
int main()
{
    n=read(),scanf("%Lf",&eu),l=0,r=100000;
    for(register int i=1;i<=n;i++)
    {
        scanf("%Lf%Lf%Lf",&s[i],&kk[i],&v[i]);
    }
    while(l+eps<=r)
    {
        mid=(l+r)/2.0;
        check(mid)?r=mid:l=mid;
    }
    for(register int i=1;i<=n;i++)
    {
        res+=s[i]/vl[i];
    }
    printf("%.9Lf\n",res);
}