【树形DP】

【树形DP】

参考:https://www.cnblogs.com/hanruyun/p/9788170.html

总结

典型例题

烷烃计数

https://atcoder.jp/contests/abc394/tasks/abc394_f

/*【树形DP】
状态表示:dp[u][j]:以u为中心节点 选择j条分支所能得到的最大子树
状态转移:只能选择1或者4个分支->不需要统计度数 每个节点的贡献值为1
         不添加分支(0)+添加3个分支(3)
*/
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
const int INF=-0x3f3f3f3f;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const int N=1e6+10;
int n;
int h[N],ne[N],e[N],idx;
int dp[N][5];
int ans=INF;
void add(int x,int y){
      e[idx]=y;
      ne[idx]=h[x];
      h[x]=idx++;
}
void dfs(int u,int fa){
      dp[u][0]=1;
      for(int i=h[u];i!=-1;i=ne[i]){
            int j=e[i];
            if(j!=fa){
                  //先递归找子节点
                  dfs(j,u);
                  for(int k=4;k>=1;k--){//必须要从4开始一路迭代下去->才能一直更新:选择j个分支的情况是由选择j-1个分支扩展的
                                                      //要么加3个分支,要么一个分支都不加
                        dp[u][k]=max(dp[u][k],dp[u][k-1]+max(dp[j][3],dp[j][0]));
                  }
            }
      }
      ans=max(ans,dp[u][1]);
      ans=max(ans,dp[u][4]);
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>n;
      memset(h,-1,sizeof h);
      //初始化dp最小
      for(int i=0;i<=n;i++){
            for(int j=0;j<=4;j++) dp[i][j]=INF;
      }
      for(int i=1;i<=n-1;i++){
            int u,v;
            cin>>u>>v;
            add(u,v);add(v,u);
      }
      dfs(1,-1);
      if(ans>4) cout<<ans;
      else cout<<"-1";
      return 0;
}

非典型例题

没有上司的舞会

https://fjnuacm.top/d/minor/p/286?tid=66cdb20d71a91e475b1e6ac7

这样选最多
image

【DP分析】

image
image

代码

#include<bits/stdc++.h>
using namespace std;
const int N=6010;
int n;
int happy[N];
int h[N],e[N],ne[N],idx;
int f[N][2];//第二维:0不选 1选 
//题目没有告诉我们父节点:要自己找
bool has_father[N];
void add(int x,int y){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int u){
	f[u][1]=happy[u];//选的话要把该点happy值加上 
	//递归找子树 
	for(int i=h[u];i!=-1;i=ne[i]){
		int j=e[i];
		dfs(j);
		//若不选这个点:在子树的选与不选中取最大值在+ 
		f[u][0]+=max(f[j][1],f[j][0]);
		//若选这个点:子树不能选 
		f[u][1]+=f[j][0];
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&happy[i]);
	memset(h,-1,sizeof h);//邻接表一定要初始化! 
	for(int i=0;i<n-1;i++){
		int a,b;
		scanf("%d%d",&a,&b);
		has_father[a]=true;
		add(b,a);
	}
	//找根:没有父节点的 
	int root=1;
	while(has_father[root]) root++;
	dfs(root);
	int ans=max(f[root][0],f[root][1]);
	printf("%d",ans);
	return 0;
}

二叉苹果树

https://fjnuacm.top/d/minor/p/287?tid=66cdb20d71a91e475b1e6ac7
【树形dp一般套路】
(1)dp[u][i][j]... :
当前节点为u
前i个节点
j... 表示其他状态:重量?
(2)一般要枚举第二维节点状态:for(int k=0;k<=j;k++)...
(3)dp式内(一般是dfs有返回值时)/提前递归处理子树

思路+代码

/*【树形dp】

【状态表示】 
dp[i][j]表示为,当前结点为i,保留树枝条数为j的情况下,所留下苹果数的最大值

【特殊情况】
(1)ls[i]==0 && rs[i]==0 -->叶节点-->返回0
(2)j==0 保留树枝条数为0

【状态转移】 
设中间量k:枚举,给左儿子分配的树枝数k,给右儿子分配的即为 j-k 
一般式:
dp[i][j]=max(dp[ls[i]][k-1]+dp[rs[i]][j-k-1]+la[i]+ra[i])
最左/最右单独处理
k=0 没有左边 dp[rs[i]][j-1]+ra[i] 
k=j 没有右边 dp[ls[i]][j-1]+la[i]
-->递归完成 
*/ 
#include<bits/stdc++.h> 
using namespace std;
const int N=1100;//注意这里:要开邻接表且双向存图->空间会更大 
int n,q;
/*
ls rs左右儿子  
la ra 左右儿子边上的苹果 
*/
int ls[N],rs[N],la[N],ra[N]; 
int val[N],h[N],e[N],ne[N],idx;
int dp[N][N];

