树形dp解决:《节点x选了,其相邻节点选不选问题》

35 篇文章 1 订阅

前言:
这种 节点x是否挑选对其相邻节点有影响的问题 在树中也很常见,下面是几个经典例题,让我们一起来学习掌握它吧!


例一、没有上司的舞会

题意:

原题题意:
每个人都有上司,他们之间的关系构成了一棵树。父节点是子节点的直接上司。每个节点有权值 w i w_i wi
先要挑出若干个人参加舞会,每个人都不愿和直接上司一起。
问,如何选择能够使得总权值最大?
简化题意:
一棵树,每个节点有权值 w i w_i wi
要求每条边上最多选择一个节点,总权值最大为多少?


分析:

每次状态分析只在以 x 为根节点的子树中进行。

定义状态:
  f[i, 0]:在以 i 为根的子树中,选择根节点 i 时的最大权值。
  f[i, 1]:在以 i 为根的子树中,不选根节点 i 时的最大权值。

状态转移:

  • 如果根节点 x 选的话,因为每条边上最多选择一个节点,那么其子节点都不能选,那么当前状态 f[x, 1] 就要从子节点不选的状态 f[tx, 0] 来转移:f[x, 1] += f[tx, 0];
  • 如果根节点 x 不选,那么其子节点都可选可不选,那么当前状态 f[x, 0] 可以从子节点的两种状态来转移。因为要使总权值最大,所以用每个子节点较大的一种状态 max(f[tx, 0], f[tx, 1]) 转移:f[x, 0] += max(f[tx, 0], f[tx, 1]);

初始化:
如果根节点选了,那么其初始权值为根节点的权值:f[x, 1] = w[x]


Code:
#include<bits/stdc++.h>
using namespace std;

const int N = 200010, mod = 1e9+7;
int T, n, m, k;
int a[N];
int f[N][2], w[N];
vector<int> e[N];
bool pre[N];

void dfs(int x)
{
	f[x][1] = w[x];
	
	for(auto tx:e[x])
	{
		dfs(tx); //子节点的状态没确定,先递归子节点,回溯的时候更新父节点
		
		f[x][0] += max(f[tx][0], f[tx][1]);
		f[x][1] += f[tx][0];
	}
}

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	
	for(int i=1;i<n;i++)
	{
		int x, y;cin>>x>>y;
		e[y].pb(x);
		pre[x] = 1;
	}
	
	int root;
	for(int i=1;i<=n;i++) if(!pre[i]) root=i;
	
	dfs(root);
	
	cout << max(f[root][0], f[root][1]);
	
	return 0;
}


例二、战略游戏

题意:

原题题意:
一座城市,其中所有边构成了一棵树。
每个节点的士兵可以观察到所有与该点相连的边。
为了所有的边都能被观察到,最少在多少个节点上放置士兵?
简化题意:
给定 n 个点的树,每个点有权值 1。
要求每条边上至少选择一个节点,总权值最小为多少?


分析:

分析以 x 为根的子树,同样每个点选和不选两种状态:

  • 如果根节点 x 不选,因为每条边上至少选择一个节点,所以其子节点都要选,从子节点选的状态更新。
  • 如果根节点 x 选,那么其子节点可选可不选,为了使得总权值最小,用权值较小的状态来更新。

初始化同样,选了x,那么权值初始加上x的权值。

Code:
#include<bits/stdc++.h>
using namespace std;

const int N = 2010, mod = 1e9+7;
int T, n, m, k;
int a[N];
bool pre[N];
vector<int> e[N];
int f[N][2];

void dfs(int x)
{
	f[x][1] = 1;
	f[x][0] = 0;
	
	for(auto tx:e[x])
	{
		dfs(tx);
		
		f[x][0] += f[tx][1];
		f[x][1] += min(f[tx][0], f[tx][1]);
	}
}

signed main(){
//	Ios;
	while(cin>>n)
	{
		for(int i=0;i<n;i++) pre[i]=0, e[i].clear();
		
		for(int i=1;i<=n;i++)
		{
			int x=0, cnt=0, y;
			scanf("%d:(%d)", &x, &cnt);
			for(int j=1;j<=cnt;j++){
				cin>>y;
				e[x].pb(y), pre[y]=1;
			}
		}
		
		int root;
		for(int i=0;i<n;i++) if(!pre[i]) root=i;
		
		dfs(root);
		
		cout << min(f[root][0], f[root][1]) << endl;
	}
	
	return 0;
}

其实,仔细想想,问什么每次考虑都只需要考虑 x 节点作为根节点的时候由其子树来更新呢?为什么不用父节点来对其更新呢?

因为这是一棵树。

