树形dp基础

分类

选点

  • 最大独立集:在树上选取一些点,使得点之间两两之间没有边相连,求能选取的最多点数或最大点权。
  • 最小点覆盖:选取最少的点,使得树中的每条边都至少有一个端点被选中。可通过最大独立集的关系转化求解,因为树上的最大独立集与最小点覆盖互补。

选线

............

树上背包

............

树上选点问题

  • 状态定义:根据问题的性质和求解目标,合理定义状态。一般为dp[i][0/1]表示选和不选,如最大独立集
  • 状态转移:依据树的结构,通过子节点答案推导父节点答案,再根据题目要求一选和不选分类
  • 遍历顺序:一般dfs遍历树,先计算子节点的状态,在根据子节点的状态向上更新,这样可以确保在计算每个节点的状态时,所需的子问题已求出。

P1122 最大子树和

状态

f[i]表示以i为根,点权和最大的一棵子树或者只有根

转移

遍历i的每一个子节点j,可以选或者不选,如果选答案就是f[i]+f[j],不选就是f[i],取较大值

答案,初始化

以1为根节点,答案就是f[1]
f[i]的初始值是a[i],因为以i为根必须选i,不然答案最后没有意义,题目也没有说可以全部截掉

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
vector<int>G[16010];
int n,a[16010],ans=-1e9;
int f[16010];
void dfs(int u,int fa){
	f[u]=a[u];
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fa)continue;
		dfs(v,u);
		f[u]=max(f[u],f[u]+f[v]);
	}
	ans=max(ans,f[u]);
	return;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	cout<<ans<<'\n';
	return 0;
}

P1352 没有上司的舞会

状态

f[i][0/1]表示选取i和不选取i

转移

选i f[i][1]

初始化肯定是a[1],因为这个点已经选了,遍历子节点,对于每个子节点来说,因为自己已经选了,所以每个子节点只能加上不选的情况,即f[j][0],当然子节点肯能带来负贡献,所以要将f[j][0]+f[i][1]和f[i][1]取较大值。

不选i f[i][0]

初始化f[i][0]为0,因为没有选。
遍历子节点,因为自己没有选,所以子节点可以选也可以不选,也可以直接不选整个当前子树,将这几种情况全部取max
f[i][0]=max(f[i][0],f[i][0]+max(f[j][0],f[j][1]))

答案

因为不知道根节点选还是不选,所以直接去max

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
vector<int>G[16010];
int n,a[16010],ans=-1e9;
int f[16010][2];
void dfs(int u,int fa){
	f[u][1]=a[u];
	f[u][0]=0;
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fa)continue;
		dfs(v,u);
		f[u][0]=max(f[u][0],f[u][0]+max(f[v][0],f[v][1]));
		f[u][1]=max(f[u][1],f[u][1]+f[v][0]);
	}
	return;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	cout<<max(f[1][0],f[1][1])<<'\n';
	return 0;
}

战略游戏

状态

f[i][0/1]表示i这可子树满足条件时选i和不选i的最小放置数量。

转移

选i f[i][1]

因为选i,所以初始值f[i][1]为1,因为这个节点已经选了。因为i选了,所以子节点j可以选也可以不选,取最小值,然后把每棵子树的贡献加起来,就是i这一整个的贡献

不选i f[i][0]

没有选,所以f[i][0]初始为0,但是因为i没有选,所以子节点一定要选,加上f[j][1]

答案

不确定选子节点更优还是不选更优,所以在f[1][0]和f[1][1]只见取最小值

#include<bits/stdc++.h>
#define ll long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;

int n,a[1000010];
int f[1000010][2];
vector<int >G[1000010];
void dfs(int u,int fa){
	f[u][1]=1;
	f[u][0]=0;
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fa)continue;
		dfs(v,u);
		f[u][1]+=min(f[v][0],f[v][1]);
		f[u][0]+=f[v][1];
	}
} 
int main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
		int u,v,k;
		cin>>u>>k;
		u++;
		while(k--){
			cin>>v;
			v++;
			G[u].push_back(v);
			G[v].push_back(u);
		}
	}
	dfs(1,0);
	cout<<min(f[1][0],f[1][1]);
	return 0;
}

三色二叉树

建树

递归处理,用一个标记表示现在正在处理的节点编号,返回当前节点编号,如果是2就左右节点都递归,是1就只递归左子树,是0不管

状态

因为最大最小其实是一样的,所以用两个dp数组分别表示就好了。
f[i][0/1/2]分别表示i涂三种颜色的最优值

转移