//邻接表-->注意val值也被idx表示!-->双链表存值不会乱(一个点可能有两个值) 
void add(int x,int y,int v){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx;
	val[idx++]=v;
}
//※将邻接表转换为树
void tree(int x,int fa){//当前值和父节点值 
	int g=0;//计数器,分配左右儿子
	for(int i=h[x];i!=-1;i=ne[i]){
		int y=e[i];
		if(y!=fa){//在单链表中,不是父节点就是儿子节点 
			g++;
			if(g==1){//分配左儿子 
				ls[x]=y;
				la[x]=val[i];//注意val下标是idx的计数! 
			}
			else{
				rs[x]=y;
				ra[x]=val[i];
			}
			tree(y,x);//递归建树 
		}
	} 
}
int dfs(int i,int j){ //dp[i][j]以i为根,保留j个树枝的最大值
	//特殊情况
	if(ls[i]==0 && rs[i]==0) return 0;
	if(j==0) return 0;
	if(dp[i][j]>0) return dp[i][j];//※一个优化剪枝:这个点已经被更新过了就直接返回
	//dp递归写法! 
	for(int k=0;k<=j;k++){
		//不走左边 走右边 
		if(k==0) dp[i][j]=max(dp[i][j],dfs(rs[i],j-1)+ra[i]);
		//不走右边 走左边 -->注意else 
		else if(k==j) dp[i][j]=max(dp[i][j],dfs(ls[i],j-1)+la[i]);
		//一般情况:给左边分k个 右边就是j-k个 
		//由于走到儿子结点都需要经过一条树枝,所以实际上分配数都要-1
		else dp[i][j]=max(dp[i][j],dfs(ls[i],k-1)+la[i]+dfs(rs[i],j-k-1)+ra[i]);
	}
	return dp[i][j];
}
int main(){
	scanf("%d%d",&n,&q);
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++){
		int aa,bb,qq;
		scanf("%d%d%d",&aa,&bb,&qq);
		//无向图:建双边
		add(aa,bb,qq);
		add(bb,aa,qq);
	}
	//建树
	tree(1,0);
	int ans=dfs(1,q);
	printf("%d",ans);
	return 0; 
}

选课

https://fjnuacm.top/d/minor/p/288?tid=66cdb20d71a91e475b1e6ac7
※注意本题提前递归子树

【代码】

/*
【状态表示】 
dp[u][i]表示以节点u为根的子树,选择i个点可以获得的最大权值和
-->发现每个子节点都会占用父节点i的一部分,又有一个贡献,可以选择或不选择
-->01背包
-->dp[u][i][j]表示节点u的前i个子节点,限重为j能得到的最大权值和 
-->优化:dp[u][j]表示节点u,限重j的最大权值和
-->dp[i][1]表示该节点本身权值(只能选自己) --> 递推起点dp[now][1][1]=val[now]

【状态转移】
dp[u][i][j]=max(dp[now][i-1][j],dp[son][所有节点数][k]+dp[now][i-1][j-k]);
-->我要用到i-1的内容 都是满足k<j的 -->所以倒着循环j -->01背包优化 

for(int i=head[u]; i; i=e[i].next)//遍历所有子节点
	for(int j=n, v=e[i].to; j>0; --j)//这里和01背包一样,总重从大到小循环
		for(int k=0; k<j; ++k)//这里是不同之处,子节点的重量需要规定
			dp[u][j]=max(dp[u][j], dp[u][j-k]+dp[v][k]); dp[v][k]=dp[son][所有节点数][k] 递归处理儿子 
			
*/ 

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int h[N],ne[N],e[N],idx;
int n,m;
int dp[N][N];
void add(int x,int y){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int u){
	//递归先处理子节点
	for(int i=h[u];i!=-1;i=ne[i]) dfs(e[i]);
	
	for(int i=h[u];i!=-1;i=ne[i]){
		int y=e[i];
		for(int j=n;j>0;j--){//01背包优化:总重从大到小循环 
			for(int k=0;k<j;k++){//根节点一定要被选:dp[u][j-k]一定要留1 
				dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[y][k]);
			}
		}
	}
}
int main(){
	scanf("%d%d",&m,&n);
	n++;//原题为森林,要加0(根节点)-->必须被选 
	memset(h,-1,sizeof h);
	for(int i=1;i<=m;i++){
		int aa;
		//会有一个空节点0,如果是不需要先修课的就加到头节点下面去 
		scanf("%d%d",&aa,&dp[i][1]);//不用专门存val 
		add(aa,i);
	}
	dfs(0);
	printf("%d",dp[0][n]);
	return 0;
}

