斜率優化講解
斜率優化講解
——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\) 的式子。像這樣的式子就是斜率優化的適用範圍。
二、斜率優化
像斜率優化這樣知識點需要一道例題來進行講解。下面我們來看一道經典的 ofollow,noindex" target="_blank">例題 。
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)\) 寫吧