【前言】

在補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)\)


\[QAQ
\]

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;
}