1. 程式人生 > 實用技巧 >基礎樹形DP

基礎樹形DP

模板

P1352 沒有上司的舞會

樹狀\(dp\)模板題。

\(f[i][0]\)為第\(i\)個人來了的方案數

\(f[i][1]\)為第\(i\)和人沒來的方案數

若第\(i\)個人來了,那麼其下屬均不回來

若不來,其下屬則有來和不來兩種選擇

因此狀態轉移方程為:

  • \(f[i][0]+=f[son][1]\)

  • \(f[i][1]+=max(f[son][0],f[son][1])\)

P2015 二叉蘋果樹

樹上揹包模板題

每一個枝條都有"剪"和"不剪"兩種可能

把每一個兒子都看成一個"分組揹包"

\(dp[i][j]\)

表示第\(i\)個子樹保留\(j\)條邊

每加入一個"兒子"後,列舉該"兒子"保留的邊數,如圖

(ps:這裡i-k後面還要減1是因為還要多保留從u->v這條邊)

故狀態轉移方程為:

  • \(f[u][i]=max(f[v][i-k-1]+f[u][k]+w[u][v])(i\in[1,m+1)]\)

樹上揹包

P2014 [CTSC1997]選課

P1273 有線電視網

P1270 “訪問”美術館

P1272 重建道路


P2014 [CTSC1997]選課

和二叉蘋果樹一樣的套路。

把每一個子課程都看作是一個"分組揹包",倒序列舉即可

由於題目中可能有多棵樹

因此多開一個節點把所有"樹根"連在一起

同時,在倒序列舉時也要把這個新節點算進去

轉移方程:

  • \(f[u][i]=max(f[v][i-k-1]+f[u][k]+w[v])(i\in[1,m+1])\)

核心程式碼:

void dp(int k){
	vis[k]=1;
	for(int i=0;i<son[k].size();i++){
		if(vis[son[k][i]]!=1){
			dp(son[k][i]);
			for(int v=m+1;v>=1;v--){
				for(int K=0;K<v;K++){
					 f[k][v]=max(f[k][v],w[son[k][i]]+f[son[k][i]][K]+f[k][v-K-1]);
				}
			}
		}
	}
	return;
}

P1273 有線電視網

也是比較經典的一個樹上揹包問題

題目中要求的是在不虧本的情況下最多的觀看使用者個數

\(f[i][j]\)表示第\(i\)個站傳輸給\(j\)個使用者觀看最終剩餘的錢數

若最終剩餘錢數大於等於0,則說明未虧本

反之,則說明虧本

轉移方程則為:

  • \(f[u][i]=max(f[v][k]+f[u][i-k]-w[u][v])\)

\(dp\)完後從總人數開始倒序判斷是否虧本即可

貼個核心程式碼:

\(dp部分\)

void dfs(int x){
	dp[x][0]=0;
	if(val[x]){//如果是根節點
		size[x]=1;//人數加一
		dp[x][1]=val[x];
		return;
	}
	for(int i=0;i<son[x].size();i++){
		dfs(son[x][i]);
		size[x]+=size[son[x][i]];//計算x節點下的人數總和
		for(int j=size[x];j>=0;j--){//滾動陣列,倒序列舉
			for(int k=1;k<=size[son[x][i]];k++){//列舉子樹傳輸的觀眾數量
				dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[son[x][i]][k]-W[x][son[x][i]]);
			}
		}
	}
	return;
}


\(判斷部分\)

     for(int i=m;i>=0;i--){
	if(dp[1][i]>=0){//如果不虧本
	cout<<i;
	break;
	}
}

P1270 “訪問”美術館

跟P1273 有線電視網一樣的套路

\(f[i][j]\)為在第\(i\)個節點下偷\(j\)幅畫所需要的最小總時間

狀態轉移方程也就呼之欲出了

  • \(f[u][i]=min(f[v][k]+f[u][i-k]-2w[u][v])\)

