斜率優化講解


斜率優化講解

——by ysy

一、簡單的復習

       我在這里給出一個式子,\(f[i]=max(g[i]+calc(j))\),這是絕大部分dp式子的最基本的模型,每一道題可能只是將\(max\)改為\(min\),或者是將calc中的東西更改一下,大家思考一下是不是這樣的。

        如果當calc之中的每一項都只含有\(i\)或者是\(j\),並且這兩個字母沒有相乘的情況我們就可以用單調隊列,這個不難理解,舉個例子,像下面的這個式子:\(f[i]=max \{ f[j]+a∗num[j] \}\),就可以用單調隊列維護,因為整個式子之中只有關於\(i\)的單獨項和關於\(j\)的單獨項。

        但是像這樣的式子就不可以了:\(f[i]=max \{ f[j]+(sum[i]+sum[j])^2 \}\),因為這個式子展開后就會出現關於\(i\)的式子乘上關於\(j\)的式子。像這樣的式子就是斜率優化的適用范圍。

二、斜率優化

        像斜率優化這樣知識點需要一道例題來進行講解。下面我們來看一道經典的例題

1.列方程

        我們先想這道題的dp式子,先不管時間復雜度的問題。

        這個式子應該很好想:\(f[i]=min \{ f[j]+( \sum_{k=j+1}^{i} lenth[k] +i−j−l)^{2} \}\),我們來分析一下時間復雜度:\(O(n^3)\)

        想一下優化,我們是不是可以將求和部分寫成前綴和的形式?將\(\sum\)的部分化成\(sum[i]\)。這樣我們就可以將式子轉化成\(f[i]=min \{ f[j]+( sum[i] -sum[j] +i−j−l)^{2} \}\),這樣的話時間復雜度就降低成為\(O(n^2)\)。時間是更低了,但是還是過不了啊,這是我們就要等價地變換式子,使其成為y=kx+b的形式,這個形式就是斜率優化的核心。

2.轉化式子

        \(f[i]=min \{ f[j]+( sum[i] -sum[j] +i−j−l)^{2} \} \downarrow\)

        \(f[i]= f[j] + [ ( sum[i] + i ) - ( sum[j] + j ) - l ]^2 \downarrow\)

        令\(s[i]=sum[i]+i \downarrow\)

        \(f[i]=f[j]+( s[i] -s[j] - l)^2\)

        \(f[i] = f[j] + s[i]^2 + ( s[j] + l ) ^2 - 2\times s[i] \times ( s[j] + l) \downarrow\)

        \(f[j] + s[i]^2 + ( s[j] + l )^2 = 2 \times s[i] \times ( s[j] +l ) + f[i]\)

3.分析式子

        \(f[j] + s[i]^2 + ( s[j] + l )^2 = 2 \times s[i] \times ( s[j] +l ) + f[i]\)

        觀察上面的式子,我們發現這個式子十分像一種函數,y=kx+b,可能大家會有疑問,這個式子和直線的表達是有什么形似之處呢?

        我們將\(f[j] + s[i]^2 + (s[j] + l)^2\)這個部分看做一個整體記為\(y\),這個部分可以看成一個整體的條件是:這個整體中的所有部分都是已求出的,並且當知道\(i\)\(j\)之后可以\(O(1)\)求出。顯然這個整體滿足。同理我們將\(2 \times s[i]\)\((s[i] + l)\)這兩個部分也分別看做整體,並分別記為\(k\)\(x\)。這樣式子就化為\(y=kx+f[i]\)

        下一步,我們建立以個平面直角坐標系,這個平面直角坐標系中的每一個點的坐標\((x,y) ​\)都對應的是上面式子中的\(x ​\)\(y ​\),這樣我們就能夠將每一個與\(i ​\)有關的東西處理完事之后標到平面直角坐標系之中。每一個點的坐標表成\(( s[i],f[i] + (s[i] + l)^2 ) ​\),可能有人會問為什么縱坐標沒有了\(s[i]^2 ​\),並且橫坐標沒有了\(l ​\),這個問題下面會解答,請稍作等待。

        如果我們想用\(j\)來轉移\(i\)的話,就要讓斜率為\(2 \times s[i]\)的直線過點\((s[j],f[j] + (s[j] + l)^2)\),並且此時直線的截距就是新的\(f[i]\),因為\(f[i]\)為這條直線的\(b\)。再看下面的圖解,我們將求過的點都標到平面直角坐標系中,我們可以發現,我們想過的這個點一定在我們維護的大圓包上,像點2這樣的點就不能被用來更新,因為過點3所得截距,一定比過點2所得截距小,那么我們能發現當點3求出之后,只要比較一下,點2和點3形成的直線的斜率和點1和點2形成的直線的斜率,如果2、3形成的比1、2形成的要小,那么3號點一定比2號點更優。我們再看,假設下圖之中已經維護好1到5的所有點,那么就會出現這樣的大圓包。我們用求出6的點的直線去和這些點相交,我們發現只有點4在當前直線上時能使截距最小(畫一畫圖就能發現是過點4時,直線的截距最小),根據是由點4轉移,我們可以發現,當兩個點1、3的斜率小於\(2 \times s[i]\)的時候,橫坐標小的點一定不能用來轉移,同理斜率大於\(2 \times s[i]\)的兩個點,橫坐標大的也不能夠用來轉移,這個性質是不是很好?

        根據上面我們發現的式子,我們可以維護一個類似於單調隊列的隊列來維護我們的大圓包。但是這個大圓包具體怎么維護呢?我們先看如何求斜率。如果給你直線上的兩個點,我想大家一定會求斜率。就是兩點的縱坐標相減的差除上兩點的橫坐標相減的差。這里也就解釋了,為什么上文中的縱坐標沒有了\(s[i]^2\),因為兩式相減時\(s[i]\)是相同的,從而\(s[i]^2\)也就是相同的,所以相減時就將其減掉了,因此\(s[i]^2\)不用出現在縱坐標之中。同理在相減時我們的橫坐標也不需要\(l\)

