樹形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]
主要的瓶頸就在於:如何求出每個點的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
這道題目藍書上面有講解,就不贅述了。