1. 程式人生 > 實用技巧 >樹形dp

樹形dp

前言

樹形dp是一種在樹上做的dp,通常的結構為:

void dp(int u) {
    v[u]=true;
    for(int i=head[u];i;i=nxt[i]) {
        int v=to[i];
        dfs(v);
        update(f[u],f[v]);  //用f[v]來更新f[u]
    }
}

先遞迴子節點,再從子節點向上轉移,這樣就是一般的樹形dp。

樹直徑

考慮這樣一道題:
給定一棵無根樹,求兩個距離最遠的節點間的距離,兩個相鄰結點距離為1。
首先,兩個距離最遠的結點必定是“邊界點”,即度為1的點。
我們不妨對於每個點,求出這個點到這個點的子樹的所有邊界點的最長距離與次長距離,設為f[x][1],f[x][2]

,那麼答案也就自然是\(\max_{1\leq i\leq n}(f[i][1]+f[i][2])\)
主要的瓶頸就在於:如何求出每個點的f呢?
我們看下dp程式碼

void dfs(int u) {
	v[u]=true;
	for(int i=head[u];i;i=nxt[i]) {
		int y=to[i];
		if(v[y])    //已經訪問過了(即父節點),跳過
		  continue;
		dfs(y); //遞迴子節點
        //以下程式碼請好好斟酌
		if(h[u][1]<=h[y][1]+1) {    //如果最長路可以更新
			h[u][2]=h[u][1];    //將次長路更新為最長路
			h[u][1]=h[y][1]+1;  //將最長路更新為新值
		}
		else if(h[u][2]<=h[y][1]+1) //如果次長路可以更新
		  h[u][2]=h[y][1]+1;    //更新次長路
	}
}

因為這個樹是一個無根樹,所以我們從任意一個點開始執行dfs都可以。

樹上揹包問題

CTSC1997 選課

題目連結:選課acwing286
這n門課構成了一個森林的結構,為了簡便,可以新增零號節點為各個樹根的父親,方便dp。
狀態定義:f[u][t]表示在以u為根的子樹中選t門的最高得分。
狀態的轉移實際上是一個分組揹包模型,一個點的所有子節點看做是一組物品v,每組物品有f[v][i]的價值與i的消費。每組物品中只能取一個。

#include <iostream>
#include <cstring>

using namespace std;
const int N=3e2+5;
int n,m;
int head[N],to[N],nxt[N],cnt;
int score[N];
int f[N][N];

void add(int u,int v) {
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
}


void dp(int u) {
    f[u][0]=0;
	for(int i=head[u];i;i=nxt[i]) {
		int v=to[i];
		dp(v);
		for(int t=m;t>=0;t--)   //揹包狀態轉移
		  for(int j=t;j>=0;j--)
		    f[u][t]=max(f[u][t],f[u][t-j]+f[v][j]);
	}
	if(u!=0)
	  for(int i=m;i>0;i--)
	    f[u][i]=f[u][i-1]+score[u];
}

int main() {
    memset(f,0xc0,sizeof(f));
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		int u;
		cin>>u>>score[i];
		add(u,i);
	}
	dp(0);
	cout<<f[0][m];
	return 0;
}

二叉蘋果樹

題目連結:二叉蘋果樹
有了上面一道題,這道題就更加簡單了,直接看程式碼。
注意不同的是,這道題是邊權,上一題是點權。

#include <iostream>
#include <cstring>

using namespace std;
const int N=3e2+5;
int n,m;
int head[N],to[N],val[N],nxt[N],cnt;
bool vis[N];
int f[N][N];

void add(int u,int v,int w) {
	to[++cnt]=v;
	val[cnt]=w;
	nxt[cnt]=head[u];
	head[u]=cnt;
}

void dp(int u) {
	vis[u]=true;
    f[u][0]=0;
	for(int i=head[u];i;i=nxt[i]) {
		int v=to[i],w=val[i];
		if(vis[v])
		  continue;
		dp(v);
		for(int t=m;t>=0;t--)
		  for(int j=t;j>=0;j--)
		    f[u][t]=max(f[u][t],f[u][t-j-1]+f[v][j]+w); //此處減一是要把自己算上去
	}
}

