树状dp入门题合集

10 篇文章 0 订阅

题目:

  • POJ1463
  • POJ3342
  • Codeforces1084D
  • Codeforces1118F1
  • POJ1655

- POJ1463

题意:原题意是让你选择尽可能少的点,选择的这些点和它最临近点加起来可以涵盖所有点。换一种离散数学上的说法,就是给你一棵树(树是一种连通无回路的图),让你把这棵树变成一个二部图,然后求两部分哪个部分顶点。

思路:树(记忆)形(化)d(搜)p(索),从任一个点出发(我写的 任一个点出发的代码过不了 按题解改成从根节点出发就能过 咱也不知道为啥 咱也不敢问TAT),开始深搜,到叶节点为终止点,记录下是否选择开始回溯(注意叶节点是否选择在刚开始就初始化了),dp[i][0]表示i这个点不选,所需要选的最少数量,dp[i][1]表示这个点选,所需要选的最少数量。

注意,如果父节点被选,那子节点可以被选也可以不被选;如果父节点没被选,那子节点一定要选。可以和这篇博客的第二道题POJ3342对比一下,第二道也是父子节点有约束情况。
两道题具体不同:本题是要求要覆盖所有的结点,并且要尽可能少;而下面party那道题是要求尽可能多的人去,但是父节点去了子节点就不能去。
所以说:前者是儿子来约束父亲,如果父亲没选(照顾不到儿子),那孩子必须选,而父亲选了,孩子可以选也可以不选,即:

dp0+=dp[to][1];
dp1+=min(dp[to][1],dp[to][0]);

后者是父亲来约束儿子,如果父亲去了,儿子一定不去,而父亲不去,儿子可以去也可以不去,即:

dp1+=dp[to][0];
dp0+=max(dp[to][0],dp[to][1]);

两题看似差不多,但是有这样细微的差别,一定要搞明白!!!

AC代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define maxn 1600
using namespace std;
int n,cnt;
int dp[maxn][2],con[maxn],out[maxn],root[maxn]; //dp[x][0]代表
struct Side{
	int y,next;
}side[maxn<<2];
int head[maxn];//记录第一个点连接的边的编号
void init(){
	cnt=0;
	memset(dp,0,sizeof(dp));
	memset(con,0,sizeof(con));
	memset(side,0,sizeof(side));
	memset(head,-1,sizeof(head));//初始化为-1!!!
	memset(out,0,sizeof(out));
	memset(root,0,sizeof(root));
}
void add(int x,int y){
	cnt++;
	side[cnt].y=y;
	side[cnt].next=head[x];
	head[x]=cnt;
}
void tree_dp(int rt){
	int dp0=0,dp1=0;
	con[rt]=1;
	if(!out[rt]) return;//若是叶节点 就返回
	for(int i=head[rt];i!=-1;i=side[i].next){
		int to=side[i].y;
		if(!con[to]){//若这个点没来过  防止倒搜 
			tree_dp(to);
			dp0+=dp[to][1];
			dp1+=min(dp[to][1],dp[to][0]);
		}
	}
	dp[rt][0]=dp0;
	dp[rt][1]=dp1+1;
}
int main() {
	//freopen("stdin.txt","r",stdin);
	while(scanf("%d",&n)!=EOF) {
		init();
		char c;
		for(int i=0; i<n; i++) {//从0开始!!! 
			int sta,num;//起始点编号和边数
			scanf("%d%c%c%d%c",&sta,&c,&c,&num,&c);
			int end;//终点编号
			for(int j=1; j<=num; j++) {
				scanf("%d",&end);
				add(sta,end);
				add(end,sta);
				out[sta]++;
				root[end]=1;
			}
		}
		int rt;
		for(int i=0;i<n;i++){
			if(!root[i]) rt=i;
			if(!out[i]){//叶子节点
				dp[i][1]=1;
				dp[i][0]=0;
			}
		}
		tree_dp(rt);
		int minn=min(dp[rt][0],dp[rt][1]);
		printf("%d\n",minn);//求个最小值!!!至少嘛! 
	}
	return 0;
}

- POJ3342