這裡\(w[u][v]\)要乘2是因為要進出各一趟

核心程式碼:

int dfs(int x){
	if(paint[x]!=0){
		return paint[x];
	}
	int s=0;
	for(int i=0;i<son[x].size();i++){
		
		int v = son[x][i];
		int t=dfs(v);
		s+=t;
		for(int j =s;j>0;j--){
			for(int k=0;k<=t;k++){
				dp[x][j] = min(dp[x][j] , dp[v][k] + dp[x][j-k]+w[x][v]*2);
			}
		}
	}
	return s;
}

P1272 重建道路

同樣也是一道比較經典的樹上揹包問題

\(f[i][j]\)為第\(i\)個節點斷出一個大小為\(j\)的子樹所需要的斷開總數

狀態轉移方程:

  • \(f[u][i]=min(f[v][k]+f[u][i-k]-1)\)

(\(v\)為根的子樹提供\(k\)個節點,\(u\)和其他兒子提供\(j-k\)個節點)

同時,由於一開始時一個子樹都沒有加進來

即把\(u\)的所有"兒子"都切斷了

因此當把\(v\)兒子加進來的時候要把之前那段減去的邊加回來

核心程式碼:

void dfs(int x){
	size[x] = 1;
	if(!is_son[x]){
		dp[x][1] = 0;
		size[x] = 1;
		return;
	}
	for(int i=0;i<son[x].size();i++){
		int v = son[x][i];
		dfs(v);
		size[x]+=size[v];
		for(int j = size[x];j>=0;j--){
			for(int k = 1;k<=size[v];k++){//這裡題解裡很多人都寫成了<j,問題是子樹可能本身就沒有這麼多子節點,感覺有些問題
				dp[x][j] = min(dp[x][j],dp[x][j-k]+dp[v][k]-1);
			}
		}
	}
}

普通樹形\(DP\)

P2016 戰略遊戲

P2458 [SDOI2006]保安站崗 題解

P4084 [USACO17DEC]Barn Painting G

P2585 [ZJOI] 三色二叉樹

P2279 消防局的設立


P2016 戰略遊戲

帶了點貪心思想的樹形\(DP\)

如果父節點放了一個守衛

那其子節點就都不用放守衛了

反之,子節點都要放一個守衛

轉移方程:

  • \(f[u][0]+=f[v][1]\)
  • \(f[u][1]+=min(f[v][1],f[v][0])\)

為什麼不用兒子的兒子("孫子")節點來看守兒子節點?

如果一個節點不是葉子節點,那他的子節點數必定大於或等於\(1\),因此如果用兒子節點來看守其父節點,花費的數量肯定會更多(或不變)。

遺憾的是題解裡似乎沒人說正確性的證明?,還是說太簡單了都懶得證了

核心程式碼:

void dfs(int x){
   if(x!=1509)
	dp[x][1]=1,dp[x][0]=0;
	for(int i=0;i<son[x].size();i++){
		dfs(son[x][i]);
		dp[x][0]+=dp[son[x][i]][1];
		dp[x][1]+=min(dp[son[x][i]][1],dp[son[x][i]][0]);		
	}
}

關於這道題目的帶點權版:

P2458 [SDOI2006]保安站崗

題解連結

P4084 [USACO17DEC]Barn Painting G

樹上\(DP\)求方案數。

還算是比較簡單的題目吧...

設:

\(f[i][0]\)為第\(i\)個節點塗紅色的方案數

\(f[i][1]\)為第\(i\)個節點塗綠色的方案數

\(f[i][2]\)為第\(i\)個節點塗藍色的方案數

假設第\(i\)號節點塗了紅色,那麼它的上一個節點就只能塗綠色和藍色

其他情況也同理

用乘法定理乘一下即可。

轉移方程:

  • \(\begin{cases}f[u][1]=f[u][1]*((f[v][2]+f[v][3]))\\f[u][2]=f[u][2]*((f[v][1]+f[v][3]))\\f[u][3]=f[u][3]*((f[v][1]+f[v][2]))\end{cases}\)

