動態 DP 學習筆記
動態 DP 學習筆記
\(\texttt{by Tweetuzki}\)
不得不承認,去年提高組 D2T3 對動態 DP 起到了良好的普及效果。
動態 DP 主要用於解決一類問題。這類問題一般原本都是較為簡單的樹上 DP 問題,但是被套上了喪心病狂的修改點權的操作。舉個例子,我們來看一道例題。
【模板】動態 DP
給定一棵 \(n\) 個點的樹。\(i\) 號點的點權為 \(a_i\)。有 \(m\) 次操作,每次操作給定 \(u, w\),表示修改點 \(u\) 的權值為 \(w\)。你需要在每次操作之後求出這棵樹的最大權獨立集的權值大小。
我們首先考慮沒有修改的情況下怎麽做。首先先選取 \(1\)
\[ f_{i, 0} = \sum_{j} \max(f_{j, 0}, f_{j, 1}) \f_{i, 1} = \sum_{j} f_{j, 0} + a_i \]
這裏 \(j\) 表示 \(i\) 號點的所有兒子。特殊地,若點 \(i\) 為葉子節點,\(f_{i, 0} = 0, f_{i, 1} = a_i\)。
最後的答案就是 \(\max(f_{1, 0}, f_{1, 1})\)
接下來帶上修改。
首先根據動態規劃的轉移方程可以發現,我們修改了一個點的點權,只會更改從這個點到根這條路徑上節點的 DP 值,其他值是不會發生更改的。這時候如果我們要對整棵樹重新求一遍最大權獨立集,未免太過浪費。所以我們希望能夠更改這條鏈上的 DP 值。
由於樹可能會退化成一條鏈,這樣每次更新就是 \(\mathcal{O(n)}\) 的,顯然不可接受。我們希望這條鏈只更新 \(\log n\) 次……
點分治!抱歉博主太弱了,不會那個被稱作“全局平衡二叉樹”的厲害做法。
這時候我們請出解決樹上問題的神器——重鏈剖分。
重鏈剖分有一些性質,這些性質正是它在動態 DP 中能夠發揮作用的重要保障。
- 每個點到根的路徑上,最多經過 \(\log n\) 條輕邊。也就是說,重鏈的條數最多也只有 \(\log n\) 條。這為動態 DP 的時間復雜度做了保障。
- 每條重鏈的鏈尾都是葉子節點,且只有葉子節點沒有重兒子。這為動態規劃的初始狀態和轉移方式做了保障。
- 重鏈剖分中,一條重鏈所在的區間在剖出的 DFS 序上,是連續的一段區間。這為可以使用數據結構維護區間信息,達到快速轉移做了保障。
那麽在宏觀上,我們相當於在更新時,對於這些重鏈暴力地互相轉移更新。接下來我們考慮一些微觀問題:在一條鏈裏,怎麽支持快速修改和查詢這條鏈的 DP 值。
我們保持 \(f\) 數組的定義不變。為了迎合重鏈剖分劃分出了輕重兒子,我們形式化地定義 \(g\) 數組:\(g_{i, 1}\) 表示 \(i\) 號點的所有輕兒子,都不取的最大權獨立集;\(g_{i, 0}\) 表示 \(i\) 號點的所有輕兒子,可取可不取形成的最大權獨立集。這樣就可以把上述的 DP 式子大大簡化了(至少沒有了那個 \(\Sigma\))。
\[ f_{i, 0} = g_{i, 0} + \max(f_{j, 0}, f_{j, 1}) \f_{i, 1} = g_{i, 1} + a_i + f_{j, 0} \]
這裏的 \(j\) 表示 \(i\) 號點的重兒子。特殊地,對於葉子節點,\(g_{i, 0} = g_{i, 1} = 0\)。
但是感覺這玩意兒好像不大優美?第二個轉移式子中,\(g_{i, 1}\) 和 \(a_i\) 都只和 \(i\) 有關,那麽我們不妨把它們合並起來。我們重新定義 \(g_{i, 1}\):表示 \(i\) 號點只考慮輕兒子的取自己的最大權獨立集。那麽這時候,第二個方程就可以變為 \(f_{i, 1} = g_{i, 1} + f_{j, 0}\)。
但是這玩意兒咋區間維護嘞?回想一下當初學習斐波那契的時候,我們碰到過這樣的 DP 方程:
\[f_i = f_{i - 1} + f_{i - 2}\]
這個方程涉及上一步的貢獻,沒法滿足結合率,不太舒服。於是我們定義了一個矩陣,化加為乘,於是我們愉快地用快速冪 AC 了。
這道題我們也給它套個矩陣。對於每個點,都表示一個狀態,這個狀態共有兩個值,於是我們考慮維護一個 \(1 \times 2\) 的矩陣。
\[ \begin{vmatrix} f_{i, 0} & f_{i, 1} \end{vmatrix} \]
現在我們要從一個點的重兒子 \(j\) 轉移到 \(i\) 上,也就是說我們需要構造出一個轉移矩陣使得 \(\begin{vmatrix} f_{j, 0} & f_{j, 1} \end{vmatrix}\) 能夠轉移到 \(\begin{vmatrix} f_{i, 0} & f_{i, 1} \end{vmatrix}\)。但是我們回顧一下這個轉移方程(已更改 \(g_{i, 1}\) 的定義):
\[ f_{i, 0} = g_{i, 0} + \max(f_{j, 0}, f_{j, 1}) \f_{i, 1} = g_{i, 1} + f_{j, 0} \]
它一點也不滿足矩陣乘法的形式啊!
別慌……我們大膽地重定義矩陣乘法!
我們定義一個新的運算符 \(*\),對於矩陣 \(\mathrm{A}, \mathrm{B}\),定義 \(\mathrm{A} * \mathrm{B}\) 的結果 \(\mathrm{C}\),滿足:
\[ \mathrm{C}_{i, j} = \max_{k}(\mathrm{A}_{i, k} + \mathrm{B}_{k, j}) \]
實現到代碼上,就是
struct Matrix {
int mat[MaxN][MaxN];
}
inline Matrix operator * (Matrix a, Matrix b) {
Matrix c;
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
for (int k = 0; k < n; ++k)
c.mat[i][j] = max(c.mat[i][j], a.mat[i][k] + b.mat[k][j]);
return c;
}
但是這個東西為什麽具有結合率呢?
- 一種感性的理解:由於 \(\max\) 操作和加法操作都是滿足結合率的,所以這個運算滿足結合率。
- 一種理性但不太嚴謹的證明:讀者不妨拿出之筆,計算幾組 \((\mathrm{A} * \mathrm{B}) * \mathrm{C}\) 和 \(\mathrm{A} * (\mathrm{B} * \mathrm{C})\) 的值(如果您計算比較厲害,帶上參數算當然更好)。一般情況下,證明了三個滿足條件,對於所有情況都是能滿足條件的。
於是我們口胡完了結合率的證明。那麽我們就可以用了。接下來我們要構造一個轉移矩陣,這個是相對難的一個內容。我就介紹一下我個人構造轉移矩陣的拙劣方法吧。
在構造一個轉移矩陣之前,我們先想辦法把這玩意兒變形,變得和運算 \(*\) 差不多。
\[ f_{i, 0} = \max(f_{j, 0} + g_{i, 0}, f_{j, 1} + g_{i, 0}) \f_{i, 1} = \max(g_{i, 1} + f_{j, 0}, -\infty) \]
接著我們把已知的狀態和要轉移到的狀態寫在一起,把未知的轉移矩陣用 \(\mathrm{U}\) 表示。
\[ \begin{vmatrix} f_{j, 0} & f_{j, 1} \end{vmatrix} * \mathrm{U} = \begin{vmatrix} f_{i, 0} & f_{i, 1} \end{vmatrix} \]
我們原來是一個 \(1 \times 2\) 的矩陣,要形成一個 \(1 \times 2\) 的矩陣,那麽 \(\mathrm{U}\) 應當是一個 \(2 \times 2\) 的矩陣。那麽我們設矩陣左上、右上、左下、右下四個位置分別為 \(u_1, u_2, u_3, u_4\)。接下來把每個位置對應上去。
\(f_{i, 0}\) 的值應該為 \(\max(f_{j, 0} + u_1, f_{j, 1} + u_3)\)。對應轉移方程,我們發現 \(u_1\) 應該就是 \(g_{i, 0}\),\(u_3\) 也是 \(g_{i, 0}\)。同樣的,\(f_{i, 1}\) 的值應該為 \(\max(f_{j, 0} + u_2, f_{j, 1} + u_4)\)。對應轉移方程,我們發現 \(u_2\) 應該是 \(g_{i, 1}\),而不存在 \(f_{j, 1}\) 項,就將 \(u_4\) 賦為 \(-\infty\)。最後寫出來,檢查一遍:
\[ \begin{vmatrix} f_{j, 0} & f_{j, 1} \end{vmatrix} * \begin{vmatrix} g_{i, 0} & g_{i, 1} \\ g_{i, 0} & -\infty \end{vmatrix} = \begin{vmatrix} f_{i, 0} & f_{i, 1} \end{vmatrix} \]
嗯……好像沒問題?
這樣子,我們對於一條重鏈,我們的葉子節點就存儲了最初始的值,鏈上每個節點都對應著一個轉移矩陣。我們發現這個轉移矩陣和重鏈信息是沒有任何關系的,且因為這個矩陣滿足結合率,對於一條重鏈,我們可以之間線段樹維護區間乘積(或者叫……“\(*\) 積”?)。然後到了一條重鏈鏈頭,因為這個點是它父親的輕兒子,我們需要更新它父親節點所在的點的轉移矩陣。這樣子一直跳到根節點就可以了。貌似……大功告成?
重鏈剖分剖出的 DFS 序,由於先訪問了鏈頭,所以這個區間中,鏈頭在區間左端,鏈尾在區間右端。我們存儲的初始信息在葉子節點(也就是鏈尾)上,因此我們的矩陣 \(*\) 法應當是轉移矩陣在前,要維護的值矩陣在後。我們要把這個矩陣前後換個順序,再轉個個兒,加上一些推算,可以變形成:
\[ \begin{vmatrix} g_{i, 0} & g_{i, 0} \\ g_{i, 1} & -\infty \end{vmatrix} * \begin{vmatrix} f_{j, 0} \\ f_{j, 1} \end{vmatrix} = \begin{vmatrix} f_{i, 0} \\ f_{i, 1} \end{vmatrix} \]
這樣就真的做完了。最後我寫一些關於代碼實現的小細節:
- 對於一個點查其 dp 值,需要從這個點一直查到區間鏈尾。因此,樹剖時我們需要多維護一個 \(\texttt{End[i]}\)(這裏的 \(i\) 是一條重鏈的鏈頭),表示以 \(i\) 為鏈頭的這條鏈,鏈尾(葉子)節點在 DFS 序上的位置。
- 更新線段樹上某個點的轉移矩陣時,傳入的如果是矩陣,遞歸下去常數太大。一個解決方法是,在線段樹外,維護一個矩陣組 \(\texttt{Val[i]}\),表示每個節點對應的轉移矩陣。這樣在線段樹更新找到對應位置時,直接賦值進來即可。
最後貼上代碼。
解釋一下變量名:
\(\texttt{Id[i]}\) 表示 \(i\) 號點在 DFS 序中的位置,\(\texttt{Dfn[i]}\) 表示在 DFS 序中下標 \(i\) 的位置對應的是什麽點(與 \(\texttt{Id[i]}\) 相反),\(\texttt{Fa[i]}\) 是父親節點,\(\texttt{Siz[i]}\) 是子樹大小,\(\texttt{Dep[i]}\) 是該節點深度(好像沒什麽用),\(\texttt{Wson[i]}\) 是 \(i\) 號節點的重兒子,\(\texttt{Top[i]}\) 表示 \(i\) 號點所在重鏈鏈頂編號。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int MaxN = 100000 + 5, MaxM = 200000 + 5;
const int MaxV = 400000 + 5;
const int INF = 0x7F7F7F7F;
struct Matrix {
int mat[2][2];
Matrix() {
memset(mat, -0x3F, sizeof mat);
}
inline Matrix operator * (Matrix b) {
Matrix c;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j)
for (int k = 0; k < 2; ++k)
c.mat[i][j] = max(c.mat[i][j], mat[i][k] + b.mat[k][j]);
return c;
}
};
int N, M; int cntv, cnte;
int A[MaxN];
int Fa[MaxN], Siz[MaxN], Dep[MaxN], Wson[MaxN];
int Top[MaxN], Id[MaxN], Dfn[MaxN], End[MaxN];
int F[MaxN][2];
int Head[MaxN], To[MaxM], Next[MaxM];
Matrix Val[MaxN];
struct SegTree {
int L[MaxV], R[MaxV];
Matrix M[MaxV];
inline void Push_up(int i) {
M[i] = M[i << 1] * M[i << 1 | 1];
}
void Build_Tree(int left, int right, int i) {
L[i] = left, R[i] = right;
if (L[i] == R[i]) {
M[i] = Val[Dfn[L[i]]];
return;
}
int mid = (L[i] + R[i]) >> 1;
Build_Tree(L[i], mid, i << 1);
Build_Tree(mid + 1, R[i], i << 1 | 1);
Push_up(i);
}
void Update_Tree(int x, int i) {
if (L[i] == R[i]) {
// 直接賦值,減小常數
M[i] = Val[Dfn[x]];
return;
}
int mid = (L[i] + R[i]) >> 1;
if (x <= mid) Update_Tree(x, i << 1);
else Update_Tree(x, i << 1 | 1);
Push_up(i);
}
// 查詢一個點的 DP 值,相當於查詢這條重鏈上鏈尾矩陣和鏈中轉移矩陣的 ‘*‘ 積
Matrix Query_Tree(int left, int right, int i) {
if (L[i] == left && R[i] == right) return M[i];
int mid = (L[i] + R[i]) >> 1;
if (right <= mid)
return Query_Tree(left, right, i << 1);
else if (left > mid)
return Query_Tree(left, right, i << 1 | 1);
else
return Query_Tree(left, mid, i << 1) * Query_Tree(mid + 1, right, i << 1 | 1);
}
} T;
inline void add_edge(int from, int to) {
cnte++; To[cnte] = to;
Next[cnte] = Head[from]; Head[from] = cnte;
}
void readin() {
scanf("%d %d", &N, &M);
for (int i = 1; i <= N; ++i)
scanf("%d", &A[i]);
for (int i = 1; i < N; ++i) {
int u, v;
scanf("%d %d", &u, &v);
add_edge(u, v); add_edge(v, u);
}
}
void dfs1(int u) {
Siz[u] = 1;
for (int i = Head[u]; i; i = Next[i]) {
int v = To[i];
if (v == Fa[u]) continue;
Fa[v] = u; Dep[v] = Dep[u] + 1;
dfs1(v);
Siz[u] += Siz[v];
if (Siz[v] > Siz[Wson[u]]) Wson[u] = v;
}
}
void dfs2(int u, int chain) {
cntv++;
Id[u] = cntv; Dfn[cntv] = u;
Top[u] = chain;
End[chain] = max(End[chain], cntv);
// 第二次樹剖時直接更新 F, G 數組(這裏直接將 G 放入矩陣更新)
F[u][0] = 0, F[u][1] = A[u];
Val[u].mat[0][0] = Val[u].mat[0][1] = 0;
Val[u].mat[1][0] = A[u];
if (Wson[u] != 0) {
dfs2(Wson[u], chain);
// 依照定義,重兒子不應計入 G 數組
F[u][0] += max(F[Wson[u]][0], F[Wson[u]][1]);
F[u][1] += F[Wson[u]][0];
}
for (int i = Head[u]; i; i = Next[i]) {
int v = To[i];
if (v == Fa[u] || v == Wson[u]) continue;
dfs2(v, v);
F[u][0] += max(F[v][0], F[v][1]);
F[u][1] += F[v][0];
Val[u].mat[0][0] += max(F[v][0], F[v][1]);
Val[u].mat[0][1] = Val[u].mat[0][0];
Val[u].mat[1][0] += F[v][0];
}
}
void init() {
readin();
dfs1(1); dfs2(1, 1);
}
void update_path(int u, int w) {
Val[u].mat[1][0] += w - A[u];
A[u] = w;
Matrix bef, aft;
while (u != 0) {
// 計算貢獻時,應當用一個 bef 矩陣還原出少掉這個輕兒子的情況,再將 aft 加入更新
bef = T.Query_Tree(Id[Top[u]], End[Top[u]], 1);
T.Update_Tree(Id[u], 1);
aft = T.Query_Tree(Id[Top[u]], End[Top[u]], 1);
u = Fa[Top[u]];
Val[u].mat[0][0] += max(aft.mat[0][0], aft.mat[1][0]) - max(bef.mat[0][0], bef.mat[1][0]);
Val[u].mat[0][1] = Val[u].mat[0][0];
Val[u].mat[1][0] += aft.mat[0][0] - bef.mat[0][0];
}
}
void solve() {
T.Build_Tree(1, N, 1);
for (int i = 1; i <= M; ++i) {
int u, w;
scanf("%d %d", &u, &w);
update_path(u, w);
Matrix Ans = T.Query_Tree(Id[1], End[1], 1);
printf("%d\n", max(Ans.mat[0][0], Ans.mat[1][0]));
}
}
int main() {
init();
solve();
return 0;
}
動態 DP 學習筆記