《算法笔记》9.6小节——数据结构专题(2)->并查集

9.6.1并查集的定义

合并Union,查找find,集合set

9.6.2并查集的基本操作

初始化,查找,合并
并查集产生的每一个集合都死一棵树

9.6.3路径压缩

对查找操作进行压缩,有递推写法和递归写法
问题 A: 通信系统
问题描述:某市计划建设一个通信系统。按照规划,这个系统包含若干端点,这些端点由通信线缆链接。消息可以在任何一个端点产生,并且只能通过线缆传送。每个端点接收消息后会将消息传送到与其相连的端点,除了那个消息发送过来的端点。如果某个端点是产生消息的端点,那么消息将被传送到与其相连的每一个端点。
为了提高传送效率和节约资源,要求当消息在某个端点生成后,其余各个端点均能接收到消息,并且每个端点均不会重复收到消息。
现给你通信系统的描述,你能判断此系统是否符合以上要求吗?

  • 输入
输入包含多组测试数据。每两组输入数据之间由空行分隔。
每组输入首先包含2个整数N和M,N(1<=N<=1000)表示端点个数,M(0<=M<=N*(N-1)/2)表示通信线路个数。
接下来M行每行输入2个整数A和B(1<=A,B<=N),表示端点A和B由一条通信线缆相连。两个端点之间至多由一条线缆直接相连,并且没有将某个端点与其自己相连的线缆。
当N和M都为0时,输入结束。
  • 输出
对于每组输入,如果所给的系统描述符合题目要求,则输出Yes,否则输出No。
  • 样例输入
4 3
1 2
2 3
3 4

3 1
2 3

0 0
  • 样例输出
Yes
No

这题我犯了好多小错误
1、当n0&&m0的时候才可以break,我写成了n0||m0的时候break,这就导致m=0时其实是正常情况,也被返回了
2、在while循环里不要return,我在判断m!=n-1时就直接printf no 然后return了,后面的情况也没有被执行
3、这里有一个很巧妙的判断,就是要想端点都能接受消息,并且不会重复接收消息,那么m必须是n-1才可以
我一开始想到的是在union时,如果发现两个点已经在同一个集合里了,这样再union会形成环,说明会重复接到消息,不过这样当然没有直接判断m=n-1来的方便

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
using namespace std;
const int maxn = 1010;//最多的端点个数
int father[maxn];//存放父亲节点
int root[maxn];//存放以i作为根节点的节点数量
int findFather(int x)
{
	int a = x;//带上了路径压缩
	while (x != father[x])
		x = father[x];
	while (a != father[a])
	{
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}
void Union(int a, int b)
{
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA!=faB)//说明可能重复接到消息		
		father[faA] = faB;		
}
void init(int n)//初始化n个节点
{
	for (int i = 1; i <= n; i++)
	{
		father[i] = i;
		root[i] = 0;
	}
}
int main()
{	
	int n, m;
	int i;
	int flag;
	int a, b;
	while (scanf("%d %d", &n, &m) != EOF)
	{
		if (n == 0 && m == 0)
			break;
		init(n);
		for (i = 0; i < m; i++)
		{
			scanf("%d %d", &a, &b);
			Union(a, b);
		}
		if (m != n - 1)
		{
			printf("No\n");

		}
		else
		{
			flag = 0;
			for (i = 1; i <= n; i++)
			{
				root[findFather(i)]++;
			}
			for (i = 1; i <= n; i++)
			{
				if (root[i] == n)
				{
					flag = 1;
					break;
				}
			}
			if (flag == 1)
				printf("Yes\n");
			else

				printf("No\n");
		}		
	}
	return 0;
}

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

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