加分二叉树

https://www.luogu.com.cn/problem/P1040
※树形dp写法+二叉树特性

/*【思路】
中序遍历:每一个节点都有可能成为根
计算规则:左子树x右子树+根节点->递归定义
->有重复状态
【区间DP】 
dp[i][j] -> 节点i到j成树的最大得分 -> 答案即为dp[1][n]
->区间内枚举根即可
初始化:dp[i][i]=a[i]
->注意左右子树为空的情况(一定不是最优解) 
根据中序遍历性质:dp[i][k-1]左子树 dp[i][k+1]右子树
【输出前序遍历】
root[i][j] -> 记录i-j成树时的根节点 -> 递归输出 
*/ 
#include<bits/stdc++.h>
using namespace std;
const int N=35;
int n,a[N];//注意a是存值! 
long long f[N][N];//注意开longlong 
int root[N][N];
//递归输出前序遍历(输出节点编号):根->左->右 
void print(int l,int r){
	if(l>r) return;//递归出口 
	//输出根节点
	printf("%d ",root[l][r]);
	if(l==r) return;//该树只有一个节点->退出
	//找左子树 
	print(l,root[l][r]-1);
	//找右子树
	print(root[l][r]+1,r);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		//初始化dp和root
		f[i][i]=a[i];
		f[i][i-1]=1;
		f[i+1][i]=1;
		root[i][i]=i;//自己是自己树的根节点 
	}
	for(int len=1;len<=n;len++){
		for(int i=1;i+len<=n;i++){
			int j=i+len;//要讨论左右子树为空->可初始化左子树空 右子树空在找的时候体现->长度要溢出一格 
			f[i][j]=f[i+1][j]+f[i][i];//左右子树为空时->一定不是最优解 
			root[i][j]=i;//默认从起点选根
			for(int k=i;k<=j;k++){//会出现下标反的情况->约定为1(看上面初始化) 
				int res=f[i][k-1]*f[k+1][j]+f[k][k];
				if(f[i][j]<res){
					f[i][j]=res;
					root[i][j]=k;
				}
			}
		}
	}
	printf("%lld\n",f[1][n]);
	print(1,n);
	return 0;
}

皇宫看守

https://www.acwing.com/file_system/file/content/whole/index/content/4184022/

【解法一】树形DP

※状态设计

//要开longlong! 
/*【解法2:树形dp】 
线性dp->转2维->【状态设计】->要拓展不放守卫的情况:父亲,儿子 
dp最小值 
dp[i][1]:当前点放守卫 
dp[i][2]:当前点不放守卫->被子节点监视->找最小儿子
dp[i][3]:当前点不放守卫->被父节点监视->子节点的子节点要放 
*/ 
#include<bits/stdc++.h>
using namespace std;
const int N=15000;
typedef long long ll;
int n;
ll k[N];
int e[N],h[N],ne[N],idx;
ll dp[N][5]; 
bool has_father[N];
void add(int x,int y){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int u){
	for(int i=h[u];i!=-1;i=ne[i]){
		int j=e[i];
		dfs(j);//先递归找子树->从下向上找
		//情况1
		dp[u][1]+=min(dp[j][1],min(dp[j][2],dp[j][3])); 
		//情况3:儿子的儿子一定要有 
		dp[u][3]+=dp[j][2];
	}
	//选自己要加上自己 
	dp[u][1]+=k[u];
	//处理情况2:找自己最小的儿子->其他儿子保持原样->无所谓 
	int sum=0;//先记录处理前的所有子节点的状态 
	for(int i=h[u];i!=-1;i=ne[i]){
		int j=e[i];
		sum+=min(dp[j][1],dp[j][2]);//记录最小值 
	}
	//初始化一个最大值 
	dp[u][2]=2147483640;
	for(int i=h[u];i!=-1;i=ne[i]){
		int j=e[i];//j点放守卫->去掉j点原先状态,并加上放守卫的状态(其他点无关) 
		dp[u][2]=min(dp[u][2],sum-min(dp[j][1],dp[j][2])+dp[j][1]);
	}
}
int main(){
	scanf("%d",&n);
	memset(h,-1,sizeof h);
	for(int i=1;i<=n;i++){
		int tmp;
		scanf("%d",&tmp);
		scanf("%lld",&k[tmp]);
		int m;
		scanf("%d",&m);
		while(m--){
			int son;
			scanf("%d",&son);
			add(tmp,son);//默认往下找 
			has_father[son]=1;
		}
	}
	//找根
	int root=1;
	while(has_father[root]) root++;
	dfs(root);
	ll ans=min(dp[root][1],dp[root][2]);
	printf("%lld",ans);
	return 0;
} 

