算法与数据结构实验题-树-message

算法与数据结构实验题 6.2 message

★实验任务
现有一个树形的消息通信系统,当节点 i 要发送消息时,它会查找它的子节
点,向还没发送过消息的子节点发送消息,如果有多个这样的子节点,则优先选
择节点编号最小的节点,而它的子节点也以同样的策略向下传送消息。当节点 i
的某个子节点完成了消息发送任务后,节点 i 继续查找剩余还没发送过消息的子
节点发送消息,直到节点 i 已经向所有的子节点发送过消息了,则节点 i 完成了
它的消息发送任务。
现有 q 次询问,每次给出一个节点编号 u 和整数 k,问从节点 u 开始发送消
息,则第 k 个收到消息的节点的编号是多少,如果不存在这样的节点,则输出
“-1”。

★数据输入
第一行包含两个整数 n 和 q,表示树中的节点数(2≤n≤2·10^5)和询问的
次数(1≤q≤2·10^5)。
接下来的一行包含 n-1 个整数 pi(1≤i≤n-1) 表示第 i + 1 个节点的父节点
的下标(1≤pi≤i)。节点 1 是根节点。
接下来 q 行,每行两个整数 u 和 k(1≤u, k≤n),u 是开始传播消息的节点
编号,k 表示询问第 k 个收到消息的节点。

★数据输出
输出 q 行,每行表示第 q 次询问的节点编号,如果不存在这样的节点,则
输出“-1”。

输入示例
9 6

1 1 1 3 5 3 5 7

3 1

1 5

3 4

7 3

1 8

1 9

输出示例
3

6

8

-1

9

4


分析:这题其实就是考的前序遍历吧。因为数据量很大,“询问”的次数也特别多,每次都遍历很耗时间。稍微想象就知道其实只要遍历一次,记录前序列表,再在递归里记录每个节点的子节点数,把这两点结合后,后面的每次询问都可以在常数时间完成。

接下来贴上菜鸡代码(递归实现):

#include <iostream>
#define maxsize 100001
using namespace std;
typedef struct node* link;
typedef struct node {
	int number;
	link next;
}node;

int count1 = 0;      //这个count1用来帮助遍历树时记录前序遍历序号
unsigned short int list[maxsize];   //存前序链表的数组(数组大小要与最大节点数100001相同,元素能表示的最大大小要足够表示最大节点的序号(也是100001))
unsigned short int count2[maxsize];      //这个用来记录每个节点的子节点的数量


void visit(int root) {
	list[++count1] = root;
}
class tree {
private:
	link* son;                  //存有第i个节点的子节点链表的数组
	int length;                //链表指针数组长度
public:
	tree() :son(new link[maxsize]()),length(maxsize){}        //刚创建时只有一个根节点
	void add(int number, int father) {          //这个函数用来传入某个节点的下标和其父亲,把该节点添加到树中
		if (son[father] == NULL) {              //该节点的子节点链表还未创建时,马上创建一个
			link p = new node();
			p->number = number;
			p->next = NULL;
			son[father] = p;
		}
		else {
			link p;
			for (p = son[father]; p->next != NULL; p = p->next);     //找到插入的位置
			link tmp = new node();
			tmp->number = number;
			tmp->next = NULL;
			
			p->next = tmp;
		}
	}

	int pre_order(int root) {           //返回值的含义就定为:根节点下标为root的树含有的节点总数
		int sum = 0;
		if (son[root] != NULL) {          //当前节点还有子节点就继续递归遍历
			visit(root);
			link p = son[root];
			while (p != NULL) {
				sum+=pre_order(p->number);
				p = p->next;
			}
			count2[root] = sum;        //注意:count2数组存的sum是树的子节点总数,返回的是树的节点总数,差了一个根节点
			return sum + 1;
		}
		else {
			visit(root);
			count2[root] = 0;     //叶结点没有子节点
			return 1;
		}
	}
};

int main() {
	tree a;
	int n,q,i;

	cin >> n >> q;
	for (i = 2; i <= n; i++) {
		int tmp;
		cin >> tmp;
		a.add(i, tmp);             //把节点添加进树中
	}

	a.pre_order(1);

	for (i = 0; i < q; i++) {             //接下来就要判断第k个接收到某处发出的通知的节点了
		int u, k;                         
		cin >> u >> k;

		if (k-1 > count2[u])   cout << -1<<endl;
		else                 cout << list[u + k - 1]<<endl;
	}

	system("pause");
}

有种说法是递归的程序开销比较大。(????)
(18.12.6:回来补充了:有优秀代码是直接用递归过的。。据说自己模拟的
栈开销和系统栈也差不多?)
有空再来一种用栈实现的模拟递归版本,
(18.11来咯:)

#include <iostream>
#include <stack>
#define maxsize 100001
using namespace std;
typedef struct node* link;
typedef struct node {
	int number;
	link next;
}node;

int count1 = 0;      //这个count1用来帮助遍历树时记录前序遍历序号
unsigned short int list[maxsize];   //存前序链表的数组(数组大小要与最大节点数100001相同,元素能表示的最大大小要足够表示最大节点的序号(也是100001))
unsigned short int count2[maxsize];      //这个用来记录每个节点的子节点的数量

void visit(int root) {
	list[++count1] = root;
}
class tree {
private:
	link * son;                  //存有第i个节点的子节点链表的数组
	int length;                //链表指针数组长度
public:
	tree() :son(new link[maxsize]()), length(maxsize) {}        //刚创建时只有一个根节点
	void add(int number, int father) {        //这个函数用来传入某个节点的下标和其父亲,把该节点添加到树中
		if (son[father] == NULL) {            //该节点的子节点链表还未创建时,马上创建一个
			link p = new node();
			p->number = number;
			p->next = NULL;
			son[father] = p;
		}
		else {
			link tmp = son[father];      //使用尾插法,使子节点链表存储顺序为从大到小,使后面的栈模拟递归更方便
			link p = new node();
			p->number = number;
			p->next = tmp;
			son[father] = p;
		}
	}
	void preorder(int root) {  //所谓的用栈模拟(虽然感觉跟普通递归的系统栈原理还是差了不少就是了)
		stack<int> s;
		s.push(root);
		while (!s.empty()) {
			visit(root = s.top());
			s.pop();
			if (son[root] != NULL) {
				for (link p = son[root]; p != NULL; p = p->next)
					s.push(p->number);
			}
		}
	}
	void count3(int n) {                  //通过存子节点的链表数组,从后往前确认每个节点的子节点数
		int sum;
		for (int i = n; i >= 1; i--) {
			if (son[i] == NULL)   count2[i] = 0;           //当前节点没有子节点
			else {
				link p;
				for (p = son[i],sum=0; p != NULL; p = p->next) {
					sum += 1+count2[p->number];      //  当前节点子节点数=每个子节点(这里的1)+每个子节点的子节点数
				}
				count2[i] = sum;
			}
		}
	}
};



int main() {
	tree a;
	int n, q, i;

	cin >> n >> q;
	for (i = 2; i <= n; i++) {
		int tmp;
		cin >> tmp;
		a.add(i, tmp);       //把节点添加进树中
	}

	a.preorder(1);        //前序遍历获得前序列表
	a.count3(n);          //统计每个节点的子节点数

	for (i = 0; i < q; i++) {     //接下来就要判断第k个接收到某处发出的通知的节点了啊啊啊
		int u, k;
		cin >> u >> k;

		if (k - 1 > count2[u])   cout << -1 << endl;
		else                 cout << list[u + k - 1] << endl;
	}
	system("pause");
}

收获:
1、主要是又稍微接触了一下递归,特别是从某鸭那学到的在递归中用返回值记录额外信息的方法确实好用啊,一下子让这题的第二部分变得简单起来。
2、自己画图理解模拟递归的时候好像懂了点递归和模拟递归遍历的区别(很肤浅):

系统递归是函数一调用就把这层函数的相关信息压入栈,只在最后栈顶代表的函数(树的遍历中就是指处理叶结点的那层函数)执行完所有语句,达到return条件后才出栈,轮到上层的节点。

优点:系统递归在遍历完最末端节点后往上返回,栈中存有完整的从末端到上层的信息,也许可以利用这点,使用函数的返回值处理一些有关父子节点关系的问题。(在这题中就是在遍历时可以方便的统计某个节点的子节点总数)。
缺点:因为遍历完的节点没有马上出栈,所以树的层数太多的情况下,递归栈会叠得太高,可能会爆栈。

模拟递归是栈非空时取栈顶元素,处理完就出栈。将元素放入栈的顺序要与实际想要操作的顺序相反(因为每次是从栈顶取元素)。(这题中是提前在插入节点时就在链表处使用了头插法,使节点数据在链表中的存储数据自然相反)

优点:一个节点处理完就出栈,减少很多空间的占用
缺点:节点太快出栈了,难以保存节点间的相互关系。

3、上面两个获取每个节点的子节点数的办法,其中第二个有一定的局限性:用第二种方法计算一个节点的子节点数的时候,需要知道其每个子节点的子节点数,而按下标从1到n遍历并不能做到这点。但是特别地注意到:在这题中,下标越大的节点刚好越晚被加入树中,所以只要从后往前遍历,就可以满足刚才的条件,顺利的计算出每个节点的子节点数。粗体的条件在别的题中也许不满足。。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值