C++剑指offer:解题报告之DP优化学习记 (二) ——浅论DP斜率优化 (Print Article 【HDU - 3507】 )
鏈接:https://share.weiyun.com/5LzbzAc
目錄
前言
斜率優化前期準備
1.從狀態轉移方程出發
2.推理狀態轉移方程
對結論的進一步推導
干貨!綜合結論
?判斷斜率大小的方法:叉乘
正片開始:代碼部分
后記
前言
之前我們對DP優化已經有了很多的了解,比如:單調隊列優化DP,四邊形優化DP。
今天,我們要講的是傳說中的斜率優化。它與四邊形優化相似,都是從數學角度上來推理。斜率優化在四邊形優化上又需要更復雜的數學推理,其中涉及到的求一條直線的斜率,以及運用叉乘來判斷兩條直線的斜率大小,都足以讓初學者望而生畏。但只要理解透了其中的數學方法,也就是那么回事,挺簡單的(我們老師說能第一次理解的人很少,所以如果你看不懂得話,可以第二天再來看一遍)。我在理解斜率優化時也是非常的煎熬,現在也是剛剛入門,所以有些地方寫的有紕漏的話請見諒。
斜率優化前期準備
1.從狀態轉移方程出發
既然是一種優化DP的方法,那么他的起點就一定離不開狀態轉移方程。所以讓我們來看一下這個轉移方程
這是一個很簡單的狀態轉移方程,很容易想到用單調隊列優化把時間復雜度降到。(如果你不知道單調隊列優化DP,應該看看,因為接下來的斜率優化也要用到它)
How about this?(那么這個轉移方程呢?)
觀察這個轉移方程,現在它還能用單調隊列優化嗎?顯然不能。拆開這一項,會得到,它不能分解為只與i或j有關的部分。于是上面的單調隊列優化方法就不好使了,于是就引出了我們今天要學的內容:斜率優化了。
先來看看此題:Print Article
?題目大意:輸出N個數字a[N],輸出的時候可以連續的輸出,每連續輸出一串,它的費用是 “這串數字和的平方加上一個常數M”。n<=500000,求最下的費用。
此題轉移方程就是(sum為前綴和也就是a1+a2+...+ai)
假設i是當前循環最外層,也就是可以假設它是一個常量。在不優化的情況下,我們是把j從1到i-1循環,找出最小決策點j來使最小。所以我們的主要任務就是找出最優決策點j。下面讓我們來看看是怎么推出斜率優化的。
2.推理狀態轉移方程
我們考慮兩個決策點k與j,如果決策j更優,那么也就是,好了,現在只要用一下你們的紙和筆,就可以推出這個式子:
似乎只能化簡這么多了。但讓我們觀察不等式的右邊這一項,因為我們已經規定了sum為前綴和數組;
所以如果j>k,則sum[j]-sum[k]是>0的,化簡后得;
如果j<k,則sum[j]-sum[k]<0,得
觀察這個式子,我們可以把設為,設為(相當于把x,y,看成兩個函數),則上式可以化簡為:
如果有j>k,則sum[j]-sum[k]<0,有等價于決策j優于k
如果有j<k,則sum[j]-sum[k]<0,有等價于決策j優于k
(這里講的有點快,說一下,sum[i]已經被設為常數了)
既然我們已經把他們設為x,y了,則可以把這兩個式子代入平面直角坐標系里(滑稽),那么j和k就是坐標系上的兩個點,所以就是兩個坐標的直線的斜率(如果你不懂什么是斜率,請看這里)。
那么我們就可以得出我們的第一個結論:如果兩個決策點的斜率小于sum[i],則靠后的決策點更優;否則靠前的決策點更優。——結論1.斜率則是程序中是用函數來實現的。
也就是說在我們碰到其他題時,也需要用類似的方法對狀態轉移方程進行化簡,并求出函數x和函數y.
對結論的進一步推導
盡管我們已經得出了一個逼格很高的結論,但這對于斜率優化是遠遠不夠的,所以我們還要對此結論進一步推導。
先設一個函數g(i,j)為點i和點j之間的斜率,也就是。然后,讓我們考慮3個決策點的情況:i,j,k,k<j<i且(也就是k到j的斜率大于i到j的斜率。為了方便理解,這里給出圖像)。
下面讓我們利用我們剛才得出的結論1,?升級我們的結論。
可以分三種情況來討論,設結論1里的sum[i]為一個常量P。
(當我們在考慮斜率大小時,我們可以這樣想:斜率越大,直線就越斜。上圖中的就比要斜)
這里貼出結論方便結合圖理解:如果兩個決策點的斜率小于P,則靠后的決策點更優;否則靠前的決策點更優。
?1.如果與均小于P,則j比i優,k比j優。所以最優決策點為k
?2.如果與均大于P,則j比k優,i比j優。所以最優決策點為i
?3.如果g[i][j]<P且g[j][k]>P,則i比j優,k比j優。所以最優決策點不為j
根據對3個情況的推理,我們發現不論如何,j都無法成為最佳決策點,所以可以排除j。于是就得出了我們的第2個論斷:所有的決策點滿足一個下凸包性質,也就是最優決策點的斜率是單調遞增的。
下凸包就是如圖的情況:
這里我們就可以代入單調隊列了。為什么?因為由上述我們得到的結論,我們知道這些最優決策點相鄰之間的斜率是單調遞增的。將這些決策點放入隊列,這個隊列就具有了單調性,也就是可以用單調隊列來實現了。
設這個單調隊列q里存的決策點a,b,a<b,當前的數組sum[i]是會隨著i遞增的;當我們的最優決策點取在b時,b前面的所有決策點都將無效!為什么?因為sum[i]是遞增的,所以需要我們匹配的斜率也是越來越大的,因為a<b,a前面的所有決策點都小于a,所以a及a前面的所有決策點都將無效。(在程序中,我們在單調隊列之中的實現就是彈出隊頭。)但隨著i的增加,b也可能不再適用,于是可以拿b與b后一個點c的斜率與sum[i]作比較:如果<sum[i],則彈出b。知道為止。
干貨!綜合結論
所以我們對程序的優化應該這樣來實現:
?1,用一個單調隊列來維護所有決策點。
?2,找最佳決策點時,設當前求解狀態為i,從隊頭開始,如果已有元素a b c,當i點要求解時,如果<sum[i],那么說明b點比a點更優,a點可以排除,于是a出隊,直到第一次遇到>sum[i],此時j-1即為最佳決策點。
?3,假設隊列中從頭到尾已經有元素a b c。那么當d要入隊的時候,我們維護隊列的下凸性質,即如果<,那么就將c點刪除。直到找到>=為止,并將d點加入在該位置中。
Ps:寫到這里有點寫不下去了,于是直接粘貼了老師課件的原話。所以結合一下待會兒的代碼湊合著看吧。。。。。。
?判斷斜率大小的方法:叉乘
(這個部分原來是沒有的,現在抽出空來補充一下)
因為上文涉及到判斷斜率的大小,但通常我們判斷斜率是直接通過比較大小來比較的。雖然這種方法很方便,但因為涉及到除法的精度問題,這里引入一個叉乘的概念。
要運用叉乘來判斷斜率的大小,我們還要知道一個東西叫向量,通常用字母p來表示。
向量可以看成一條線段。在平面直角坐標系中,如果有一條直線,它的一個端點坐標為(x1,y1),另一個端點坐標為(x2,y2)。
那么這條線段的線段就為p(x1-x2,y1-y2)。(相當于把這條線段平移到了原點)
(記住,結論中我們也將那些決策點連成了線段)
設有二向量p1(x3,y3),p2(x4,y4)。
則它們的叉乘p1×p2=(x3*y4-x4*y3)。在物理上,它們可以表示為一個平行四邊形的有向面積。所以:
?若p1×p2>0,則p2在p1的逆時針方向;
?若p1×p2<0,則p2在p1的順時針方向;
?若p1×p2=0,則p1與p2方向重合。
所以我們可以就可以結合上面的結論的最后一條。因為我們要保證最后隊尾三個決策點為下凸包,所以我們在從隊尾入隊時,先將向量求出來,再用叉乘判斷它們是否為下凸包。如果是,則退出循環;如果不是,則彈出隊尾。
正片開始:代碼部分
注釋已經打好了,結合著上面的結論看吧.
#include <iostream> #include <cstdio> #include <cstring> #include <queue>using namespace std;#define infI 0x3f #define infL 1LL<<60 #define N 500010 #define LL long long #define mem(a,n) memset(a,n,sizeof(a));LL read() {LL f=1,s=0;char a=getchar();while(!(a>='0'&&a<='9')) { if(a=='-') f=-1 ; a=getchar(); }while(a>='0'&&a<='9') { s=s*10+a-'0'; a=getchar();}return f*s; }LL sum[N],f[N],q[N]; int head,tail=1,n,m;LL px(LL i) {//求一個點的橫坐標xreturn sum[i]*2; }LL py(LL i) {//求一個點的縱坐標yreturn sum[i]*sum[i]+f[i]; }int main() {while(cin>>n>>m) {//循環讀入mem(f,0);mem(q,0);head=0,tail=1;for(int i=1;i<=n;i++)//初始化sum數組sum[i]=read();for(int i=1;i<=n;i++)sum[i]+=sum[i-1];for(int i=1;i<=n;i++) {while( head+1<tail && (py(q[head+1])-py(q[head])) <= sum[i]*( px(q[head+1]) - px(q[head] ) ) )//實現總結論的第2條;(py(q[head+1])-py(q[head]))求的是斜率。//PS:你們可能看不懂,這里為了避免循環的精度問題進行了移項。head++;f[i]=f[q[head]]+m+(sum[i]-sum[q[head]])*(sum[i]-sum[q[head]]);//動態規劃while( head+1 < tail && (px(q[tail-1])-px(q[tail-2]))*(py(i)-py(q[tail-1])) <= (px(i)-px(q[tail-1]))*(py(q[tail-1])-py(q[tail-2])))//判斷斜率大小,這里運用了叉乘的方法。 tail--;q[tail++]=i;}cout<<f[n]<<endl;}}?
后記
在寫博客的過程中,我自己也感覺到了對斜率優化不是很了解,相當于復習了一遍,還是很感慨的.
總結
以上是生活随笔為你收集整理的C++剑指offer:解题报告之DP优化学习记 (二) ——浅论DP斜率优化 (Print Article 【HDU - 3507】 )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python5 5的 阵列_Biopyt
- 下一篇: s3c2440移植MQTT