和书上的思想一致

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
using namespace std;
const int maxn = 1010;//最多的端点个数
int father[maxn];//存放父亲节点
int root[maxn];//存放以i作为根节点的节点数量
int findFather(int x)
{
	int a = x;//带上了路径压缩
	while (x != father[x])
		x = father[x];
	while (a != father[a])
	{
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}
void Union(int a, int b)
{
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA!=faB)//说明可能重复接到消息		
		father[faA] = faB;		
}
void init(int n)//初始化n个节点
{
	for (int i = 1; i <= n; i++)
	{
		father[i] = i;
		root[i] = 0;
	}
}
int main()
{	
	int n, m;
	int i;	
	int a, b;
	while (scanf("%d", &n) != EOF)
	{
		if (n == 0)
			break;
		scanf("%d",&m);
		init(n);
		for (i = 0; i < m; i++)
		{
			scanf("%d %d",&a,&b);
			Union(a, b);
		}
		for (i = 1; i <=n; i++)
		{
			root[findFather(i)] = 1;
		}
		int ans = 0;
		for (i = 1; i <= n; i++)
		{
			ans += root[i];
		}
		//算出来一共有多少个集合
		printf("%d\n",ans-1);
	}
	return 0;
}

问题 C: How Many Tables
问题描述:今天是伊格内修斯的生日。他邀请了很多朋友。现在是晚餐时间。Ignatius 想知道他至少需要多少张桌子。你要注意,并不是所有的朋友都互相认识,所有的朋友都不想和陌生人呆在一起。

这个问题的一个重要规则是,如果我告诉你 A 认识 B,B 认识 C,这意味着 A、B、C 彼此认识,所以他们可以留在一张桌子上。

例如:如果我告诉你A认识B,B认识C,D认识E,那么A、B、C可以在一张桌子上,而D、E必须在另一张桌子上。所以伊格内修斯至少需要 2 张桌子。

  • 输入
输入以整数 T(1<=T<=25) 开头,表示测试用例的数量。然后是 T 测试用例。每个测试用例以两个整数 N 和 M(1<=N,M<=1000) 开始。N 表示好友的数量,好友从 1 到 N 标记,然后是 M 行。每行由两个整数 A 和 B(A!=B) 组成,这意味着朋友 A 和朋友 B 彼此认识。两个案例之间会有一个空行。
  • 输出
对于每个测试用例,只需输出 Ignatius 至少需要多少张表。不要打印任何空白。
  • 样例输入
2
6 4
1 2
2 3
3 4
1 4

8 10
1 2
2 3
5 6
7 5
4 6
3 6
6 7
2 5
2 4
4 3
  • 样例输出
3
2

一样的思路啦,这题说一张桌子的人相互认识,实际上A认识B,B认识C就可以在一张桌子,没有要求A认识C,恰好是并查集

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
using namespace std;
const int maxn = 1010;//最多的端点个数
int father[maxn];//存放父亲节点
int root[maxn];//存放以i作为根节点的节点数量
int findFather(int x)
{
	int a = x;//带上了路径压缩
	while (x != father[x])
		x = father[x];
	while (a != father[a])
	{
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}
void Union(int a, int b)
{
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA!=faB)//说明可能重复接到消息		
		father[faA] = faB;		
}
void init(int n)//初始化n个节点
{
	for (int i = 1; i <= n; i++)
	{
		father[i] = i;
		root[i] = 0;
	}
}
int main()
{	
	int T;
	int n, m;
	int i;	
	int j;
	int a, b;
	scanf("%d",&T);
	for (j = 0; j < T; j++)
	{
		scanf("%d %d", &n, &m);
		init(n);
		for (i = 0; i < m; i++)
		{
			scanf("%d %d", &a, &b);
			Union(a, b);
		}
		for (i = 1; i <= n; i++)
		{
			root[findFather(i)] = 1;
		}
		int ans = 0;
		for (i = 1; i <= n; i++)
		{
			ans += root[i];
		}
		//算出来一共有多少个集合
		printf("%d\n", ans );
	}
	
	return 0;
}

问题 D:越多越好
问题描述:王先生想要一些男孩帮他做一个项目。因为项目比较复杂,男生越多越好。当然也有一定的要求,王先生选了一个足够容纳男生的房间。没有被选中的男孩必须立即离开房间。房间里一开始就有1000万男孩,编号从1到1000万。王先生选了以后,在这间屋子里的任何两个都应该是朋友(直接或间接),否则就只剩下一个男孩了。鉴于所有直接的朋友对,您应该决定最佳方式。

  • 输入