換根\(DP\)

一種形式十分優美的樹形\(DP\)

P3478 [POI2008]STA-Station

P2986 [USACO10MAR]Great Cow Gathering G

P3047 [Nearby Cows G]Great Cow Gathering G

CF708C Centroids

CF1187E Tree Painting


P3478 [POI2008]STA-Station

換根DP的模板題。

這裡我們設

  • \(size[i]\)為以\(1\)為根節點時節點\(i\)的子樹大小

  • \(dep[i]\)為以\(1\)為根節點時節點\(i\)的深度大小

  • \(dp[i]\)為以\(i\)為根節點時深度之和的大小

很明顯,我們可以通過一遍DFS求出以\(1\)為根節點時的深度之和

如果一個個的去算的話

照這個資料範圍,顯然會T飛

這個時候就要用到換根DP了

換根\(DP\)優化

可以看出,當我們把根節點從1換到3時

對子節點3的貢獻由兩部分組成

1.自己子樹的貢獻(圖中的k)

2.父親節點\(1\)的貢獻


如何轉移

  • 首先是\(k\),作為自己子樹所產生的貢獻肯定要加上

  • \(dp[u]\)為以\(u\)為根節點時的深度總值,在計算時,要減去\(v\)的子樹所產生的貢獻,不然就重複計算了,同時

在以 \(u\)為根時,v節點及其子樹內的所有節點的深度都增加了\(1\),需要減去

(圖中紅色的節點)

合起來就是\(dp[u]-(size[v]+k)\)

  • 除v子樹外的其他節點也一樣

在以\(v\)為根時,除\(v\)節點及其子樹外的其他節點的深度都增加了\(1\)

(圖中藍色的節點)

合起來就是\((size[1]-size[v])\)

得到轉移方程

  • \(dp[v] = k+(dp[u]-(k+size[v]))+(size[1]-size[v])\)

化簡一下

  • \(dp[v] = dp[u]-2size[v]+size[1]\)

核心程式碼:

void dfs1(int x){
	size[x] = 1;
    vis[x] = 1;
	for(int i=0;i<son[x].size();i++){
		int v = son[x][i];
		if(!vis[v]){
		dep[v] = dep[x] +1;
		dfs1(v);
		size[x]+=size[v];	
		}
			
	}
}
void dfs2(int x){
    vis[x] = 1;
	for(int i=0;i<son[x].size();i++){
		int v = son[x][i];
		if(!vis[v]){
		dp[v] = dp[x] +size[1] - 2*size[v];
		dfs2(v);	
		}
	}
}

P2986 [USACO10MAR]Great Cow Gathering G

前面那道題目的帶權值版

一模一樣的思路,只需要把狀態轉移方程轉換一下即可。

 void dfs(int u,int fa){
	
	
    for(int i=head[u];i;i=edge[i].next){
    	int v =edge[i].v;
    	if(v==fa) continue;
    	dfs(v,u);
    	size[u] += size[v];
    	sum[u]+=(sum[v]+edge[i].w*size[v]);	
	}
}

 void dp(int u,int fa){
	for(int i=head[u];i;i=edge[i].next){
    	   int v =edge[i].v;
			   	if(v==fa) continue;
			f[v] = 1LL*f[u] + AN*edge[i].w - 2*size[v]*edge[i].w;
			ans = min(ans,f[v]);
			dp(v,u);
	}
}

P3047 [Nearby Cows G]

1.\(狀態表示\)

\(size[i][j]\)為第i個節點向下\(j\)層所包含的點權和

\(f[i][j]\)為第\(i\)個點距離它不超過 \(j\)的所有節點權值和

2.狀態轉移

對於\(size[i][j]:\)

\(size[u][j] =\sum\ size[v][j-1]\) 自己向下\(j\)層即為兒子向下\(j-1\)

對於\(f[i][j]:\)

兒子對它的貢獻:

