前言:
这种 节点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;
}
注意,虽然这题中考虑了父节点,但是只是考虑父节点对根节点状态更新的影响,而不是用父节点来更新。
所以整体的方向没有错。这种考虑父节点影响的题目还是头一次见。
哪里有问题欢迎留言讨论~