【前言】
在補Codeforce的DP時遇到一個比較新穎的題,然後在知乎上剛好 hycc 桑也寫了這道題的相關題解,這裡是作為學習並引用部落格的部分內容
這道題追根溯源發現2016年這個演算法已經在APIO2016煙花表演與Codeforces 713C引入,自那之後似乎便銷聲匿跡了。相關題型數量也較少,因而在這裡結合前輩們的工作做一些總結。---by hycc
問題引入:Codeforces 713C
題目連結:Here
題意:
給定 \(n\) 個正整數 \(a_i\) ,每次操作可以選擇任意一個數將其 \(+1\) 或 \(-1\) ,問至少需要多少次操作可以使得 \(n\) 個數保持嚴格單增。
資料範圍:\(1\le n\le 3000,1\le a_i\le 10^9\)
對我來說這道題其實和曾經寫過的 POJ-3666:求不升的DP是一樣的
這個題是求升序的DP,那麼有什麼變化呢
不升的條件是:\(a_i -a_j \ge 0\)
升序的條件是:\(a_i -a_j \ge i - j\) 對任意 \(i,j\) 均滿足
有沒有理解到什麼?移項有:\(a_i - i \ge a_j - j\)
所以將 \(a\) 數字變形一下就和POJ3666就是一個題!
【AC Code】
const int N = 3100;
int n, m;
ll f[N][N], a[N], b[N];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
a[i] = a[i] - i;
b[i] = a[i];
}
sort(b + 1, b + 1 + n);
m = 1;
for (int i = 2; i <= n; ++i) if (b[i] != b[i - 1]) b[++m] = b[i];
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; ++i) {
ll Min = LLONG_MAX;
for (int j = 1; j <= m; j++) {
Min = min(Min, f[i - 1][j]);
f[i][j] = abs(b[j] - a[i]) + Min;
}
}
ll ans = LLONG_MAX;
for (int i = 1; i <= m; ++i) ans = min(ans, f[n][i]);
cout << ans << "\n";
}
當然上面說的思路並不是本篇部落格實際想表達,以下才是正文
對於樸素的 \(\mathcal{O}(n^2)\ DP\) :
一個顯然的性質:如果不是“嚴格單增”而是“嚴格非降”,那麼最終形成的嚴格非降序列,其中每個元素一定屬於 \(\{a_i\}\)
將元素離散化後可以設計 \(f_{i,j}\) 表示到第 \(i\) 個數取 \(j\) 的最少運算元
那麼有轉移 \(f_{i,j} = \min\limits_{k\le j}f_{i-1,k} + | a_i - j|\) ,記錄 \(f_{i-1,*}\) 的字首 \(\min\) 即可做到 \(\mathcal{O}(n^2)\)
至於如何做到“嚴格非降”,\(a_{i-1} < a_i,a_{i -1} \le a_i - i,a_{i-1}-(i-1)\le a_i - i\)
於是令 \(a_i = a_i - i\) 即可。
賽後的評論區中出現了一種 \(\mathcal{O}(Nlog\ N)\)的做法,也就是 Slope Trick演算法的第一次現身(?)
Slope Trick:解決一類凸代價函式的DP優化問題
當序列DP的轉移代價函式為
連續
分段線性函式
凸函式
時,可以通過記錄分段函式的最右一段 \(f_r(x)\) 以及其分段點 \(L\) 實現快速維護代價的效果。
如:\(f(x)=\left\{\begin{array}{rr}
-x-3 & (x \leq-1) \\
x & (-1<x \leq 1) \\
2 x-1 & (x>1)
\end{array}\right.\)
可以僅記錄 \(f_r(x) = 2x - 3\) 與分段點 \(L_f = \{-1,-1,1\}\) 來實現對該分段函式的儲存。
注意:要求相鄰分段點之間函式的斜率差為 \(1\) ,也就是說相鄰兩段之間斜率差 \(\ge 1\) 的話,這個分段點要在序列裡出現多次。
優秀的性質:
\(F(x),G(x)\) 均為滿足上述條件的分段線性函式,那麼 \(H(x) =F(x)+G(x)\) 同樣為滿足條件的分段線性函式,且 \(H_r(x) = F_r(x) + G_r(x),L_H = L_F \bigcup L_G\) 。
該性質使得我們可以很方便得運用資料結構維護 \(L\) 序列。
回顧:Codeforces 713C
轉移方程為 \(f_{i,j} = \min\limits_{k\le j}f_{i-1,k} + |a_i - j|\)
令 \(F_{i}(x)=f_{i, x}, G_{i}(x)=\min\limits _{k \leq x} f_{i-1, k}=\min \limits_{k \leq x} F_{i-1}(k)\)
那麼有 \(F_i(x) = G_i(x) + |x -a_i|\) ,其中 \(F_i,G_i\) 均為分段線性函式。
\(G_i\) 求的是 \(F_{i-1}\) 的關於函式值的字首最小值,由於 \(F_{i-1}\) 是一個凸函式,因而其最小值應該在斜率 \(=0\) 處取得,其後部分可以捨去。
而每次由 \(G_i(x)\) 加上 \(|x-a_i|\) ,等價於在 \(L\) 中新增兩個分段點 \(\{a_i,a_j\}\)
因而 \(G_i\) 各段的函式斜率形如 \(\{...,-3,-2,-1,0\}\) ,加上 $|x-a_i| $後斜率變為 \(\{...,-3,-2,-1,0,1\}\) ,因而需要刪除末尾的分段點。
具體實現中:使用大根堆維護分段點單調有序,每次加入兩個 \(a_i\) ,再彈出堆頂元素。
總複雜度 :\(\mathcal{O}(n\ log\ n)\)
\]
Codeforces 1534G
題意:
一個無限大的二維平面上存在 \(n\) 個點 \((x_i,y_i)\) 均需要被訪問一次,從 \((0,0)\) 出發,每次可以向右或向上移動一個單位。
可以在任意位置 \((X,Y)\) 訪問 \((x_i,y_i)\) 並付出 \(\max\{|X-x_i|,|Y-y_i|\}\) 的代價(訪問後依然留在 \((X,Y)\) )。同一位置可以訪問多個點。
問:至少需要花費多少代價才能使得所有點均被訪問?
資料範圍: \(1\le n\le 800000,0\le x_i,y_i\le 10^9\)
結合上圖可以看出,對於點 \((X,Y)\) ,一定會選擇路徑與直線 \(x+y=X+Y\)(紅線)的交點 \((x,y)\) 處作為訪問的發起點(在這條線上 \(|X-x| = |Y-y|\) )。
考慮到這條紅線是傾斜的,因而將座標系順時針翻轉 \(45^°\),即 \((x+y,x-y)\) 代替 \((x,y)\)
此時,每次移動變為 \((x+1,y-1)\) 或 \((x+1,y+1)\)
把所有點按新的 \(x\) 座標排序,即可轉為序列上的問題。
設值域為 \(M\) ,則很容易寫出 \(\mathcal{O}(nM)\) 的轉移方程:
\(f_{i,Y}\) 表示從左到右考慮到橫座標為 \(x_i\) 的所有點,當前路徑到了 \((x_i,Y)\) 的最小代價,
那麼有
$f_{i,Y}=\min\limits_{Y-\left|x_{i}-x_{i-1}\right|\leq k\leq Y+\left|x_{i}-x_{i-1}\right|}f_{i-1, k}+\sum\limits_{(x, y), x=x_{i}}|Y-y| $
同樣,設 \(F_{i}(x)=f_{i, x}, G_{i}(x)=\sum\limits_{x-\left|x_{i}-x_{i-1}\right| \leq k \leq x+\left|x_{i}-x_{i-1}\right|} f_{i-1, k}\)
那麼 \(F_{i}(x)=G_{i}(x)+\sum_{\left(x^{\prime}, y^{\prime}\right), x^{\prime}=x_{i}}\left|x-y^{\prime}\right|\)
主要問題在於 \(G_i(x)\) 的維護,是取一個區間範圍 \([L,R]\) 內的最小值。
若斜率為 \(0\) 的兩端點在 \([L,R]\) 內,那麼直接取最小值即可。
若斜率為 \(0\) 的兩端點在 \(L\) 左側,需要取 \(L\) 處的值作為最小值。
若斜率為 \(0\) 的兩端點在 \(R\) 右側,需要取 \(R\) 處的值作為最小值。
因而,需要維護斜率為 \(0\) 的折線的兩側分割點 \((a,b)\) ,同時還需要支援從斜率為 \(0\) 處向兩側訪問,因而使用小根堆與大根堆分別維護 \(b\) 右側以及 \(a\) 左側的點。
每次新增新的分割點時,根據新分割點與 \(a,b\) 的大小關係決定插入小根堆or大根堆,同時調整 \(a,b\) ,每次調整複雜度是 \(\mathcal{O}(1)\) 的(從小根堆中取出塞入大根堆或反之)
【AC Code】借用 jiangly的程式碼
#include <bits/stdc++.h>
using i64 = long long;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::pair<int, int>> a;
for (int i = 0; i < n; i++) {
int x, y;
std::cin >> x >> y;
a.emplace_back(x + y, x);
}
std::priority_queue<i64> hl;
std::priority_queue<i64, std::vector<i64>, std::greater<>> hr;
for (int i = 0; i < n + 5; i++) {
hl.push(0);
hr.push(0);
}
i64 tag = 0, mn = 0;
int last = 0;
std::sort(a.begin(), a.end());
for (auto [s, x] : a) {
int d = s - last;
last = s;
tag += d;
if (x <= hl.top()) {
mn += hl.top() - x;
hl.push(x);
hl.push(x);
hr.push(hl.top() - tag);
hl.pop();
} else if (x >= hr.top() + tag) {
mn += x - (hr.top() + tag);
hr.push(x - tag);
hr.push(x - tag);
hl.push(hr.top() + tag);
hr.pop();
} else {
hl.push(x);
hr.push(x - tag);
}
}
std::cout << mn << "\n";
return 0;
}