斜率優化DP學習筆記
最近瞎搞了搞斜率優化...
斜率優化DP是一種優化DP的思想,通過高中數學線性規劃那套理論加上凸包那套理論單調的優良性質將O(N^2)的DP列舉轉移優化成O(N)的轉移
先來一道簡單題:[APIO2010]特別行動隊
我們設\(s_i\)是字首和陣列,設\(f_i\)為前\(i\)個巨佬的戰鬥力和最大值,不難列出狀態轉移方程:
\[f_i=\max(f_j+a(s_i-s_j)^2+b(s_i-s_j)+c)\]
把式子拆開得到
\[f_i=f_j+as_i^2-2as_is_j+as_j^2+bs_i-bs_j+c\]
移項、合併項
\[as_j^2+f_j=(2as_i+b)s_j+(f_i-as_i^2-bs_i-c)\]
如果我們將\((s_j,as_j^2+f_j)\)當做笛卡爾平面上的點,那麼\(f_i\)的最大值就是以\(2as_i+b\)為斜率、經過這些點截距的最大值加上\(as_i^2+bs_i+c\)
根據題意,\(s_j\)是單調遞增的,\(2as_i+b\)是單調遞減的,所以我們可以維護上凸殼,根據轉移中斜率的單調性來在凸殼上確定轉移點。由於是上凸殼,並且斜率單調遞減,所以轉移點是單調遞增的,即我們可以單調地維護賺一點。轉移後再將新的點加入上凸殼即可。時間複雜度為\(O(n)\)。由於座標都是整點,需要通過叉積判凸殼,一般都要開long long,這裡偷懶直接define int long long下面是亂七八糟的程式碼/
#include <bits/stdc++.h> #define int long long using namespace std; int n, a, b, c, s[1000010]; int f[1000010]; int x[1000010], y[1000010], p, tot; signed main() { scanf("%lld", &n); scanf("%lld%lld%lld", &a, &b, &c); for (int i = 1; i <= n; i++) scanf("%lld", &s[i]), s[i] += s[i - 1]; tot = 1, p = 1; for (int i = 1; i <= n; i++) { int k = 2 * a * s[i] + b; while (p < tot && y[p + 1] - k * x[p + 1] >= y[p] - k * x[p]) p++; f[i] = y[p] - k * x[p] + a * s[i] * s[i] + b * s[i] + c; int newx = s[i], newy = a * s[i] * s[i] + f[i]; while (tot > 1 && (newx - x[tot]) * (y[tot] - y[tot - 1]) - (newy - y[tot]) * (x[tot] - x[tot - 1]) <= 0) tot--; if (p > tot) p = tot; x[++tot] = newx; y[tot] = newy; } printf("%lld\n", f[n]); return 0; }
練習:玩具裝箱toy
再來看一道題:土地徵用
假設土地A的面積可以被土地B的面積完全包含,由於需要購買所有土地,所以我們可以只購買B(A跟著一起購買了)。我們把每個土地面積當做二維平面上的整點(x,y),則對於所有存在偏序關係的點都可以被刪除。我們可以按x排序,第二維通過單調棧來清除所有存在偏序關係的點,這樣就剩下了滿足x單增、y單減的點。
FJ每次選土地一定是選一段連續的區間更優:假設某一段區間內有土地不選,則顯然該土地在這段區間內選擇不會比不在區間內選擇差,所以我們還是要選擇連續的土地一起買。
我們可以設狀態了,設\(f_i\)為購買前\(i\)塊土地最小值,則
\[f_i=\min(f_j+y[j+1]*x[i])\]
顯然我們可以通過斜率優化,稍微推一下式子即可,這裡就不囉嗦了,下面是亂七八糟的程式碼
#include <bits/stdc++.h>
#define int long long
using namespace std;
struct fuck
{
int x, y;
} a[50010];
int n, tot, f[50010];
int x[50010], y[50010];
signed main()
{
scanf("%lld", &tot);
for (int i = 1; i <= tot; i++)
scanf("%lld%lld", &a[i].x, &a[i].y);
sort(a + 1, a + 1 + tot, [](const fuck &a, const fuck &b) {return a.x < b.x;});
for (int i = 1; i <= tot; i++)
{
while (n > 0 && a[n].y <= a[i].y)
n--;
a[++n] = a[i];
}
tot = 1;
x[1] = a[1].y, y[1] = 0;
int p = 1;
for (int i = 1; i <= n; i++)
{
if (p > tot) p = tot;
while (p < tot && a[i].x * x[p + 1] + y[p + 1] < a[i].x * x[p] + y[p])
p++;
f[i] = a[i].x * x[p] + y[p];
while (tot > 1 && (x[tot] - x[tot - 1]) * (f[i] - y[tot]) - (y[tot] - y[tot - 1]) * (a[i + 1].y - x[tot]) > 0)
tot--;
y[++tot] = f[i];
x[tot] = a[i + 1].y;
}
printf("%lld\n", f[n]);
return 0;
}
時間複雜度\(O(n\log n)\)(因為有個排序),注意DP過程的時間複雜度是\(O(n)\)的。
斜率優化DP最主要就是先寫暴力,然後推式子,推出類似一次函式解析式形式,然後稍微分析下凸包情況即可
注意開long long,有必要可以開double。