软件学院天梯赛参赛队员第一次训练 L2-1 红豆生南国 (25 分)(完全二叉树,树的遍历,重建二叉树,DFS)

题目:

有诗云:

    相思 (王维  唐)

红豆生南国, 春来发几枝。

愿君多采撷, 此物最相思。

那么,我们来采红豆吧!

假设红豆树是这个样子的:

二叉树.png

这种红豆树的特点是:

  • 每个结点都有一个正整数编号,标在结点内部。结点的编号各不相同。
  • 最上方一层结点是 红豆(图中红圈所示的5个结点),这一层被称之为红豆层。
  • 树的根结点、左子结点、右子结点、左子树、右子树等的定义与“数据结构”中的“二叉树”相同,但它毕竟是“自然界中的树”,树根在最下方,如图中的结点5
  • 图中这棵红豆树是“完全二叉红豆树”,类似“数据结构”中的“完全二叉树”。(“完全二叉树”的定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是完美二叉树。对于一个有N个结点的二叉树,若其结点对应于相同深度完美二叉树的层序遍历的前 N 个结点,这样的树就是完全二叉树) 从图上看,就是:要么每一层(包括红豆层)的结点数达到最大值,要么只在红豆层的最右边缺少一些结点。

对于红豆树,我们定义两种遍历顺序:

  1. 正序遍历:先访问树根结点,再正序遍历其左子树,最后正序遍历其右子树
  2. 逆序遍历:先逆序遍历其右子树,再逆序遍历其左子树,最后访问树根结点

对于给定的一棵完全二叉红豆树以及一些要采撷的结点,计算每次采撷能采到的红豆数量。

注意:我们采的点,可能是红豆,也可能不是红豆。采撷一个结点的意思是,把这个结点及这个结点的子树的全部结点从树中采下来。

例如:若采结点7,这是红豆结点,我们将获得1颗红豆;若采结点11,这不是红豆结点(而是一个枝结点!),我们将获得红豆树的一枝,包含2个红豆结点(8和2)。

输入格式:

输入有四行。

第一行是一个不超过60的正整数N,表示完全二叉红豆树中的结点数量。

第二行是N个不超过1000的结点编号序列,以空格间隔,表示的是这棵树的逆序遍历序列。

第三行是一个不超过N的正整数K,表示进行K次采撷。

第四行是K个正整数,依次表示每次要采的结点编号。

输出格式:

输出包含K+1行,

前K行,对于输入的每个采撷的点,在一行输出相应获得的红豆数量。如果这个点已经被采掉了,则输出Zao Jiu Cai Diao Le!。如果这个点在原树中根本不存在,则输出Kan Qing Chu Le?

最后一行,输出采撷结束之后,这棵红豆树的正序遍历序列,用空格分隔,最后一个结点之后没有空格。如果采撷结束之后树已空,则输出Kong Le!

输入样例1:

对于题目中给出的图,对应的输入是:

12
10 4 3 12 6 7 1 2 8 11 9 5
4
15 12 11 2

输出样例1:

Kan Qing Chu Le?
1
2
Zao Jiu Cai Diao Le!
5 9 1 7 6

输入样例2:

对于题目中给出的图,对应的输入是:

12
10 4 3 12 6 7 1 2 8 11 9 5
1
5

输出样例2:

5
Kong Le!

题意: 

简单来说,就是给定一颗完全二叉树,最后一层的点是果子,然后有m次询问,给定某一个节点编号,需要把这个节点和它对应的子树都砍掉,并输出得到了多少个果子,如果该编号不存在或者已经砍掉了则特判输出即可。

思路:

首先明确我们要解决的问题

  1. 如何根据完全二叉树的"逆序遍历"(可以转化成后序遍历,左右根的顺序遍历),重建这颗二叉树
  2. 如何找到并标记所有的果子
  3. 如何判定给定节点编号是否在树中
  4. 如何判定给定节点编号是否已经被砍掉
  5. 如何模拟摘果子的过程
  6. 如何前序遍历这棵树并输出

首先我们要搞懂,为什么仅根据一次遍历结果就能够重建二叉树,因为这是一颗完全二叉树,它的空节点位置是固定的,都集中在最后一层和倒数第二层,而普通的二叉树却做不到这一点。还有我们上次做的,通过“#”标识空节点,也能够做到一次遍历重建二叉树。一般来说,完全二叉树用一个数组存就行了,和堆的存储一样。然后递归建树即可,注意这里和后序遍历区别在,是先遍历右子树,再是左子树最后是根节点,重建代码如下:

void build(int u)
{
	if(u <= n)                   //下标不能越界
	{
		build(2 * u + 1);        //先遍历右子树
		build(2 * u);            //然后是左子树
		cin>>tree[u];            //tree数组用来存树
		mp[tree[u]] = u;         //mp用来标记某一节点是否在树中
	}
}