\(size[v][j]\)

自己向下\(j\)層,兒子節點肯定也要向下\(j\)

父親對它的貢獻:

\(f[u][j-1]-size[v][j-2]\)

父親節點擴充套件\(j-1\)層的值減去和兒子節點的值所重複包含的\(j-2\)層值

轉移方程:

\(f[v][j] = f[u][j-1]+size[v][j]-size[v][j-2]\)

核心程式碼:

void dfs(int u,int fa){
	
	for(int i=head[u];i;i=edge[i].next){
		int v =edge[i].v;
		if(v==fa) continue;
		dep[v]=dep[u]+1;
		dfs(v,u);
		for(int i=1;i<=k;i++){
			size[u][i]+=size[v][i-1];
		}
	}
}
void dp(int u,int fa){
	
	for(int i=head[u];i;i=edge[i].next){
		int v=edge[i].v;
		if(v==fa) continue;

		for(int i=1;i<=k;i++){
			if(i-2>=0)
			f[v][i] = size[v][i]+f[u][i-1] - size[v][i-2];
			else f[v][i] = size[v][i]+f[u][i-1];
		}
			
		dp(v,u);
	}
}

CF708C Centroids

一道做起來比較麻煩的換根\(DP\)

分析

首先對於一個節點來說,大小大於\(n/2\)的節點肯定只有一個,這個顯而易見

再來看如何改造

如果說該節點本身的重兒子就小於\(n/2\),那肯定可以成為樹的重心

反之,肯定要在重兒子裡找出一個重量最大的且小於等於\(n/2\)的子樹,並將其斷開,連線到根節點上(相當於刪去這顆子樹)

如果重兒子的大小減去被刪去兒子的大小小於等於\(n/2\),則說明可以改造

反之,無法改造

如何轉移

分兩種情況來討論

\(1\).該節點不是其父親節點重兒子

其父節點的重兒子不會被改變,只需要判斷該節點的重兒子是否改成其父節點即可

\(2\).該節點是其父親節點的重兒子

其父親節點的重兒子會變為其"次大"兒子,其兒子節點的重兒子不會改變

核心程式碼:

void dfs(int u){
	vis[u] = 1;
	size[u] = 1;
	for(int i=0;i<son[u].size();i++){
		int v = son[u][i];
		if(!vis[v]){
		dfs(v);
		size[u]+=size[v];
		if(size[v] > size[maxson[u]])
			maxson[u] = v;	
	  }	
	 }
	   
			if(maxson[u]!=0){
				
				if(size[maxson[u]]<=n/2) dp[u] = size[maxson[u]]; 
				else dp[u] = dp[maxson[u]];
			}
}


void exchange(int u,int v){
        size[u] = size[u] - size[v];
		size[v] = size[v] + size[u];
		if(v==maxson[u]){
			maxson[u] = 0;
			for(int i=0;i<son[u].size();i++){
				int V = son[u][i];
				if(V!=v&&size[V] > size[maxson[u]]){
					maxson[u] = V;
				} 
			}
			if(maxson[u]!=0){
				
				if(size[maxson[u]]<=n/2) dp[u] = size[maxson[u]];
				else dp[u] = dp[maxson[u]];
			}
		}
		if(size[maxson[v]]<size[u]){
			maxson[v] = u;
			if(maxson[v]!=0){
				
				if(size[maxson[v]]<=n/2) dp[v] = size[maxson[v]];
				else dp[v] = dp[maxson[v]];
			}
		}	
}
void dfs2(int u){
	vis[u]  = 1;
    
	 if(size[maxson[u]]<=n/2||size[maxson[u]] - dp[maxson[u]]<=n/2) ans[u]=1;
	for(int i=0;i<son[u].size();i++){
		int v = son[u][i];
		if(!vis[v]){
			exchange(u,v);
			dfs2(v);
			exchange(v,u);
		}
	}
}

end.

基環樹部分還是先緩緩吧,暫時還未完全掌握