树形DP总结

Part 0 前言

刚学完了树形DP,是时候来总结一下了!😀

Part 1 基础部分

树形DP就是在树上的DP。

这里先放一下遍历树的模板:

void dfs(int u,int fa){
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==fa)continue;
		dfs(v);
	}
}

DP的操作就放在dfs操作的后面,再加上初值和特判,就变成了这样:

void dfs(int u,int fa){
	|初值定义| 
	if(|特判条件|)|特判操作| 
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==fa)continue;
		dfs(v);
		|DP操作|
	}
}

Part 2 练习部分

P1352 没有上司的舞会

难度:★★☆☆☆

题目大意:给你一棵带点权的树,每次只能取父节点的权或子节点的权,求最大的点权和为多少。

分析:因为能不能区由子节点决定,那么可以设 fi,0/1f_{i ,0/1} 为节点 ii 取(1)和不取(0)的最大权值和。转移方程就为 fi,1=fj,0+aif_{i,1} = f_{j,0} + a_ifi,0=max(fj,0,fj,1)+aif_{i,0} = max(f_{j,0} , f_{j,1})+ a_ijjii 的子节点,aia_i 为节点 ii 的权值)。答案就是 max(froot,0,froot,1)max(f_{root,0},f_{root,1})

代码实现

#include<bits/stdc++.h>
#define maxn 6005
using namespace std;
int n,head[maxn],nex[maxn],to[maxn],f[maxn][2],num[maxn],edgenum;
bool root[maxn];
void addedge(int u,int v){ 
	nex[++edgenum]=head[u];
	to[edgenum]=v;
	head[u]=edgenum;
	root[v]=1;
}
void dp(int u){
	f[u][0]=0;//定义初值 
	f[u][1]=num[u];
	for(int i=head[u];i;i=nex[i]){
		int v=to[i];
		dp(v);
		f[u][0]+=max(f[v][0],f[v][1]);//DP操作 
		f[u][1]+=f[v][0];
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&num[i]);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		addedge(v,u);
	}
	for(int i=1;i<=n;i++)
		if(!root[i]){//是否为根节点 
			dp(i);
			printf("%d\n",max(f[i][0],f[i][1]));//输出答案 
			break;
		}
	return 0;
}

P1122 最大子树和

难度:★☆☆☆☆

题目大意:给你一棵带点权的树,让你求最大的子树权值和。

分析:因为要求最大子树和,所以设 fuf_u 为以节点 uu 为根的最大子树和。fu=max(0,fv)+auf_u = max(0,f_v)+a_uvvuu 的子节点,aua_u 为节点 uu 的权值)。答案就是 fuf_u 的最大值。

代码实现

#include<bits/stdc++.h>
using namespace std;
struct edge {
	int next,to;
} e[100000];
int n,a[100000],head[100000],cnt,f[100000],ans;
void add(int x,int y) {
	e[++cnt].next=head[x];
	e[cnt].to=y;
	head[x]=cnt;
}
void dfs(int u,int fa) {
	f[u]=a[u];
	for(int i=head[u]; i; i=e[i].next) {
		int v=e[i].to;
		if(v!=fa) { 
			dfs(v,u);
			f[u]+=max(0,f[v]);//相加 
		}
	}
	ans=max(ans,f[u]);//记录最大值 
}
int main() {
	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);
		add(x,y);
		add(y,x);
	}
	dfs(1,0);
	printf("%d",ans);
	return 0;
}

P2015 二叉苹果树

难度:★★★☆☆

题目大意:给你一棵带点权的二叉树,求一颗有 QQ 条边的子树的最大点权和。

分析:通过样例可以看出这是一道树上的 01背包 题目,通过类比思想把状态转化为 fx,if_{x,i} 表示当前节点 xx 的子树上保留 ii 条边的最大点权和。转移就变成了这样:

for(int j=q;j>=1;j--)
	for(int k=j-1;k>=0;k--)
		f[x][j]=max(f[x][j],len[i]+f[v][k]+f[x][j-k-1]);