其次如何标记果子,果子都在最后一层,果子之上每一层都是满的,从上到小每一层满的个数依次是:1,2,4,8……,计算每一层上面所有的节点个数(包含本层)依次是:1,3,7,15……这样我们就可以发现规律,我们按照满二叉树的节点个数进行计算,如果从第一层到该层的节点个数小于满二叉树所应有的节点个数,那么就可以判断该层就是最后一层,注意特判一下原本就是满二叉树的情况,代码如下:

int bits[] = {0 , 1 , 3 , 7 , 15 , 31 , 63};        //节点个数不超过60,所以开到63即可

int x = 0;
while(bits[x] < n)                                  //统计最后一层满足满二叉树节点个数的层数
	x++;                                            //x层是满足条件的下一层

for(int i = bits[x - 1] + 1 ; i <= n ; i++)	        //从x-1层到树的最后一个节点,都是果子,用bool数组进行标记
	isFruit[i] = true;

然后再说如何判断给定节点编号是否在树中,其实在重建树的时候就提到了,直接在建树的时候就开数组标记即可。

再说如何判断被砍掉,还记得我们重建的时候用tree数组去存树吗,由于每个节点编号都是正整数,那么我们在砍树的过程中,直接把要砍掉的节点tree的值置为零即可。

然后是重头戏,如何模拟砍树?首先明确砍树的过程,从一个节点出发,将自己和自己的子树全部砍掉,这里我们可以递归遍历去写,根据上面说的,砍树时,每走到一个点,就把这个节点对于的tree的值置为空。并且由于我们之前标记了果子,所以每走到一个果子节点,结果就加一,再递归地计算左子树的果子个数和右子树的果子个数即可,代码如下:

int pick(int u)
{
	int cnt = 0;                        //初始化果子个数
	if(u <= n && tree[u])               //下标没有越界并且该节点没被砍掉
	{
		if(isFruit[u])                  //如果当前节点是果子,则个数加一
			cnt++;    
		tree[u] = 0;                    //将该节点状态置为砍掉了
		cnt += pick(2 * u);             //递归计算左子树果子个数
		cnt += pick(2 * u + 1);	        //递归计算左子树果子个数
	}
	return cnt;                         //返回本次砍树得到的果子个数
}

最后前序遍历输出就比较简单了,从根节点出发,先遍历左子树,再遍历右子树,最后是根节点,注意,这里要跳过被砍掉了的节点,代码如下:

void preTravel(int u)
{
	if(u <= n && tree[u])                                    //下标不能越界并且没有被砍掉
	{
		ans.push_back(tree[u]);                              //将当前节点加入结果数组
		preTravel(2 * u);                                    //递归遍历左子树
		preTravel(2 * u + 1);                                //递归遍历右子树
	}
}
        
preTravel(1);                                               //从根节点出发开始前序遍历
for(int i = 0 ; i < ans.size() ; i++)
	cout<<ans[i]<<(i == ans.size() - 1 ? "\n" : " ");       //避免行末空格输出

代码:

//L2-1 红豆生南国 (25 分)
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;

int n , k , x;
int tree[N] , mp[N];
bool isFruit[N];
vector<int> ans;
int bits[] = {0 , 1 , 3 , 7 , 15 , 31 , 63};

void build(int u)            //重建二叉树
{
	if(u <= n)
	{
		build(2 * u + 1);
		build(2 * u);
		cin>>tree[u];
		mp[tree[u]] = u;
	}
}

int pick(int u)               //摘果子
{
	int cnt = 0;
	if(u <= n && tree[u])
	{
		if(isFruit[u])
			cnt++;
		tree[u] = 0;
		cnt += pick(2 * u);
		cnt += pick(2 * u + 1);	
	}
	return cnt;
}

void preTravel(int u)        //前序遍历
{
	if(u <= n && tree[u])
	{
		ans.push_back(tree[u]);
		preTravel(2 * u);
		preTravel(2 * u + 1);
	}
}

int main()
{
	cin>>n;
	build(1);
	
	int x = 0;
	while(bits[x] < n)
		x++;
	
	for(int i = bits[x - 1] + 1 ; i <= n ; i++)	
		isFruit[i] = true;
	
	cin>>k;
	while(k--)
	{
		cin>>x;
		if(!mp[x])                               //节点不在树中
			cout<<"Kan Qing Chu Le?"<<endl;
		else if(!tree[mp[x]])                    //该节点被砍掉了
			cout<<"Zao Jiu Cai Diao Le!"<<endl;
		else                                     //模拟砍树
			cout<<pick(mp[x])<<endl;
	}
	
	if(!tree[1])                                 //树根都被砍掉了
		cout<<"Kong Le!"<<endl;
	else
	{
		preTravel(1);
		for(int i = 0 ; i < ans.size() ; i++)
			cout<<ans[i]<<(i == ans.size() - 1 ? "\n" : " ");
	}
	return 0; 
} 

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值