[模板] 斜率優化dp詳解
演算法簡介
今天xinyue講了斜率優化,全程懵逼,居然還有這麼牛逼的東西。
於是與achen討論了一下,總結一些東西。
斜率優化Dp其實是單調佇列的推廣,單調佇列、旋轉卡殼、斜率優化都利用了單調性降低時間複雜度。
演算法簡介
舉個例子
有些動規狀態轉移方程可以寫成
f[i]=min/max{f[j]+…+x[i]},省略號中只有與j有關的變數。
我們就可以用單調佇列進行優化,使O(n^2)降為O(n)
但是對於這一類的方程:
f[i]=min/max{f[j]+(x[i]-x[j])^2}
展開後得到
f[i]=min/max{f[j]+x[i]^2+x[j]^2-2*x[i]*x[j]
紅色部分不僅有j相關的量,還有與i有關的量,從而使單調佇列失效。
此時我們就需要用到斜率優化。
引例
hdu3507
題目大意:
給出N個單詞,每個單詞有個非負權值Ci,現要將它們分成連續的若干段,每段的代價為此段單詞的權值和,還要加一個常數M,即(∑Ci)^2+M。現在想求出一種最優方案,使得總費用之和最小。
容易寫出方程:
f[i]=min{f[j]+(s[i]-s[j])^2+M}(0<=j<=i-1)
其中s是字首和
可是範圍是500000,又不能用單調佇列,那怎麼辦呢?
演算法核心
數學分析法見此大爺部落格,講的挺詳細->傳送門
以下談談斜率優化的數形結合理解方法:
以下純粹空談,請結合引例分析理解。
根據動規方程狀態i從狀態j轉化而來,
我們可以化成類似f[i]=(f[j]+…)+(-i*f[i-1]*f[j])+(i+…)的形式
其中藍色部分只與j有關,紅色部分與i,j有關,綠色部分只與i或常數有關
我們可以固定i,故變形為
f[i]-i-…=(f[j]+…)+(-i*f[i-1]*f[j])
考慮幾何意義,
令藍色部分為y,紅色部分中的i部分為導數k,紅色部分中j部分為x,綠色部分只是常數,在幾何意義上只是截距,與單調性無關,可以設為B
故得y=kx-B
是不是很像直線方程?
假設關於i的部分>0且隨著i增加而增加,求的是最小值
則k隨著i增加而增加,對於有效點集我們可以畫出下圖。
是不是很像一個下凸包?
我們用當前的斜率k從下方去不斷逼近下凸包,最終會先碰到哪一個點?
一定是與斜率最相近的點,因為函式單調遞增,故小於斜率的決策肯定沒有後面的優,捨去。
因此我們可以用一個類似單調佇列的雙端佇列來維護狀態j,以下是實現方案:
若導數小於當前斜率,舍掉隊首。
根據方程使用隊首取出j算出當前f[i]的值
接著我們要加入結點i,但還得維護佇列的下凸性,如果加入結點i破壞了下凸性,就彈去隊尾,直到下凸位置(請dalao們不要吐槽示意圖,沒時間改了)
因此可以得到O(n)的演算法
當然,若方程關於i的部分隨著i增加而減小,且>0,則維護上凸性。以此類推,樹形結合分析。
核心程式碼如下:
int Left=1,Right=1;
Q[1]=0;
f[0]=0;
for(int i=1; i<=n; i++) {
while(Left<Right&&Slope(Q[Left],Q[Left+1])<=sumd[i])Left++; //維護隊首(刪除非最優決策)
int Front=Q[Left]; //取出佇列中最優元素j
f[i]=Cal(i,Front); //根據方程計算當前f
while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--; //維護隊尾(維護下凸包性質)
Q[++Right]=i; //入隊
}
引例分析
f[i]=min{f[j]+(s[i]-s[j])^2+M}
展開得
f[i]=min{f[j]+s[i]^2+s[j]^2-2*s[i]*s[j]+M}
令f[i]=B,f[j]+s[j]^2=y,2*s[j]=x,k=s[i]
因此k*x+B=y
k是s[i],字首和隨著i增大而增大,因為求最小值,故維護下凸包。
#include<algorithm>
#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<vector>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
inline const int Get_Int() {
int num=0,bj=1;
char x=getchar();
while(x<'0'||x>'9') {
if(x=='-')bj=-1;
x=getchar();
}
while(x>='0'&&x<='9') {
num=num*10+x-'0';
x=getchar();
}
return num*bj;
}
long long n,m,Q[500005],f[500005],s[500005];
double Slope(long long j,long long k) { //求斜率
return double((f[j]+s[j]*s[j])-(f[k]+s[k]*s[k]))/(2*s[j]-2*s[k]);
}
int main() {
while(scanf("%lld%lld",&n,&m)!=EOF) {
for(int i=1; i<=n; i++)s[i]=s[i-1]+Get_Int();
int Left=1,Right=1;
Q[1]=0;
f[0]=0;
for(int i=1; i<=n; i++) {
while(Left<Right&&Slope(Q[Left],Q[Left+1])<=s[i])Left++; //維護隊首(刪除非最優決策)
int Front=Q[Left];
f[i]=f[Front]+(s[i]-s[Front])*(s[i]-s[Front])+m; //計算當前f
while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--; //維護隊尾(維護下凸包性質)
Q[++Right]=i; //入隊
}
printf("%lld\n",f[n]);
}
return 0;
}
經典例題
這裡留下的坑慢慢填吧,希望填的完。
後記
斜率優化這一部分比較難懂,讀者可以自己在紙上推算一下。
如果有疑問或者認為本文有問題請在下面↓提出,感謝大家的支援。