一学就废的并查集它来了

题目描述

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?


输入

测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。


输出

对每个测试用例,在1行里输出最少还需要建设的道路数目。


样例输入

4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0


样例输出

1
0
2
998


提示

Huge input scanf is recommended.


算法思想

数组下标作为一个点,数组中存储的元素作为其连接的另一个点,全联通即:将所有下标对应的点归拢到一个点上(也就是除了根节点,大家对应的都不是自己啦),相当于大家都跟一个点连接的话,当然是全联通状态啦。


代码实现


寻找根节点

定义一个函数用来搜索给出结点的根节点,最终返回根节点的值(康康大家是不是来自同一地方嘛)

int find(int x){
    
    
	while(data[x]!=x){
    
    
		x=data[x];
	}
	return x;//返回根节点的值
}

当联通关系不多,树的深度不深的时候,用上一种方法无疑是很快的,但是如果联通关系错综复杂,树的深度很深,例如每层只有一个节点这种奇葩情况,遍历搜索根节点就会很费时间,这时候就要用到“路径压缩“这一方法,即将所查询节点的数组内容由”父节点“改为”祖宗节点“。

int find(int x){
    
    //寻找根节点
	int ans,tmp;
	ans = x;
	while(data[x]!=x){
    
    //x是根节点
		x=data[x];
	}
	while (ans != x) {
    
    //路径压缩
		tmp = data[ans];
		data[ans] = x;//数组内容直接储存根节点,不再存储父节点
		ans = tmp;//接下来压缩父节点
	}
	return x;//返回根节点的值
}

汇总连接情况

将所给的两个点的连接情况进行记录,根节点不一样的话说明是第一次告知两个节点联通,所以要把根节点改成一样的, 一样的话当然就是告知过了,不需要改变(大型认祖归宗活动,哦兄弟我们是一家的,来来来族谱上记一下/哦兄弟我们是一家的,来来来……桥豆麻袋!族谱上记过了……不用记了诶)淦!你忘了我们很久以前见过面的!!!
⬆(淦!你忘了我们很久以前见过面的!!!)

void add(int x, int y){
    
    
	if(find(x)!=find(y)){
    
    
		data[find(x)]=find(y);
	}
}

完整代码

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

int data[1003];

int find(int x){
    
    //寻找根节点
	while(data[x]!=x){
    
    
		x=data[x];
	}
	return x;//返回根节点的值
}

void add(int x, int y){
    
    
	if(find(x)!=find(y)){
    
    
		data[find(x)]=find(y);
	}
}

int main(int argc, char const *argv[]) {
    
    
	int n,m;//n元素个数,m元素连接条数
	int x,y;//x,y是两个相连的元素
	int flag;//未联通的结点数

	while (cin >> n,n) {
    
    

		memset(data,0,sizeof (data));
		for (int i = 1; i <= n; i++) {
    
    
			data[i] = i;
		}

		for (cin >> m; m > 0; m--) {
    
    
			cin >> x;
			cin >> y;
			add(x,y);
		}

		flag = -1;
		for (int i = 1; i <= n; i++) {
    
    
			if(data[i]==i){
    
    
				flag++;//数组内容等于数组下标表示和自己联通
			}
		}
	
		cout << flag << endl;
	}

	return 0;
}

关于flag的初值

其实难以理解的地方可能是flag的初值为什么要设置成-1,flag存储的是数组内容等于数组下标的情况(自己连接自己的情况)有多少,每有一个就说明有一种连接种类,全联通状态只有一个这样的情况——即根节点,也就是除了根节点和自己连接,其他的节点都不和自己连接,所以要把根节点这个情况排除在外,即flag初值为-1。
这样一来,当全联通时,只有根节点的data[i]=i,flag++,flag的值就为0,最后输出flag的值也就是还需要修多少条路。

举个栗子可能更方便理解
当输入为
5 2
1 2
4 5

此时data数组的情况是(第四行):在这里插入图片描述
连接种类有三种:根节点为2的1、2连接、根节点为3的自己和自己连接、以及根节点为5的4、5连接。
此时data[i]=i的情况会出现3次,flag的终值也就是-1自加三次,也就是2,表示还需要修两条路,举个例子,3 2,3 4这样修,那会出现什么情况呢?(为了便于理解拆分一下这个过程)
在这里插入图片描述
在这里插入图片描述
3 2的时候,3将2的根节点(也就是2本身)认作祖宗,3 4的时候,经过find函数查看族谱,诶大家祖宗不一样诶但我们是一家人,那就一个祖宗认另一个祖宗为爹好了,这样大家就在一起了,于是经过add函数修改族谱,3他爹2就认4他爹5当爹,1和3就成了5的乖孙孙。大家就是一家人了mua。(谁认谁当祖宗就看你怎么写了哦吼,data[find(x)]=find(y)是后面的数当前面的数的祖宗,data[find(y)]=find(x)就是前面的当祖宗咯)

猜你喜欢

转载自blog.csdn.net/Jormungand_V/article/details/108920910