题意:上一题题解有提到,即公司组织参加舞会,公司成员之间有隶属关系,隶属关系之间有约束,即相邻的两级,上级去,他的下级就不能去。即爹去,儿不能去,爹不去,儿可去可不去。
dp[i][0]代表不选i这个点时候子树所能去的最大人数,dp[i][1]表示选i这个点的时候子树所能去的最大人数。
这题多了一点小思维点就是判断方案是否重复。这样就需要多开一个unique数组。 开什么数组!都说了是个小思维。当然这个小思维我是没有想出来的,毕竟是个菜鸡儿。
判重复思路借鉴:https://blog.csdn.net/weixin_43179892/article/details/84076430
注意先别急着点链接!!!别点链接!!!看看下面我讲的,应该能看懂的!!!

方法很巧妙!下面说一下大体思路,(我猜那人的)大题思路是根据这个核心代码片段来的:

dp1+=dp[to][0];
dp0+=max(dp[to][0],dp[to][1]);

首先,爹去,一定选择儿子不去,其次,爹若不去,用u代表儿子,一定选max(dp[u][0],dp[u][1])里面较大的。
所以我们遍历所有点,找到一个点u使得dp[u][0]>=dp[u][1],这样不论v的爹去不去,dp[u][0]都是被选的,也就是说一定是不选u的。在这种情况下(u不选,即u的孩子都是可选可不选),那么再看看u的子节点v,若出现dp[v][1]和dp[v][0]相等的情况,那就说明有重复的情况啦!可以根据下面的核心代码想一下。
最后因为没有判断过大boss,所以这里需要特判一下!

关键判重代码:

int flag=0;//0无重复
		for(int i=1;i<=n;i++){
			if(dp[i][0]>=dp[i][1]){//不论i的父节点选不选 都会选dp[i][0] 
				for(int j=head[i];j!=-1;j=side[j].next){//又写成i了!!! 
					int to=side[j].y;//写成i了zz。。。
					if(dp[to][0]==dp[to][1]){
						flag=1;break;//有重复的
					}
				}
			}
			if(flag) break;
		}
		if(dp[1][0]==dp[1][1]) flag=1; //boss这里需要特判!

AC代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<map>
#include<string>
#define ll long long
#define maxn 220
using namespace std;
int n,num,cnt;//num人数,cnt边数 
int dp[maxn][2];
map<string,int> mp;
struct Side{
	int y,next;
}side[maxn<<1];
int head[maxn];
void init(){
	num=0,cnt=0;
	mp.clear();
	memset(dp,0,sizeof(dp));
	memset(side,0,sizeof(side));
	memset(head,-1,sizeof(head));
}
void add(int x,int y){
	cnt++;
	side[cnt].y=y;
	side[cnt].next=head[x];
	head[x]=cnt;
}
void tree_dp(int rt){
	if(head[rt]==-1) return;
	int dp0=0,dp1=0;
	for(int i=head[rt];i!=-1;i=side[i].next){
		int to=side[i].y;
		tree_dp(to);
		dp1+=dp[to][0];
		dp0+=max(dp[to][0],dp[to][1]);
	}
	dp[rt][0]=dp0;
	dp[rt][1]=dp1+1;
}
int main(){
	while(scanf("%d",&n)!=EOF){
		if(n==0) break;
		init();
		string boss,s,f;//这里尝试过用字符串而不是string 但是WA了。。。按题解来了就。。。
		cin>>boss;
		mp[boss]=++num;
		for(int i=2;i<=n;i++){
			cin>>s>>f;
			if(mp.find(s)==mp.end()) mp[s]=++num;//记住用法
			if(mp.find(f)==mp.end()) mp[f]=++num;
			add(mp[f],mp[s]);
		}
		for(int i=1;i<=n;i++){
			if(head[i]==-1){//若是叶节点 
				dp[i][0]=0;
				dp[i][1]=1; 
			}
		}
		tree_dp(1);//搜一次 这里不用找root了。。。 
		int maxx=max(dp[1][0],dp[1][1]);
		printf("%d ",maxx);
		int flag=0;//0无重复
		for(int i=1;i<=n;i++){
			if(dp[i][0]>=dp[i][1]){//不论i的父节点选不选 都会选i 
				for(int j=head[i];j!=-1;j=side[j].next){//又写成i了!!! 
					int to=side[j].y;//写成i了zz。。。
					if(dp[to][0]==dp[to][1]){
						flag=1;break;//有重复的
					}
				}
			}
			if(flag) break;
		}
		if(dp[1][0]==dp[1][1]) flag=1; 
		if(flag) printf("No\n");
		else printf("Yes\n");
	}
	return 0;
}

