斜率優化通常使用單調佇列輔助進行實現,用於優化 \(dp\) 的時間複雜度。
使用單調佇列優化 \(dp\) ,通常可以解決型如: \(dp[i]=min(f(j))+g(i)\) 的狀態轉移方程。其中 \(f(i)\) 是只關於 \(i\) 的函式, \(g(j)\) 是只關於 \(j\) 的函式。樸素的解決方法是在第二層迴圈中列舉 \(j\) 來實現最小值,時間複雜度為 \(o(n^2)\) 。可以使用單調佇列來維護這個最小值實現 \(o(n)\) 的時間複雜度。
而斜率優化利用上述方法進行改進,實現對於型如: \(dp[i]=min(f(i,j))+g(i)\) 的狀態轉移方程。對比第一種情況,可以發現函式 \(f\) 函式與兩個值 \(i,j\) 都有關,簡單地使用單調佇列是無法優化的。這時候就開始引入主題斜率優化了。
下面結合一道例題來具體詳解。題目來自於 \(hnoi2008\) 省選題目。
有 \(n\) 個數字 \(c_1\) , \(c_2...c_n\) ,把它分為若干組,給出另乙個數 \(l\) ,設每組的第乙個數下標為 \(i\) ,最後乙個數下標為 \(j\) ,則每組的花費為\((i-j+\sum_^jc_k-l)^2\),總花費為所有組的花費之和。求最小總花費。
先考慮樸素的 \(dp\) 做法。
設 \(dp[i]\) 為將前 \(i\) 個數字分組後的最小花費。求和可以考慮使用字首和來優化,設字首和陣列為 \(pre\) 。則狀態轉移方程可以寫為:
\(dp[i]=min(dp[j]+(sum[i]-sum[j])+(i-(j+1))-l)^2,0≤j<i)\)
即是:\(dp[i]=min(dp[j]+(sum[i]-sum[j]+i-j-l-1)^2,0≤j<i)\)
那麼 \(sum\) 陣列可以初始化為:
for(int i = 1; i <= n; i++)
設 \(pre[i]=sum[i]+i\) ,再進一步設 \(l=l+1\) 那麼狀態轉移方程可以寫為:
\(dp[i]=min(dp[j]+(pre[i]-pre[j]-l)^2,0≤j
狀態轉移
int get_dp(int i, int j)
\(pre\) 陣列就可以進一步寫為:
for(int i = 1; i <= n; i++)
若列舉 \(j\) ,則時間複雜度為 \(o(n)^2\) ,時間複雜度不優。使用斜率優化可以對其進行優化。
假設當前列舉到 \(i\) ,需要得到 \(i\) 的狀態。假設有兩個決策點 \(j\) , \(k\) ,滿足決策點 \(j\) 優於決策點 \(k\) 。用符號語言可以表達為:
\(dp[j]+(pre[i]-pre[j]-l)^2
展開得:
\(dp[j]+pre[i]^2+pre[j]^2+l^2-2\times pre[i]\times pre[j]-2\times l\times pre[i]+2\times l\times pre[j]
\(dp[j]+pre[j]^2-dp[k]-pre[k]^2
觀察可得:左邊的式子只與 \(j\) 和 \(k\) 有關,但右邊的式子還與 \(i\) 有關。也可以發現若滿足上述式子,則會有 \(j\) 優於 \(k\) 。再分類討論:
\(j>k\) ,則 \(pre[j]>pre[k]\),移項得 \(\frac, \(2\times (pre[i]-l)\) 可以 看為乙個常數。那麼意味著點 \(j(dp[j]+pre[j]^2,pre[j])\) 與點 \(k(dp[k]+pre[k]^2,pre[k])\) 所構成的直線的斜率小於 \(2\times (pre[i]-l)\) 這個常數。
\(j,則 \(pre[j],移項得 \(\frac>pre[i]-l\) , \(2\times (pre[i]-l)\) 可以 看為乙個常數。那麼意味著點 \(j(dp[j]+pre[j]^2,pre[j])\) 與點 \(k(dp[k]+pre[k]^2,pre[k])\) 所構成的直線的斜率大於 \(2\times (pre[i]-l)\) 這個常數。
獲得分子的函式:
int get_up(int j, int k)
獲得分母的函式:
int get_down(int j, int k)
有了上述的一級結論,可以進一步推導出二級結論:
設 \(x,y\) 的斜率表示為 \(k(x,y)\) 。若存在三點 \(a,b,c\) ,有 \(k(a,b)>k(b,c)\) ,即是影象形成上凸的形狀時,那麼點 \(b\) 絕對不是最優的。
分類討論:
\(k(a,b)>k(b,c)>pre[i]-l\) ,則對於上述結論可以得出 \(a\) 比 \(b\) 更優,捨去 \(b\) 。
\(pre[i]-l>k(a,b)>k(b,c)\) ,則對於上述結論可以得出 \(c\) 比 \(b\) 更優,捨去 \(b\) 。
\(pre[i]-l且 \(pre[i]-l>k(b,c)\) ,則對於上述結論可以得出 \(a\) 和 \(c\) 都比 \(b\) 更優,捨去 \(b\) 。
那麼就可以得出答案的點必須滿足 \(k(a_1,a_2)。全部呈現出下凸狀態,如下圖。
這樣下標遞增,斜率遞增的點集可以使用單調佇列來維護。
找出當前最優的點為 \(que[head]\) ,即隊頭元素。
while(get_up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * get_down(que[head + 1], que[head]) && head < tail)
head++;
用當前點 \(i\) 來更新佇列,使得該佇列呈下凸之勢。
while(get_up(que[tail], que[tail - 1]) * get_down(i, que[tail]) >= get_up(i, que[tail]) * get_down(que[tail], que[tail - 1]) && head < tail)
tail--;
按照上述方法進行狀態轉移,得到的 \(dp[n]\) 就是當前的最優解。
注意要開 \(long\)
\(long\)
#include #define int long long//注意開long long吖( ⊙ o ⊙ )!
void quick_read(int &n)
while(c >= '0' && c <= '9')
n *= op;
}void quick_write(int n)
if(n >= 10)
quick_write(n / 10);
putchar(n % 10 + 48);
}const int maxn = 5e5 + 5;
int dp[maxn];
int pre[maxn], val[maxn];
int n, l;
int que[maxn];
int head, tail;
int get_dp(int i, int j)
int get_up(int j, int k)
int get_down(int j, int k)
void line_dp()
quick_write(dp[n]);//輸出答案
}void read()
}signed main()
動態規劃 斜率優化
一 引用 一般dp 方程可以轉化成 dp i f j x i 的形式,其中 f j 中儲存了只與 j相關的量。這樣的 dp方程可以用單調佇列進行優化,從而使得 o n 2 的複雜度降到 o n 可是並不是所有的方程都可以轉化成上面的形式,舉個例子 dp i dp j x i x j x i x j ...
斜率優化動態規劃
fi min fj s i2 sj l 2 2 si s j l fi min f j s i2 sj l 2 2si sj l 形如這個式子關於i的項與關於j的項混雜 相乘 的狀態轉移方程,可以使用斜率優化來加速 接下來以優化這個式子為例說說斜率優化.將上方給出的式子去掉 min min,僅關於j...
動態規劃 單調斜率優化DP
acwing 1087.修剪草坪 旅行商問題 輸入樣例 641 351 23 輸出樣例 acwing 1087.修剪草坪 動態規劃 f i max f i 1 f i j 1 sum i j sum i f i 表示從前i個中選,且合法的方案數。令x i j,則有 f i min f i 1 f x...