C++蓝桥杯 算法训练之结点选择

C++ 蓝桥杯题目讲解汇总(持续更新)


VIP试题 结点选择

资源限制

时间限制:1.0s 内存限制:256.0MB

问题描述

有一棵 n 个节点的树,树上每个节点都有一个正整数权值。如果一个点被选择了,那么在树上和它相邻的点都不能被选择。求选出的点的权值和最大是多少?

输入格式

第一行包含一个整数 n 。

接下来的一行包含 n 个正整数,第 i 个正整数代表点 i 的权值。

接下来一共 n-1 行,每行描述树上的一条边。

输出格式

输出一个整数,代表选出的点的权值和的最大值。

样例输入

5
1 2 3 4 5
1 2
1 3
2 4
2 5

样例输出

12

样例说明

选择3、4、5号点,权值和为 3+4+5 = 12 。

数据规模与约定

对于20%的数据, n <= 20。

对于50%的数据, n <= 1000。

对于100%的数据, n <= 100000。

权值均为不超过1000的正整数。

解题思路

首先得弄清楚题目的意思,我一开始没看懂,后来尝试着画出树,就明白了

C++蓝桥杯 算法训练之结点选择_第1张图片
这是一个树上求解最大独立集问题,问题的关键就是在于选择当前结点和不选择当前结点的状态转移方程,这里很关键,这里大家不懂的话可以看看刘汝佳老师的算法竞赛入门经典关于树形DP的求解 ( P 280 − P 283 P_{280}-P_{283} P280P283),其次主要和本题最接近的例题是 9-13,如下:

C++蓝桥杯 算法训练之结点选择_第2张图片
C++蓝桥杯 算法训练之结点选择_第3张图片
难点:

  • 树形结构的存储,这里我也是看书本上的使用vectortree这种存储结构进行存储,但是感觉可能耗内存比较大,不过简单明了,vector类似于队列,python中的list结构

  • 要注意这里的树不是那种常见的二叉树,结点和父节点都很明确那种,这里的树可以看成一个无向图

  • 注意自顶而下的DFS分解过程,以及自底而上的回溯求解过程

  • 注意转移方程,不选择当前结点的时候,不一定要选择其儿子结点,因为其儿子结点选不选择是由孙子结点的值决定的,但是不选择当前结点,一定不选择其儿子结点,下面是两种情况下的状态转移方程:

    ​ 设u为当前节点,i是它的子结点,d(u,1)代表选择u的值,d(u,0)代表不选择u的值

    • 如果选择当前节点的话,则一定不选择其子结点

      d ( u , 1 ) = ∑ i ∈ s o n d ( u , 0 ) (1) \displaystyle d(u,1)=\sum_{i\in{son}} d(u,0) \tag{1} d(u,1)=isond(u,0)(1)

    • 如果不选择当前节点的话,则 d ( u , 0 ) d(u,0) d(u,0)的值肯定是选择儿子和不选择儿子结点两种情况下的最大值

    d ( u , 0 ) = ∑ i ∈ g r a n d s o n m a x { d ( i , 0 ) , d ( i , 1 ) } (2) d(u,0) = \displaystyle \sum_{i\in{grandson}} max \{d(i,0),d(i,1)\} \tag{2} d(u,0)=igrandsonmax{d(i,0),d(i,1)}(2)

代码-C++

基于错误的代码,改进为正确ac代码:

#include
#include
#include 
#define maxn 100010
using namespace std;

vector<int> tree[maxn]; //树的存储结构
int d[maxn][2]; //每个结点有两个属性值,一个不选 下标0,一个选 下标1 
int flag[maxn]; //代表是否被访问的flag数组 

void dfs(int u){
	/*
	输入: 结点u
	输出: 选择结点u和不选择结点u时候的最大属性值(权值) 
	*/	
	flag[u]=1; //代表已经访问过
	int k=tree[u].size();
	for(int i=0;i<k;i++){
		//如果子结点没有访问过就进行dfs其子结点 
		if(!flag[tree[u][i]]){	
			dfs(tree[u][i]); //dfs之后,从叶子往根进行更新 
			// 如果选择结点u的话,其儿子结点一定不可选...
			d[u][1] += d[tree[u][i]][0];  
			// 没有选择结点u的话,其儿子结点可选可不选,所以取这两种情况的最大值... 
			d[u][0] += max(d[tree[u][i]][1],d[tree[u][i]][0]);
		
		}
	} 
}
int main(){
	int n,a,b;
	cin>>n;
	for (int i=1;i<=n;i++)
		cin>>d[i][1];
	memset(flag,0,sizeof flag);	
	for(int i=1;i<n;i++){
		cin>>a>>b;
		tree[a].push_back(b);
		tree[b].push_back(a);
	}
	dfs(1);
	cout<<max(d[1][0],d[1][1]);	
	return 0;
}

/*
10
41 982 686 796 781 801 63 52 60 748
3 4
8 5
1 9
10 7
2 3
1 6
1 3
2 7
4 5

5
1 2 3 4 5
1 2
1 3
2 4
2 5
*/

错误的代码,错误原因:

我看完了书上的一个例题,我以为我会了,实际上漏洞百出,感觉递归和回溯真的很重要!!!

  • 第一个:没考虑是一个无向图(树),这里的树,不是我们理解的二叉树那样,所以我犯了第一个错误。
  • 第二个,当前结点不选的时候,不一定要选择其子结点,子结点可选可不选的,还得看孙子选不选的最大值
#include
#include
#define N 100010 
#define max(x,y) x>y?x:y
using namespace std;
vector<int> tree[N];
int w[N];

int dfs(int u){
//	cout<
	if (tree[u].empty()) return w[u];
	int k = tree[u].size(); //代表结点u的子结点数目
//	cout<
	int res1=w[u]; //res1代表的是选择当前结点和它的孙子结点 
	int res2=0; //res2代表的是不选择当前结点,选择它的儿子结点 
	for(int i=0;i<k;i++){
//		cout<
		int t=tree[tree[u][i]].size();
//		cout<
		for(int j=0;j<t;j++){
//			cout<
//			cout<
//			cout<
			res1+=dfs(tree[tree[u][i]][j]); //加上每一个儿子节点i的儿子结点j的值,也就是孙子结点j的值 
//			cout<
		}
	}
	
	for(int i=0;i<k;i++){
		res2+=dfs(tree[u][i]); // 加上每一个儿子结点的值即可 
	}
	
	return max(res1,res2); 
	
}

int main(){
	int n,a,b;
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>w[i];
	
	for(int i=1;i<=n-1;i++){
		cin>>a>>b;
		tree[a].push_back(b); //代表a结点的子节点为b 
	}
	int ans;
	ans=dfs(1);
	cout<<ans; 
	
	return 0;
}

总结:

看了刘汝佳的算法竞赛入门经典,从280页到283页,学习了树形DP以及树上最大独立集的求解问题

但是不得不说的是,自己对于自顶向下的深搜分解,自底而上的回溯求解运用的不熟悉,我本想用res1,res2求解,但是更新访问的数组总是出问题,回头看书本,发现可以尝试用d[N][2]这样一个数组来记录其两种情况下的属性值。
还有要注意的是需要看网课了,不然C++的语法跟不上,邻接表邻接矩阵、结构体、指针也都要去学习,光掌握会用vector存储图/树结构效率还是有待提高的,淦,加油!

你可能感兴趣的:(蓝桥杯)