- Codeforces1084D

思路及核心思想转载自:https://blog.csdn.net/mmk27_word/article/details/85001714
题意:给你一棵树,每个点都有权值,每条边也有权值,问你求出一条路,使得:路中所有点的权值减去边的权值获得最大。
思路:树状dp,dp[i] 保存从i 节点开始走向子节点得到的最大价值
核心代码:这个核心代码tql!!!膜!

	dfs(v,u);
	ans=max(ans,dp[u]+dp[v]-val);
	dp[u]=max(dp[u],dp[v]+a[u]-val);

现在来解释一下这段核心代码什么意思:
首先v是u的子节点,搜索了v之后,就知道了v的信息(往叶节点方向走(注意只是叶节点方向并不一定走到叶节点)得到的最大的那条路dp[v],以及ans:由v的子树们组成的最长的路)。
这样就可以用dp[v]来更新ans(此时ans为由u的子树们组成的最长的路 ans=max(ans,dp[u]+dp[v]-val);)与dp[u](即u往叶节点走的最大路dp[u]=max(dp[u],dp[v]+a[u]-val);)。
这种非常巧妙的代码是非常棒的!再次膜大佬。
下面是AC代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 300100
using namespace std;
ll n,cnt,ans;//cnt总边数(双向 
//color记录结点颜色   num1 num2 记录以该节点为根节点
ll a[maxn],head[maxn];
ll dp[maxn];
struct Side{
	ll y,val,next;
}side[maxn<<1];
void init(){
	cnt=0;
	cl(a,0);
	cl(head,-1);
}
void add(ll x,ll y,ll val){
	cnt++;
	ans=0;
	side[cnt].y=y;
	side[cnt].val=val;
	side[cnt].next=head[x];
	head[x]=cnt;
}
void dfs(ll u,ll pre){
	dp[u]=a[u];
	ans=max(ans,dp[u]);
	for(ll i=head[u];i!=-1;i=side[i].next){
		ll v=side[i].y,val=side[i].val;
		if(v==pre) continue;
		dfs(v,u);//这里搜完所以dp[v]最大值已经求出来了
		ans=max(ans,dp[u]+dp[v]-val);//核心代码
		dp[u]=max(dp[u],dp[v]+a[u]-val);
	}
}
int main(){
	cin>>n;
	init();
	for(ll i=1;i<=n;i++){
		scanf("%lld",&a[i]);
	}
	ll u,v,val;
	for(ll i=1;i<n;i++){
		scanf("%lld%lld%lld",&u,&v,&val);
		add(u,v,val);
		add(v,u,val);
	}
	dfs(1,-1);//搜第一个结点 默认第一个结点是根树 
	
	printf("%lld",ans);
	return 0;
}

- CodeForces 1118F1

题意:有一棵树,上面各个节点是红色(标号1)或者蓝色(标号2)或者无色(标号0)。若去掉某一条边,得到的两个子树都不同时拥有红、蓝两种颜色,那这条边就叫“好边”。问你一共有多少条“好边”。
思路:把任意一个节点当做根,深搜遍历所有的子树,统计所有子树红蓝各多少个点。然后深搜结束。
下面是判断“好边”的核心代码:
num1、num2代表各个节点对应子树红色蓝色节点数量。

int ans=0;
	for(int i=2;i<=n;i++){//搜每个结点  每个结点都代表这个子树 
		if(num1[i]&&num2[i]) continue;//若当前这个子树 红蓝都有就不行
		int x=num1[1]-num1[i];
		int y=num2[1]-num2[i];
		if(x==0||y==0) ans++;
	}

有人可能问,我们不是找符合要求的边吗?为什么要遍历点呢?
试想,若有n个点,则边数一定是n-1,若除了根节点,那正好每个节点对应一条边(树状图上是它上面那条边),所以遍历节点就是遍历边啦!
所以,我们可以得到,若某个节点对应的子树红蓝色都有,那这个节点对应的上面那条边就不行咯,不然生成的下面这个子树是两种颜色都有的。另外,若根节点对应子树(就是最大的树)的某种颜色数量和你遍历的当前子树相等(当然是在当前子树只有一种颜色的前提下),那说明当前子树涵盖了这种颜色的所有节点,所以当前子树的根节点对应的边可以,ans++。

