1. 程式人生 > 其它 >淺談樹形 DP

淺談樹形 DP

樹形 DP 是 NOIP/CSP 常考型別,是最重要的 DP。

由於樹固有的遞迴性質,樹形 DP 一般都是遞迴進行的。

基礎

以下面 [【LG P1352】沒有上司的舞會] 為例,介紹一下樹形 DP 的一般過程。

某大學有 \(n\) 個職員,編號為 \(1\) ~ \(n\)。他們之間有從屬關係,父結點就是子結點的直接上司。現在有個週年慶宴會,宴會每邀請來一個職員都會增加一定的快樂指數 \(a_i\),但是,如果某個職員的上司來參加舞會了,那麼這個職員就不肯來參加舞會了。求最大的快樂指數。

我們可以定義 \(f_{i,0/1}\) 代表以 \(i\) 為根的子樹的最優解(第二維的值為 \(0\)

代表 \(i\) 不參加舞會的情況,\(1\) 代表 \(i\) 參加舞會的情況)。

顯然,我們可以推出下面兩個狀態轉移方程(其中下面的 \(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 二分做法。

挖坑,未完待續……

參考資料

本文作者:AFewMoon,文章地址:https://www.cnblogs.com/AFewMoon/p/15484288.html

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。

限於本人水平,如果文章有表述不當之處,還請不吝賜教。