输入的第一行包含一个整数 n (0 ≤ n ≤ 100 000) - 直接朋友对的数量。以下 n 行每行包含一对数字 A 和 B,由一个空格分隔,表明 A 和 B 是直接朋友。(A≠B,1≤A,B≤10000000
  • 输出
一行中的输出正好包含一个整数,等于王先生可以保留的最大男孩数。
  • 样例输入
3
1 3
1 5
2 5
4
3 2
3 4
1 6
2 6
  • 样例输出
4
5

1、这题注意数组别越界了,不然会显示运行错误
2、另外当n=0时,按照题目的意思,应该只剩下一个男孩,即输出1,但是我的程序没有注意到,直接输出了0,也通过了
3、内存限制问题,这一题的内存限制成了128MB
我的程序是79316KB,主要是开了一个1e7的数组,4字节Int×1e7==40000KB=40MB,题目中开了两个,即father[maxn]和root[maxn],大约80MB,是小于128MB的
4、减少时间,有两个改进的地方,一个是记录maxntemp,这样就不用循环到maxn浪费时间了,第二个是不使用下面这个循环计算根为i的集合的元素个数

for (i = 1; i <=maxntemp; i++)
		{
			root[findFather(i)]++;
		}

改进1完整代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
using namespace std;
const int maxn = 10000010;//最多的端点个数
int father[maxn];//存放父亲节点
int root[maxn];//存放以i作为根节点的节点数量
int findFather(int x)
{
	int a = x;//带上了路径压缩
	while (x != father[x])
		x = father[x];
	while (a != father[a])
	{
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}
void Union(int a, int b)
{
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA!=faB)//说明可能重复接到消息		
		father[faA] = faB;		
}
void init(int n)//初始化n个节点
{
	for (int i = 1; i <= n; i++)
	{
		father[i] = i;
		root[i] = 0;
	}
}
int main()
{	

	int n, m;
	int i;	
	int max;
	int a, b;
	int maxntemp=0;
	while (scanf("%d", &n) != EOF)
	{
		init(maxn-1);
		for (i = 1; i <=n; i++)
		{
			scanf("%d %d", &a, &b);
			Union(a, b);
			if (a > maxntemp)
				maxntemp = a;
			if (b > maxntemp)
				maxntemp = b;
		}
		for (i = 1; i <=maxntemp; i++)
		{
			root[findFather(i)]++;
		}
		max = 0;
		for (i = 1; i <=maxntemp; i++)
		{
			if (root[i] > max)
				max = root[i];
		}
		printf("%d\n", max);
	}	
	return 0;
}

而是在Union的时候就计算个数,并且更新全局变量max,这里就需要考虑n=0时,输出1(没有考虑时会输出答案错误),有点不懂为啥上面的代码没有考虑,也是正确的
如下:

if(n!=0)
		printf("%d\n", max);
		if (n == 0)
			printf("1\n");

改进2完整代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
using namespace std;
const int maxn = 10000010;//最多的端点个数
int father[maxn];//存放父亲节点
int root[maxn];//存放以i作为根节点的节点数量
int max;
int findFather(int x)
{
	int a = x;//带上了路径压缩
	while (x != father[x])
		x = father[x];
	while (a != father[a])
	{
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}
void Union(int a, int b)
{
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA != faB)//说明可能重复接到消息		
	{
		father[faA] = faB;
		root[faB] += root[faA];
		if (max < root[faB])
			max = root[faB];
	}
				
}
void init(int n)//初始化n个节点
{
	for (int i = 1; i <= n; i++)
	{
		father[i] = i;
		root[i] = 1;
	}
}
int main()
{	

	int n, m;
	int i;	
	int a, b;
	int maxntemp=0;
	while (scanf("%d", &n) != EOF)
	{
		init(maxn-1);
		max = 0;
		for (i = 1; i <=n; i++)
		{
			scanf("%d %d", &a, &b);
			Union(a, b);
		}
		if(n!=0)
		printf("%d\n", max);
		if (n == 0)
			printf("1\n");
	}	
	return 0;
}

总结
之前算法课老师上课讲过这个,因为只用了数组,如果想到了并查集的思想实现起来还是很轻松的,关键就是要能想到
并查集主要考察的代码在书上已经给出来了,要不就是判断有几个集合(要几张桌子(ans),还要修几条路(ans-1)),要不就是判断集合里面元素的个数(求可以留下来的男孩数),这两个的区别一个root数组是bool,一个是Int,其实都用int 也可以。撒花花!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值