double re_x(int i){return s[i];}
double re_y(int i){return f[i]+(s[i]+l)*(s[i]+l);}
double re_k(int i,int j){return (re_y(j)-re_y(i))/(re_x(j)-re_x(i));}

        會求斜率了,我們再來看怎么維護大圓包,我們發現當隊列中最后一個的點和隊列中倒數第二個點的產生斜率大於最后一個點和新產生的點產生的斜率,那么結尾就要彈出隊列,這個用一個\(whlie\)循環就能夠解決,最后再將新產生的點放在結尾。這個實現十分像單調隊列的實現。

int main()
{
    while(head<tail&&re_k(q[tail],i)<re_k(q[tail],q[tail-1])) tail--;
    q[++tail]=i;
}

        我們再看,怎么滿足第二個性質,讓更新變成\(O(1)\)的?我們發現當隊列中第一個點和第二個點產生的斜率如果小於當前的直線,那么第二個點更新一定比第一個點更新更優,我們就要進行隊首彈出。這個過程也十分像單調隊列的維護。最后直接用隊首進行更新。

int main()
{
    while(head<tail&&re_k(q[head],q[head+1])<2*s[i]) head++;
    f[i]=f[q[head]]+(s[i]-s[q[head]]-l-1)*(s[i]-s[q[head]]-l-1);
}

        這樣我們就解決了維護的問題,最后就是將這些組裝在一起,形成下方的代碼。

#include <stdio.h>
#define N 50001
int n,l,head,tail;
long long f[N],s[N],q[N];
double re_x(int i){return s[i];}
double re_y(int i){return f[i]+(s[i]+l)*(s[i]+l);}
double re_k(int i,int j){return (re_y(j)-re_y(i))/(re_x(j)-re_x(i));}
int main()
{
    scanf("%d%d",&n,&l);
    for(int i=1;i<=n;i++)
		scanf("%lld",&s[i]),s[i]+=s[i-1];
    for(int i=1;i<=n;i++) s[i]+=i;
    q[tail]=0;
    for(int i=1;i<=n;i++)
    {
		while(head<tail&&re_k(q[head],q[head+1])<2*s[i]) head++;
		f[i]=f[q[head]]+(s[i]-s[q[head]]-l-1)*(s[i]-s[q[head]]-l-1);
		while(head<tail&&re_k(q[tail],i)<re_k(q[tail],q[tail-1])) tail--;
		q[++tail]=i;
    }
    printf("%lld\n",f[n]);
}
4.分析上方代碼的適用范圍

​        上方的代碼是有一定的適用范圍的,大家想一下,為什么我們敢彈出隊首與隊尾?

        我們再來看一下題目,這個題目顯然滿足一個特點,就是由於我們將\(s[i]\)定義為前綴和,所以他一定是單調遞增的,並且我們的點的橫坐標也是\(s[i]\)也滿足單調遞增。這兩個性質十分好。我們把隊首的元素彈出的條件是斜率小於\(2 \times s[i]\),因為\(s[i]\)滿足單調遞增,所以彈出時小於,那以后就一定一直小於下去,所以彈出就彈出了。我們再看,因為我們的橫坐標滿足單調遞增,所以每一次插入點都會在最后,因此結尾彈出也是正確的。

​        但是如果斜率沒有單調性呢?我們就不能將隊首彈出,這樣我們就不能在\(O(1)\)的時間內求出新的元素,我們可以在大圓包上進行二分。我們看下面的大圓包,會發現只有當前點和上一個點的斜率小於直線斜率,並且和下一個點的斜率大於直線的斜率時,這個點才是最優的。所以我們可以進行二分查找。

​        如果我們的橫坐標沒有單調性呢?我們就不能夠將隊尾刪掉了,我們應該用平衡樹來維護,動態維護大圓包。但是怎么維護呢?我們可以運用\(splay\),具體請聽本人口述。

三、練習

1.倉庫建設

        \(1)\)列方程,並轉化形式

                \(f[i] = min ( f[j] + x[i] \times ( P[i] - P[j]) + g[i] - g[j] + c[i]) \downarrow\)

​                $ f[i] = f[j] + x[i] \times P[i] - x[i] \times P[j] +g[i] -g[j] +c[i] \downarrow$

                \(f[j] - g[j] + x[i] \times P[i] +g[i] + c[i] = x[i] \times P[j] + f[i]\)

        \(2) ​\)找點

                顯然這里的點就是\(( f[j] - g[j] ,P[j] )\),斜率就是\(x[i]\),截距就是\(f[i]\)

        \(3)\)寫吧

2.剩下的習題

        土地購買特別行動隊防御准備序列分割小p的牧場征途


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM