05-树8 File Transfer (25 分)

题目介绍

We have a network of computers and a list of bi-directional connections. Each of these connections allows a file transfer from one computer to another. Is it possible to send a file from any computer on the network to any other?

Input Specification:

Each input file contains one test case. For each test case, the first line contains N (2≤N≤104), the total number of computers in a network. Each computer in the network is then represented by a positive integer between 1 and N. Then in the following lines, the input is given in the format:

I c1 c2

where I stands for inputting a connection between c1 and c2; or

C c1 c2

where C stands for checking if it is possible to transfer files between c1 and c2; or

S

where S stands for stopping this case.

Output Specification:

For each C case, print in one line the word “yes” or “no” if it is possible or impossible to transfer files between c1 and c2, respectively. At the end of each case, print in one line “The network is connected.” if there is a path between any pair of computers; or “There are k components.” where k is the number of connected components in this network.

题目大意:

假定在一个网络里有n台电脑.刚开始它们并没有连接在一起.通过I指令连接网络中的两台电脑,C指令检查某两台电脑是否连接.遇到S指令,则检查通过一系列指令操作后网络中的电脑是否全都连接在一起了.如果是,输出"the network is connected",不是输出"there are k components",k表示该组计算机中有几个网络(几个连通集)

思路:

使用并查集来处理这个程序.首先,定义一个结构体和结构体数组.

typedef struct {//此结构体表示数组中的每个元素的类型描述
	elementType data;//元素值
	int parent;//父节点下标
}setType;
setType s[maxSize];//集合元素(描述为一个结构体)数组,定义为全局变量

对于并查集这种数据结构.主要定义了初始化,并和查三种操作.首先进行初始化的实现

void initial(int n) {
	for (int i = 0;i < n;i++) {
		s[i].data = i + 1;//记录电脑编号
		s[i].parent = -1;//如果是负数,表示是根结点,其绝对值为此树的元素
  //个数.如果不是,则表示父节点在数组中的下标
	}
}

并操作的实现,采用按规模归并,也可以按高度归并,两者合称为按秩归并

void setUnion(elementType x1, elementType x2) {
	int root1, root2;
	root1 = find(x1);//找到x1的根(所在的集合)
	root2 = find(x2);//找到x2的根
	if (root1 != root2) {//不在一个集合
 //为了避免联合后树向一边歪,首先比较两个集合元素个数,将少的挂在多的下面,同时更改元素个数
		if (s[root1].parent < s[root2].parent) {
			s[root1].parent += s[root2].parent;
			s[root2].parent = root1;
		}
		else {
			s[root2].parent += s[root1].parent;
			s[root1].parent = root2;
		}
	}
}

查操作的实现:

int find(elementType x) {
	int i;
	for (i = 0;i < maxSize && s[i].data != x;i++);//找到元素在数组中的位置
	if (i >= maxSize)return -1;//没找到
	for (;s[i].parent >= 0;i = s[i].parent);//找此元素的根在数组中的位置,通过i体现
	return i;
}

至此,我们已经实现了并查集的基本操作,可以用于具体的题目了.首先,明确主函数的流程.我们需要根据读入的字母判断将要进行哪一项操作.这个过程可以使用switch…case控制.

int main() {
	int num;
	char sign;//操作符
	scanf("%d\n", &num);//元素个数
	initial(num);
	for (;;) {
		sign = getchar();
		switch (sign) {
		case 'C':if (check())printf("yes\n");else printf("no\n");break;
		case 'I':input();break;
		case 'S':if (checkNetwork(num) == 1)printf("The network is connected.");else printf("There are %d components.\n", checkNetwork(num));break;
		default:break;
		}if (sign == 'S')break;
	}
 }

在主函数里面,根据不同的操作符,分别调用了check,input,checkNetwork三个函数,用于处理三种情况.对于操作符’C’,调用check函数.此函数定义为:

int check() {
	int c1, c2;
	scanf("%d%d\n", &c1, &c2);
     //读入的电脑编号在集合里面的根结点下标相同,说明在同一集合,可以连通
	if (find(c1) == find(c2))return 1;
	return 0;
}

对于操作符’I’,调用input函数,此函数定义为:

void input() {
	int c1, c2;
	scanf("%d%d\n", &c1, &c2);
	setUnion(c1, c2);//将这两台电脑放入同一个集合表示连通
}

对于操作符’S’,表示结束程序并检查连通情况,函数checkNetwork定义为:

int checkNetwork(int n) {
	int cnt = 0;//集合中有几个负数就有几个连通分量
	for (int i = 0;i < n;i++) {
		if (s[i].parent < 0)cnt++;
	}
	return cnt;
}

至此,代码编写完毕,提交代码测试结果.
在这里插入图片描述
第五个测试点超时.提示信息表示没有按大小合并集合.但是提交的代码是按照大小合并的.所以问题不出在这.问题出在find函数.find函数通过一个for循环来遍历整个数组找到元素所在下标,如果我们要找n个元素的下标,那么时间复杂度就是n^2,这个数量级是很恐怖的,说明代码还有优化空间.我们发现,集合元素的存储是可以简化的,不一定非要定义一个结构体来存储.因为任何有限集合都可以一一映射成自然数(这是显然的).我们可以去掉元素域,将结构体数组简化为int数组,其中下标表示集合元素的自然数映射,对应下标的值表示该集合元素在树中的父节点下标.本题中的电脑代码1-N可以映射成0~N-1.所以,需要定义一个映射函数map

int map(elementType e) {
	return e - 1;
}

map 函数体内的映射规则可以根据输入来调整,本题是从1-N映射成0~ N-1.如果采用a~z来对26台电脑编号(假设只有26台),那么映射规则如下:

int map(elementType e) {
	return e - 'a';
}

也可以用其他的规则编号,只需处理一下map函数,重新声明一下elementType,修改scanf的读入控制符
由于上一个版本的代码超时的原因出在find,所以重新定义find:

int find(elementType x) {
	for (;s[x] >= 0;x = s[x]) {}return x;//这里是>=
}

其他函数体内的代码基本不用修改,只需在调用find的时候将参数先通过map映射,比如check函数:

int check() {
	int c1, c2;
	scanf("%d%d\n", &c1, &c2);
	//读入的电脑编号在集合里面的根结点下标相同,说明在同一集合,可以连通
	if (find(map(c1)) == find(map(c2)))return 1;//先映射
	return 0;
}

setUnion中的find同理:

void setUnion(elementType x1, elementType x2) {
	int root1, root2;
	root1 = find(map(x1));//找到x1的根(所在的集合)
	root2 = find(map(x2));//找到x2的根
	if (root1 != root2) {//不在一个集合
//为了避免联合后树向一边歪,首先比较两个集合元素个数,将少的挂在多的下面,同时更改元素个数
		if (s[root1] < s[root2]) {
			s[root1] += s[root2];
			s[root2] = root1;
		}
		else {
			s[root2] += s[root1];
			s[root1] = root2;
		}
	}
}

还有,初始化函数直接对数组赋值:

void initial(int n) {
	for (int i = 0;i < n;i++)s[i] = -1;
}

提交结果:
在这里插入图片描述
原来提交结果:
在这里插入图片描述

和原来比快了不止一点点

优化后的代码的时间复杂度是nlogn,而原来的朴素算法的时间复杂度是n^2.因为按秩归并后,树的高度增加不会出现一边倒的情况,遇到最坏的查询情况,即每次都从树根查找它的父节点,查找时间也不过是树的高度logn,如果n个元素都是最坏的情况,时间复杂度也不过nlogn.
做了按秩归并以后,程序就快多了,那么能不能再快一点呢?可以,我们可以优化find函数,使得在调用执行find函数找父节点的同时进行路径压缩.也就是把需要查找父节点的那个结点(传入find的那个结点)在查找过程中遇到的父节点,父节点的父节点,直到根结点.都直接挂在根结点下面(它们的根都是一样的)这样,在下次查找其他元素的根的时候,就会快很多.如图:

优化后的find代码:

//路径压缩优化find
int find(elementType x) {
	if (s[x] < 0)return x;
	else {
		return s[x]=find(s[x]);
	}
}

这是一段尾递归代码,什么是尾递归?简单理解就是调用自己的时候直接通过参数传递结果,达到不压栈的目的.对于尾递归代码,编译器会自动优化,所以效率要比普通的递归高.
使用路径压缩之后效率会快多少呢?在数学上,有一个引理.证明了在做了路径压缩之后,查找的时间复杂度将降为一个常数和查找次数的乘积,也就是降到了O(N)数量级.但是由于log函数的增长速度十分缓慢,所以在数据量比较低的情况下,做不做都区别不大.也就是低数据量的情况下O(N)和O(NlongN)效率差不多.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值