搜索,每一个点找出以它为根节点的1~q的最大利益,除叶节点外。因为连接父节点和子节点还有一条边,所以父节点自己留的可用边-1。

代码实现

#include<bits/stdc++.h>
using namespace std;
int n,q;
int f[1005][1005],edgenum,len[1009],head[1009],to[1005],nxt[1005];
void addedge(int u,int v,int l){
	nxt[++edgenum]=head[u];
	to[edgenum]=v;
	len[edgenum]=l;
	head[u]=edgenum;
}
void dfs(int x,int fa){
	for(int i=head[x];i;i=nxt[i]){
		int v=to[i];
		if(v==fa)continue;
		used[v]=1;
		dfs(v,u);
		for(int j=q;j>=1;j--)//01背包要倒着做 
			for(int k=j-1;k>=0;k--)
				f[x][j]=max(f[x][j],len[i]+f[v][k]+f[x][j-k-1]);
	}
}

int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<n;i++){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		addedge(x,y,z);//双向建边 
		addedge(y,x,z);
	}
	dfs(1,0);
	printf("%d",f[1][q]);
	return 0;
}

P2014 [CTSC1997]选课

难度:★★★☆☆

题目大意:给你一棵带点权的树,求一棵有 MM 个点的子树的最大权值和。

分析:跟上面一题差不多,只是变成了单向边。

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=305,M=2001;
int f[N][N],edgenum,n,m;
int head[M],nxt[M],to[M];
void addedge(int u,int v){
    nxt[++edgenum]=head[u];
    to[edgenum]=v;
    head[u]=edgenum;
}
void dp(int x){
    for(int e=head[x];e;e=nxt[e]){
        int v=to[e];
        dp(v);
        for(int i=m+1;i>1;i--)
            for(int j=i-1;j>=1;j--)
                f[x][i]=max(f[x][i],f[x][i-j]+f[v][j]);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        f[i][1]=b;
        addedge(a,i);
    }
    dp(0);
    printf("%d\n",f[0][m+1]);
    return 0;
}

P1273 有线电视网

难度:★★★★☆

题目大意:选出一个节点数量最多的叶子节点的集合,使这些点到根节点的路径上的权值和减去集合中所有的点的权值大于等于零。

分析:设 fi,jf_{i,j} 表示在 ii 的子树中,选择 jj 个叶子节点,点权-边权的最大值。dp[u][j]=max(dp[u][j],dp[u][j-i]+dp[v][i]-edge[e].w);,最后找第一个大于等于零的 f1,if_{1,i} 即为答案。

代码实现

#include<bits/stdc++.h>
using namespace std;
int n,m,head[3010],edgenum,val[3010],dp[3010][3010];
struct node {
	int to,next,w;
} edge[10000];
void adde(int u,int v,int w) {
	edge[++edgenum].to=v;
	edge[edgenum].next=head[u];
	edge[edgenum].w=w;
	head[u]=edgenum;
}
int dfs(int u) {
	if(u>n-m) {
		dp[u][1]=val[u];
		return 1;
	}
	int sum=0,t;
	for(int e=head[u]; e; e=edge[e].next) {
		int v=edge[e].to;
		t=dfs(v);
		sum+=t;
		for(int j=sum; j>0; j--)
			for(int i=1; i<=t; i++)
				if(j-i>=0) dp[u][j]=max(dp[u][j],dp[u][j-i]+dp[v][i]-edge[e].w);
	}
	return sum;
}
int main() {
	memset(dp,~0x3f,sizeof(dp));
	scanf("%d%d",&n,&m);
	for(int u=1; u<=n-m; u++) {
		int size,v,w;
		scanf("%d",&size);
		for(int j=1; j<=size; j++) {
			scanf("%d%d",&v,&w);
			adde(u,v,w);
		}
	}
	for(int i=n-m+1; i<=n; i++)
		scanf("%d",&val[i]);
	for (int i=1; i<=n; i++)
		dp[i][0]=0;
	dfs(1);
	for (int i=m; i>=1; i--)
		if (dp[1][i]>=0) {
			printf("%d",i);
			break;
		}
	return 0;
}