淺談樹形 DP
樹形 DP 是 NOIP/CSP 常考型別,是最重要的 DP。
由於樹固有的遞迴性質,樹形 DP 一般都是遞迴進行的。
基礎
以下面 [【LG P1352】沒有上司的舞會] 為例,介紹一下樹形 DP 的一般過程。
某大學有 \(n\) 個職員,編號為 \(1\) ~ \(n\)。他們之間有從屬關係,父結點就是子結點的直接上司。現在有個週年慶宴會,宴會每邀請來一個職員都會增加一定的快樂指數 \(a_i\),但是,如果某個職員的上司來參加舞會了,那麼這個職員就不肯來參加舞會了。求最大的快樂指數。
我們可以定義 \(f_{i,0/1}\) 代表以 \(i\) 為根的子樹的最優解(第二維的值為 \(0\)
顯然,我們可以推出下面兩個狀態轉移方程(其中下面的 \(v\) 都是 \(u\) 的兒子,下同):
-
\(f_{u,0}=\sum_{\text{edge}(u,v)}\max\{f_{v,1},f_{u,0}\}\)(上司不參加舞會時,下屬可以參加,也可以不參加)
-
\(f_{u,1}=\sum_{\text{edge}(u,v)}f_{u,0}+a_i\)(上司不參加舞會時,下屬可以參加,也可以不參加)
我們可以通過 DFS,在返回上一層時更新當前結點的最優解。
程式碼:
#include<bits/stdc++.h> using namespace std; int a[6005],f[6005][2]; vector<int> v[6005]; bool staff[6005]; void dp(int x) { f[x][0]=0; f[x][1]=a[x]; for(int i=0; i<v[x].size(); i++) { int t=v[x][i]; dp(t); f[x][0]+=max(f[t][0],f[t][1]); f[x][1]+=f[t][0]; } } int main() { int n; scanf("%d",&n); for(int i=1; i<=n; i++) { scanf("%d",a+i); } for(int i=1; i<=n; i++) { int x,y; scanf("%d %d",&x,&y); v[y].push_back(x); staff[x]=1; } int root; for(int i=1; i<=n; i++) { if(!staff[i]) { root=i; } } dp(root); printf("%d",max(f[root][0],f[root][1])); return 0; }
相關練習:LG P2016戰略遊戲。
樹上揹包
樹形揹包解決的問題是給你幾個物品,但物品有依賴關係,\(a\) 依賴 \(b\),\(b\) 依賴 \(c\),選 \(a\) 就必須選 \(b\),選 \(b\) 就必須選 \(c\),一個物品只能依賴一個物品,但一個物品可以被多個物品依賴,這裡就能看出來這是一個樹。叫你選擇 \(n\) 物品可以使得價值最大,價值為多少?
一般我們的狀態就是 \(f_{i,j}\) 表示以 \(i\) 為根節點的子樹中選擇了 \(j\) 個點所得到的價值,轉移也大都是利用 dfs 回溯和揹包來進行。
以下面 【CTSC 1997】選課 為例,介紹一下樹形揹包。
有一堆樹構成的森林,共 \(n\) 個點。每個點有一個權值 \(s_i\)。一個點可以被選擇,當且僅當它到根節點的路徑上的所有點都被選擇。共選擇 \(m\) 個點,求被選擇的點的權值和的最大值。
一個小技巧:我們發現,如果 \(0\) 算一個節點的話,整張圖就是一棵樹了。
這樣的好處:
一棵樹就不用分別考慮各棵樹然後合併了。輸入方便很多,不用特別處理 \(0\) 的情況。但是 \(m\) 就會受影響。
因為根節點 \(0\) 是必選的,所以只要讓 \(m\) 增加 \(1\) 就好了。
首先,不難看出,父節點的資訊可以由子節點合併得到並且不會影響子節點。
所以使用 dp 或者記憶化搜尋就好了。
不難想到,用 \(dp_{u,i}\) 表示以節點 \(u\) 為根的子樹,選擇 \(i\) 個點可以獲得的最大權值和。然後想如何轉移。
好像遇到麻煩了!
顯然合併子節點的資訊一定能得到父節點的資訊,但使用簡單的演算法好像不行了。
沒事反正資料範圍小。
繼續觀察,發現每個子節點都會佔用父節點 \(i\) 的一部分,又有一個貢獻,可以選擇或不選擇。
重量……價值……總重……這不是 \(01\) 揹包嗎?
不同之處在於,每個子節點的重量都是變數。
重新設計狀態,用 \(dp_{u,i,j}\) 表示節點 \(u\) 的前 \(i\) 個子節點,限重為 \(j\) 能得到的最大權值和(價值和)
像 \(01\) 揹包一樣壓縮空間,得到:
\(dp_{i,j}\) 表示節點 \(u\),限重 \(j\) 的最大權值和(價值和)。
for(int i=head[u]; i; i=e[i].nxt) {
int v=e[i].to;
dp(v);
for(int j=m+1; j; j--) {
for(int k=0; k<j; k++) {
f[u][j]=max(f[u][j],f[v][k]+f[u][j-k]);
}
}
}
相關練習:
-
【LG P1273】有線電視網 樹上分組揹包經典題。
轉移方程
dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-這條邊的花費
。\(v\) 表示列舉到這一組(即i的兒子),\(k\) 表示列舉到這組中的元素:選\(k\) 個使用者。 -
【LG P1272】重建道路 類似樹上揹包的樹形 DP。
遞迴操作,\(f_{i,j}\) 表示保留 \(i\) 為根的子節點。\(c\) 陣列記錄點的度。因為這是一棵樹,所以每個點的度為 \(1\)。然後隨便設一個根,我設的 \(1\) 為根,\(1\) 的根就為 \(0\)。遞迴時傳入兩個引數,為當前節點和當前節點的根。
-
【IOI 2005】Riv 河流 樹上揹包,也有更快的 wqs 二分做法。
挖坑,未完待續……
參考資料
- 樹形 DP - OI Wiki
- 【動態規劃】樹形DP完全詳解! - RioTian
- 題解 P1273 【有線電視網】 - Fairycastle - 洛谷部落格
- 題解 P1272 【重建道路】 - beretty 的部落格 - 洛谷部落格
本文作者:AFewMoon,文章地址:https://www.cnblogs.com/AFewMoon/p/15484288.html
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。
限於本人水平,如果文章有表述不當之處,還請不吝賜教。