下面是AC代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 300100
using namespace std;
int n,cnt;//cnt总边数(双向
//color记录结点颜色   num1 num2 记录以该节点为根节点
int color[maxn],head[maxn],num1[maxn],num2[maxn];
struct Side{
	int y,next;
}side[maxn<<1];
void init(){
	cnt=0;
	cl(color,0);
	cl(head,-1);
	cl(num1,0);
	cl(num2,0);
}
void add(int x,int y){
	cnt++;
	side[cnt].y=y;
	side[cnt].next=head[x];
	head[x]=cnt; 
}
void dfs(int u,int pre){
	if(color[u]==1) num1[u]++;
	if(color[u]==2) num2[u]++;
	for(int i=head[u];i!=-1;i=side[i].next){
		int v=side[i].y;
		if(v==pre) continue;
		dfs(v,u);
		num1[u]+=num1[v];
		num2[u]+=num2[v];
	}
}
int main(){
	cin>>n;
	init();
	for(int i=1;i<=n;i++){
		scanf("%d",&color[i]);
	}
	int u,v;
	for(int i=1;i<n;i++){
		scanf("%d%d",&u,&v);
		add(u,v);
		add(v,u);
	}
	dfs(1,-1);//搜第一个结点 默认第一个结点是根树
	int ans=0;
	for(int i=2;i<=n;i++){//搜每个结点  每个结点都代表这个子树 
		if(num1[i]&&num2[i]) continue;//若当前这个子树 红蓝都有就不行
		int x=num1[1]-num1[i];
		int y=num2[1]-num2[i];
		if(x==0||y==0) ans++;
	}
	printf("%d",ans);
	return 0;
}

- POJ1635

题意:树的重心模板。
思路:树形dp,这里就直接贴一下用了大神核心代码的代码。具体思路在注释里面。
注意这里我们需要维护每个节点的两个值:dpnum和dpmax。前者指的是该节点所对应的子树的所有节点的数量,后者指的是若该点被删除,剩下的最大子树节点的数量。这很重要!
首先,深搜遍历当前点u的所有终点的v时候,每搜完一个v就代表已经知道了v的dpnum和dpmax。然后累加dpnum[v]求当前节点的dpnum。然后每搜一个v就判断一下dpmax[u]能否被dpnum[v]更新。然后搜完这个点的所有边对应的终点,再用dpnum[u]来看看父亲那边的子树能否把自己的max更新,(n-dpnum[u])就是父亲那边的子树大小,可以用来更新dpmax[u]。

AC代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define inf 0x3f3f3f3f
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 20004
using namespace std;
int n,cnt,head[maxn],dpnum[maxn],dpmax[maxn];//dpnum表示该点子树结点之和  dpmax表示该点若被删除后 剩下的最大子树 
struct Side{
	int y,next;
}side[maxn<<1];
void init(){
	cnt=0;
	for(int i=1;i<=n;i++){
		dpnum[i]=1;
		dpmax[i]=-1;
		head[i]=-1;
	}
}
void add(int x,int y){
	cnt++;
	side[cnt].y=y;
	side[cnt].next=head[x];
	head[x]=cnt; 
}
int dfs(int u,int pre){ 
	for(int i=head[u];i!=-1;i=side[i].next){
		int v=side[i].y;
		if(v==pre) continue;
		dpnum[u]+=dfs(v,u);//这一步是为了求一下节点u对应的子树节点数量
		dpmax[u]=max(dpmax[u],dpnum[v]);//看看u的子树最大支能不能被新探索出的子树更新
	}
	dpmax[u]=max(n-dpnum[u],dpmax[u]);//再看看u的父结点部分会不会被更新(意思是若dpnum[u]小,可能把dpmax更新(比如叶结点))
	return dpnum[u];//注意 返回的是结点数量 而不是子树最大值!!!
}
int main(){
	int T;
	cin>>T;
	while(T--){
		cin>>n;
		init();
		for(int i=1;i<n;i++){
			int u,v;
			scanf("%d%d",&u,&v);
			add(u,v);
			add(v,u);
		}
		dfs(1,-1);
		int minn=inf,i0;
		for(int i=1;i<=n;i++){
			if(minn>dpmax[i]){
				minn=dpmax[i];
				i0=i;
			}
		}
		printf("%d %d\n",i0,minn);
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值