对于f[i][0]它已经涂了这个颜色,所以它的贡献是1,对于f[i][1]和f[i][2]它们的初始值为0
转移f[i][0],它自己已经涂了0,所以子节点就只能涂1和2,要么左子树涂1,右子树涂2,要么左子树涂2,右子树涂1,两者之间取更优值
f[i][1]和f[i][2]的转移是一样的

答案和初始化

因为不知道最终答案是使根节点涂什么颜色更优,所以取最优值
求最大的初始为0,求最小值初始化为极大值
但是最小值的f[0][0/1/2]要初始化为0,因为左右子树没有赋值时是0,是极大值的话会影响最终答案

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
int n,tot;
string s;
int l[500010],r[500010];
int f1[500010][3];
int f2[500010][3];
int init(){
	int u=++tot;
	if(s[u]=='2'){
		l[u]=init();
		r[u]=init();
	}
	else if(s[u]=='1'){
		l[u]=init();
	} 
	return u;
}
void dfs(int u){
	if(u==0)return;
	f1[u][0]=f2[u][0]=1;
	f1[u][1]=f2[u][1]=f1[u][2]=f2[u][2]=0;
	int ll=l[u],rr=r[u];
	dfs(ll),dfs(rr);
	f1[u][0]+=max(f1[ll][1]+f1[rr][2],f1[ll][2]+f1[rr][1]);
	f2[u][0]+=min(f2[ll][1]+f2[rr][2],f2[ll][2]+f2[rr][1]);
	
	f1[u][1]+=max(f1[ll][0]+f1[rr][2],f1[ll][2]+f1[rr][0]);
	f2[u][1]+=min(f2[ll][0]+f2[rr][2],f2[ll][2]+f2[rr][0]);
	
	f1[u][2]+=max(f1[ll][0]+f1[rr][1],f1[ll][1]+f1[rr][0]);
	f2[u][2]+=min(f2[ll][0]+f2[rr][1],f2[ll][1]+f2[rr][0]);
	return;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>s;
	n=s.size();
	s=' '+s;
	init();
//	for(int i=1;i<=n;i++){
//		cout<<i<<'\n';
//		for(int j=0;j<G[i].size();j++)cout<<G[i][j]<<' ';
//		cout<<'\n';
//	}
	memset(f2,63,sizeof f2);
	f2[0][0]=f2[0][1]=f2[0][2]=0;
	dfs(1);
	cout<<max(f1[1][0],max(f1[1][1],f1[1][2]))<<' '<<min(f2[1][0],min(f2[1][1],f2[1][2]));
	return 0;
}

Barn Painting G

状态

f[i][1/2/3]表示i涂三种颜色的方案数

转移

与上一题相同
当前节点涂1时,那么遍历子节点,只能涂2和3,方案数用乘法,f[i][1]*=(f[j][2]+f[j][3]),涂2,3一样
初始化时,如果这个节点题目已经给它着色,那么已着色的方案数颜色是1,其他的初始化为0,若没有着色说明可以任意涂色,所以全部初始化为1。

答案

如果根节点没有涂色,最终答案是根节点涂三种颜色的方案数之和,因为可以任意如色,不然根节点涂的什么颜色,最终答案就是根节点涂那种颜色的方案数

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
int n,k;
int f[100010][4];
int a[100010];
vector<int>G[100010];
const int mod=1e9+7; 
void dfs(int u,int fa){
	f[u][a[u]]=1;
	if(a[u]==0){
		f[u][1]=f[u][2]=f[u][3]=1;
	}
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fa)continue;
		dfs(v, u);
		f[u][1]*=(f[v][2]+f[v][3]); 
		f[u][2]*=(f[v][1]+f[v][3]);
		f[u][3]*=(f[v][1]+f[v][2]);
		f[u][1]%=mod;
		f[u][2]%=mod;
		f[u][3]%=mod; 
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u); 
	}
	for(int i=1;i<=k;i++){
		int id,cl;
		cin>>id>>cl;
		a[id]=cl;
	}
	dfs(1,0);
//	for(int i=1;i<=n;i++)cout<<f[i][0]<<' '<<f[i][1]<<' '<<f[i][2]<<' '<<f[i][3]<<'\n';
	int ans=0;
	ans=f[1][a[1]];
	if(a[1]==0){
		ans=f[1][1]+f[1][2]+f[1][3];
	}
	cout<<ans%mod<<'\n'; 
	return 0;
}
posted @ 2025-08-01 14:49  wmq2012  阅读(6)  评论(0)    收藏  举报