int main() {
    memset(f,0xc0,sizeof(f));
	cin>>n>>m;
	for(int i=1;i<n;i++) {
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	dp(1);
	cout<<f[1][m];
	return 0;
}

典型樹形dp

沒有上司的舞會

題目連結:沒有上司的舞會acwing285
狀態定義:f[u][0]表示當u不選時,u的子樹的最大值,f[u][1]表示選u時,u的最大值。
那麼很容易寫出狀態轉移方程了。

#include <iostream>
#include <cstdio>

using namespace std;
const int N=2e4+5;
int head[N],nxt[N],to[N],cnt;
int f[N][2],h[N],n,root,in[N];

void add(int u,int v) {
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
}

void dp(int p) {
	f[p][0]=0;
	f[p][1]=h[p];
	for(int i=head[p];i;i=nxt[i]) {
		int y=to[i];
		dp(y);  //這邊要注意:如果有明確的依賴關係,就不需要判斷是否為父親,建邊時建單向邊即可。但是如果只是說有一條邊,那麼就要雙向建邊,並判斷到達點是否為當前點的父親
		f[p][0]+=max(f[y][1],f[y][0]);  //如果當前點不選,它的下級既可以選也可以不選
		f[p][1]+=f[y][0];   //如果當前點選,那麼只有不選它的下級
	}
}

int main() {
	cin>>n;
	for(int i=1;i<=n;i++)
	  cin>>h[i];
	for(int i=1;i<n;i++) {
		int u,v;
		cin>>u>>v;
		add(v,u);
		in[u]++;    //記錄入度,根的入度必定為0
	}
	for(int i=1;i<=n;i++)
	  if(in[i]==0) {
	    root=i;
	  	break;
	  }
	dp(root);   //從根開始dp
	cout<<max(f[root][0],f[root][1]);   //選根與不選根取最大值
	return 0;
}

數字轉換

題目連結:數字轉換
看起來不像樹形dp,但我們可以把它轉化成數形dp
設一個數x的約數和叫d[x],那麼對於每個x我們不妨看做是x到d[x]連一條邊。最後求這棵樹的直徑就行了。

#include <iostream>
#include <cstdio>

using namespace std;
const int N=4e5+5;
int n,m;
int head[N],to[N],nxt[N],cnt;
bool v[N];
int h[N][3],ans;
int sum[N];

void add(int u,int v) {
	to[++cnt]=v;
	nxt[cnt]=head[u];
	head[u]=cnt;
}

//求直徑傳統藝能
void dfs(int u) {
	v[u]=true;
	for(int i=head[u];i;i=nxt[i]) {
		int y=to[i];
		if(v[y]) 
		  continue;
		dfs(y);
		if(h[u][1]<=h[y][1]+1) {
			h[u][2]=h[u][1];
			h[u][1]=h[y][1]+1;
		}
		else if(h[u][2]<=h[y][1]+1)
		  h[u][2]=h[y][1]+1; 
	}
}

int main() {
	cin>>n;
    //求每個數的約數和並加邊
	for(int i=1;i<=n;i++) { //列舉因子來求約數和
		if(sum[i]<i) {  //約數和小於原數
			add(i,sum[i]);  //加兩條邊
			add(sum[i],i);
		}
		for(int j=i*2;j<=n;j+=i)    //列舉i的倍數
		  sum[j]+=i;
	}
	dfs(1);
	for(int i=1;i<=n;i++)
	  ans=max(ans,h[i][1]+h[i][2]); //直徑
	cout<<ans;
	return 0;
}

二次掃描與換根法

如果題目中要對每個點都進行統計,並且這棵樹是無根樹,那麼就可以使用這個方法。
1、第一次掃描,任選一個點為根,執行從下到上的dp
2、第二次掃描,以剛才那個點為根,進行從上到下的推導
我們可以解決比如:求每個點到任意點的最長路,或者次長路等的問題。

Accumulation Degree

這道題目藍書上面有講解,就不贅述了。