樹形DP。這是個什么東西?為什么叫這個名字?跟其他DP有什么區別?
相信很多初學者在剛剛接觸一種新思想的時候都會有這種問題。
沒錯,樹形DP准確的說是一種DP的思想,將DP建立在樹狀結構的基礎上。
既然說了這是一種思想,那么單講的話,也講不出什么東西來。所以我們結合具體題目進行講解,希望大家可以在題目中領悟這種思想。
提到樹形DP入門題,很多人都會提到沒有上司的舞會這道題,的確,這道題堪稱樹形DP的典范,但是我個人認為,這道題的處理方式不夠普遍,二叉蘋果樹這道題的處理方式相比之下更加普遍。下面我們就將結合這道題進行講解。
題意很簡單,每條邊有一個權值,保留若干條邊,求去掉邊后根節點能夠到達的所有邊的權值和最大是多少。
看到這道題,也許大家會想到數字三角形這道題,但是這里的這棵樹並不是一顆滿二叉樹,甚至也不是一顆完全二叉樹,所以我們不能用數字三角形的處理方式來做這道題。那該怎么思考呢?很明顯,這是一個樹狀結構,我們可以從這點出發來考慮。這道題明確給出了根的位置,也就確定了父子節點的關系,我們會發現,對於每個父節點的狀態,都是由它的子節點轉移過來的,所以我們大概可以得出這里有一個由子節點轉移到父節點的狀態轉移方程,又因為父節點子樹上選擇的邊數完全取決於子節點的子樹選擇的邊數。
\(f[u][i]=max(f[u][i],f[u][i−j−1]+f[v][j]+w)\)
\(f[u][i]\)表示以\(u\)為根節點的子樹選擇\(i\)條邊權值和最大為多少,這實際上就是一個背包。為什么兩個\(f\)數組的邊數為\(i-1\)條呢?因為我們若想取一顆子節點的子樹上的邊,那就必須取父節點與子節點相連的那條邊。
我們下面來看代碼
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc getchar
#define maxn 105
using namespace std;
inline ll read(){
ll a=0;int f=0;char p=gc();
while(!isdigit(p)){f|=p=='-';p=gc();}
while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
return f?-a:a;
}
struct ahaha{
int w,to,next;
}e[maxn<<1];int tot,head[maxn];
inline void add(int u,int v,int w){
e[tot].w=w,e[tot].to=v,e[tot].next=head[u];head[u]=tot++;
}int n,m;
int sz[maxn],f[maxn][maxn];
void dfs(int u,int fa){
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;if(v==fa)continue;
dfs(v,u);sz[u]+=sz[v]+1; //子樹邊數在加上子節點子樹的基礎上還要加一,也就是連接子節點子樹的那條邊
for(int j=min(sz[u],m);j;--j) //由於是01背包,所以要倒序DP
for(int k=min(j-1,sz[v]);k>=0;--k) //這一維正序倒序無所謂,但是把取min放在初始化里可以減少運算次數,算是一個優化的小習慣
f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+e[i].w);
}
}
int main(){memset(head,-1,sizeof head);
n=read();m=read();
for(int i=1;i<n;++i){ //前向星存邊,要存兩邊,便於讀取
int u=read(),v=read(),w=read();
add(u,v,w);add(v,u,w);
}
dfs(1,-1);
printf("%d",f[1][m]);
return 0;
}
以上就是這道題的做法,你理解樹形DP了嗎?
所謂的樹形DP,只不過是將一般DP的線性轉移,變成了在樹上進行轉移,本質並無差別。
下面是幾道本人篩選出的不錯的樹形DP的題目,有意者可以嘗試一下