叶子的染色

https://fjnuacm.top/d/minor/p/292?tid=66cdb20d71a91e475b1e6ac7

思路

【状态表示】f[i][j] 第i个点 将其染成j颜色 所耗费的次数
【状态转移】
如果某一点需要染成x色,且它的父节点已经被染成x色->它不需要被染色
->(1)直接继承父亲对应颜色的次数
  (2)保持父亲为非x色,并单独将此节点染成x色
->状态转移方程: 
u是v的父亲节点 
f[u][0]+=min(f[v][0]-1,f[v][1])
f[u][1]+=min(f[v][1]-1,f[v][0])
->将一个节点 染成颜色j的代价,即为其所有子节点染成颜色j代价的和

image

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=10010,M=300010;
const int INF=2147483647; 
int m,n;
int c[N];
int h[N],ne[M],e[M],idx;
int f[N][3];
int root=-1;
void add(int x,int y){
	e[idx]=y;
	ne[idx]=h[x];
	h[x]=idx++;
}
void dfs(int u,int fa){
	if(u<=n) return;//叶节点直接跳过
	for(int i=h[u];i!=-1;i=ne[i]){
		int j=e[i];
		if(j!=fa){//跳过父节点 
			dfs(j,u);//从下而上->递归找子树
			f[u][0]+=min(f[j][0]-1,f[j][1]);
			f[u][1]+=min(f[j][1]-1,f[j][0]);
		}
	}
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>m>>n;//n以内的点都是儿子 
	for(int i=1;i<=n;i++) cin>>c[i];
	memset(h,-1,sizeof h);
	for(int i=1;i<m;i++){
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);
	}
	root=n+1;//随便找一个点当根 
	//初始化
	for(int i=1;i<=m;i++){
		f[i][0]=f[i][1]=1;
		if(i<=n) f[i][!c[i]]=INF;//叶节点:表示不应染成该色 
	}
	dfs(root,root);
	int ans=min(f[root][0],f[root][1]);
	cout<<ans;
      return 0;
}

三色二叉树

https://fjnuacm.top/d/minor/p/293?tid=66cdb20d71a91e475b1e6ac7
注意二叉树存树方式:邻接矩阵
注意没有dfs而是用递推(二叉树性质)

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500010;
string s;
int n; 
//二叉树存节点:邻接矩阵 
int tree[N][2],idx;
//tr[i][0]表示i的左儿子
//tr[i][1]表示i的右儿子 
int dp[N][3];//0染绿色 1不染绿色 
int ansmax=-1,ansmin=0x3f3f3f3f;
void tr(int x){
	idx++;//idx表示访问过多少个节点(从1开始计数) 
	if(s[x]=='0') return;//叶节点直接return
	if(s[x]=='1'){
		tree[x][0]=x+1;//下一个访问的节点的编号一定是这个节点编号+1
		tr(x+1);
	}
	if(s[x]=='2'){
		tree[x][0]=x+1;
		tr(x+1);
		//注意右子树建树过程! 
		tree[x][1]=idx+1;//右节点一定是总共访问的编号+1
		tr(idx+1);
	}
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>s;
      n=s.size();
      s=' '+s;
      tr(1);
      //递推dp:直接从后往前(叶节点开始)即可
	for(int i=n;i>=1;i--){//若i遍历到了叶节点:默认左右子树为0->可初始化染色时方案数为1 
		dp[i][0]=dp[tree[i][0]][1]+dp[tree[i][1]][1]+1;//若染色:两个子树不染色+1 
		dp[i][1]=max(dp[tree[i][0]][0]+dp[tree[i][1]][1],dp[tree[i][1]][0]+dp[tree[i][0]][1]);
	}
	ansmax=max(dp[1][0],dp[1][1]);
	memset(dp,0,sizeof dp);
	for(int i=n;i>=1;i--){
		dp[i][0]=dp[tree[i][0]][1]+dp[tree[i][1]][1]+1;
		dp[i][1]=min(dp[tree[i][0]][0]+dp[tree[i][1]][1],dp[tree[i][1]][0]+dp[tree[i][0]][1]);
	}
	ansmin=min(dp[1][0],dp[1][1]);
	cout<<ansmax<<" "<<ansmin;
      return 0;
}
posted @ 2024-12-20 11:38  White_ink  阅读(56)  评论(0)    收藏  举报