1. 程式人生 > >【筆記篇】斜率優化dp(一) HNOI2008玩具裝箱

【筆記篇】斜率優化dp(一) HNOI2008玩具裝箱

公式 現在 getchar() 就是 clu cst 差距 直接 source

斜率優化dp

本來想直接肝這玩意的結果還是被忽悠著做了兩道數論
現在整天渾渾噩噩無心學習甚至都不是太想頹廢是不是藥丸的表現
各位要知道我就是故意要打刪除線並不是因為排版錯亂
反正就是一個del標簽嘛並不是什麽大事的說
講道理這一篇要不是寫laTex我就直接用html寫了
Emmmm劃掉的原因是因為跟正題一點關系都沒有啊
不讓自己寫摘要我寫第一段湊摘要好咯
第一次寫花花綠綠的blog感覺還是很新鮮的
你看看我到了正文部分還劃不劃啊(該劃的還是劃╭(╯^╰)╮)
其實文章裏有彩蛋比如這裏 被你發現了OvO 哈哈
據說找到了所有的彩蛋能獲得一些獎勵(但很不幸這是假的
不過laTex公式也可以帶顏色
這一段對的如此不整齊的原因可能是為了逼死強迫癥

明明是懶得把字數扣成一樣的非要找這麽個冠冕堂皇的借口麽

上次我們說過
\[ f[i]=max\{f[j]+x[j]\}(j\in[1,i)) \]
這樣的方程的優化(變量記錄)和
\[ f[i]=max\{f[j]+x[j]\} (j\in[i-m,i)) \]
這樣的方程的優化(單調隊列), 但是如果遇到
\[ f[i]=max\{f[j]+g(i,j)\} (j\in[1,i) \]
這樣的方程, 就不會處理了... 這也就是今天要講的斜率優化dp.
對於這種方程, 我們考慮將這個式子化成\(y\)=\(k\)\(x\)+\(b\)的形式, 其中\(y\)\(x\)是只和\(j\)相關的式子

, \(k\)是和\(i\)相關的式子, \(b\)\(f[i]\)和其他什麽常數, 然後根據類似於線性規劃的知識維護一定的單調性來優化轉移.

光這麽講也沒什麽意思嘛(鬼能看懂咯), 我們看例題. 其實就是bzoj1010 第一頁就有 傳送門在這裏~

狀態轉移方程比較顯然: 令\(f[i]\)表示裝前\(i\)個東西的最小花費, \(sum[i]\)表示\(C_i\)的前綴和, 則
\[ f[i]=min\{f[j]+(sum[i]-sum[j]+i-j-1-L)^2\} \]
這樣我們已經可以\(O(n^2)\)做了, 不過並沒有給部分分, 不知道能得幾分, 反正\(n\leq5W\)想A掉是不可能的.這輩子都是不可能的. dp優化以後跑得又快, 又可以裝逼, 我超喜歡優化的!

所以很明顯需要優化. 那麽就是說要化式子.我討厭化式子!!!
為了方便, 我們令\(s[i]=sum[i]+i,C=L+1\), 那麽
\[f[i]=f[j]+(s[i]-s[j]-C)^2=f[j]+(s[i]-C)^2-2(s[i]-C)*s[j]+s[j]^2\]
然後移項, 得到
\(f[j]+s[j]^2\)=\(2(s[i]-C)\)*\(s[j]\)+\(f[i]-(s[i]-C)^2\)
這樣就出現了\(y\)=\(k\)\(x+\)\(b\)的形式. 根據線性規劃一類的知識, 我們要在可行域中最小化\(f[i]\), 就是要最小化\(截距b\). 話說化帶顏色的式子的時候的laTex也是挺好看的..
那麽怎麽最小化截距呢? 我們畫個圖. 畫的很醜大家輕噴...
技術分享圖片
首先很明顯我們dp的時候求好\(t\)的狀態可以存在一個點\((x,\)\(y\)\()\)裏面, 即\((s[t],\)\(f[t]-s[t]^2\)\()\). 這樣我們每次就拿一條斜率為\(2(s[i]-C)\)的直線去裏面找截距的最小值就行了. 但這樣還是\(O(n^2)\)的, 因為我們沒有充分利用單調性來去掉那些根本不可能優的點.
技術分享圖片Emmmm其實這張圖畫得更草率OvO
我們可以看到 如果將要插入的點是紅色的, 那麽無論斜率怎樣, 它都不會比已經加入過的點優(截距更小), 我們就不需要考慮紅色點了, 但是藍色的點則是可能更優的, 那麽我們就要保留藍色點.
其實這裏要證明決策單調性什麽的 但畫圖就顯得很直觀 顯然成立我們就不證了(明明是因為懶←_←
經過若幹個點的驗證, 我們發現, 可能更優的點都集中在下凸殼上!
那麽我們維護下凸殼就好了. 這樣的話轉移的復雜度似乎從\(O(n)\)降到了\(O(H)\), 但似乎還是該怎麽過不去就怎麽過不去.
所以繼續考慮, 為什麽轉移的時候非要遍歷所有點呢?

我們驚奇地發現, 這個題有一個性質, 就是斜率\(k=2(s[i]-C)\)遞增的, 所以吧,
技術分享圖片
我們枚舉下一個斜率的時候, 可以發現, 如果這個點與下一個點的連線的斜率比枚舉的斜率小(前兩個點), 那麽截距就會比下一個點大, 而且由於斜率遞增, 所以差距會越來越大, 那這個點我們就可以不要了. 所以我們就可以看出可以用一個單調隊列來維護這個凸殼, 每次先把隊首不合法的彈出, 然後取隊首轉移即可. 其實這裏關於斜率的討論應該是要化一波覆蓋的式子的, 但是也是由圖顯然, 而且做半平面交的時候化過, 這裏就不化了(顯然還是因為懶←_←

這樣的話轉移的時間復雜度就從\(O(H)\)降到了\(O(1)\), 總復雜度也就降到了\(O(n)\)的水平, 就可以非常愉快的通過此題啦~ 所以不是很懂一個正解\(O(n)\)的題目給個\(n\leq5W\)的數據範圍是幾個意思...

驚奇地發現自己調了一個晚上的原因竟然是化式子移項正負號反了??? 應該打回小學重造.
本來想用叉積結果化出來的式子太臃腫 我當時還不知道自己式子化錯了然後不好調就刪掉改成斜率了.
非常不喜歡這樣損精度的方式但其實都是整數所以還好, 但是後來發現式子化錯了之後懶得改就這麽寫下去了...
有一點就是5W*1e7要開long long.. 其實代碼非常簡單... 就這麽幾行你還調了一晚上←_←

#include <cmath>
#include <cstdio>
const int N=5e4+5;
typedef long long LL;
inline int gn(int a=0,char c=0){
    for(;c<'0'||c>'9';c=getchar());
    for(;c>47&&c<58;c=getchar())a=a*10+c-48;return a;
}
LL s[N],f[N];int q[N],h=0,t=0,n,l;
double slope(int x,int y){return 1.0*(f[x]+s[x]*s[x]-f[y]-s[y]*s[y])/(s[x]-s[y]);}
int main(){
    n=gn(),l=gn()+1;
    for(int i=1;i<=n;++i) s[i]=s[i-1]+gn()+1;
    for(int i=1;i<=n;++i){
        while(h<t&&slope(q[h],q[h+1])<=2*(s[i]-l)) ++h;
        f[i]=f[q[h]]+(s[i]-s[q[h]]-l)*(s[i]-s[q[h]]-l);
        while(h<t&&slope(q[t],q[t-1])>=slope(i,q[t])) --t;
        q[++t]=i;
    } printf("%lld",f[n]);
}

我覺得代碼寫的非常清楚了 就這樣吧...
怎麽樣,找到所有透明的字了嘛?? 哈哈哈~

【筆記篇】斜率優化dp(一) HNOI2008玩具裝箱