回想之前做过的依据拓扑序进行dp,为什么要依据拓扑序呢?就是为了不出现环,保证了dp的无后效性。
而此时,树的属性就确定了没有环,根节点确定之后,就可以从叶子节点向根节点,一层一层更新。
所以对于每个点,其状态都是由子节点,子树来更新的,而不是父节点来更新。
所以,对于每个节点,就完全不用考虑父节点对其更新。
只需要将其作为根节点,用其子树将其更新。


例三、皇宫看守

题意:

给定一棵n个节点的树。
每个节点可以放置士兵,放置的士兵可以观察到 本节点 和 与该节点相邻的节点。
每个节点放置士兵都有花费 w i w_i wi
问,能够将所有点都观察到的最少花费为多少?

分析:

这一题和上一题 战略游戏 很像。
战略游戏中,题目要求的是 所有边 都要被观察到,而这一题要求 所有点 都要被观察到。

要求所有边都要被观察到,那么:

  • 节点 x 选了,那么其子节点就可选可不选;
  • 节点 x 不选,那么为了使得其所在边被观察到,所有子节点都要选。

但是要求 所有点 都要被观察到,那么情况就稍复杂了:

  • 节点 x 选了,就可以观察到所有子节点,那么子节点可选可不选;
  • 节点 x 不选,在这种情况下,就要根据其父节点选不选再分成两种情况
      1. 父节点选,那么节点 x 就能由父节点观察到,那么其子节点就可选可不选了;
      2. 父节点不选,那么为了节点 x 能够被观察到,其子节点就至少要选一个。

所以,我们就需要额外记录对于每个节点,在不选的情况下其父节点选不选

于是,状态就有三种:
f[i, 0]:在以 i 为根的子树中,在 根节点 i 不拿,其父节点拿了 的情况下,所有节点都被观察到所需的最小花费;
f[i, 1]:在以 i 为根的子树中,在 根节点 i 不拿,其父节点也不拿 的情况下,所有节点都被观察到所需的最小花费;
f[i, 2]:在以 i 为根的子树中,在 根节点 i 拿了 的情况下,所有节点都被观察到所需的最小花费。

状态转移:
1、根节点 i 拿了,那么其子节点 tx 可拿可不拿,可以由子节点的两种状态来更新:
f[i, 2] = ∑ min(f[tx, 0], f[tx, 2])
2、根节点 i 不拿,其父节点拿了,那么其子节点 tx 可拿可不拿,但是因为子节点的父节点,即根节点 i 没拿,所以不能从子节点的 f[tx, 0] 更新,只能从另外两种状态:
f[i, 0] = ∑ min(f[tx, 1], f[tx, 2]).
3、根节点 i 不拿,其父节点也不拿,那么其子节点就至少拿一个。遍历所有要拿的子节点,其余子节点可拿可不拿。因为其父节点没拿,所以也只能从另外两种状态转移。

	for(auto tx:e[x]) //枚举要选择的子节点 
	{
		int sum=0;
		if(tx == fa) continue;
		
		for(int k:e[x]) //其他子节点可选可不选 
		{
			if(k==tx || k==fa) continue;
			sum += min(f[k][1], f[k][2]);
		}
		f[x][1] = min(f[x][1], f[tx][2] + sum);
	}
完整Code:
#include<bits/stdc++.h>
using namespace std;

const int N = 200010, mod = 1e9+7;
int T, n, m, k;
int a[N], w[N];
int f[N][3];
vector<int> e[N];

void dfs(int x, int fa)
{
	f[x][0] =0; f[x][1] = 1e9;
	f[x][2] = w[x];
	
	for(auto tx:e[x])
	{
		if(tx == fa) continue;
		
		dfs(tx, x);
		f[x][2] += min(f[tx][0], f[tx][2]);
		f[x][0] += min(f[tx][1], f[tx][2]);
	}
	
	for(auto tx:e[x]) //枚举选择的子节点 
	{
		int sum=0;
		if(tx == fa) continue;
		
		for(int k:e[x]) //其他子节点可选可不选 
		{
			if(k==tx || k==fa) continue;
			sum += min(f[k][1], f[k][2]);
		}
		f[x][1] = min(f[x][1], f[tx][2] + sum);
	}
}

signed main(){
	Ios;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int x, y, cnt;
		cin>>x>>y>>cnt;
		w[x] = y;
		for(int j=1;j<=cnt;j++)
		{
			int t;cin>>t;
			e[x].pb(t);e[t].pb(x);
		}
	}
	
	dfs(1, 0);
	
	cout << min(f[1][2], f[1][1]);
	
	return 0;
}

注意,虽然这题中考虑了父节点,但是只是考虑父节点对根节点状态更新的影响,而不是用父节点来更新。
所以整体的方向没有错。这种考虑父节点影响的题目还是头一次见。


哪里有问题欢迎留言讨论~

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值