题目链接:http://acm.tju.edu.cn/toj/showp2469.html
题目大意:给定一些人,如果他们不是朋友,就不能住在一间房间。朋友间的关系是可以传递的,即A和B是朋友,B和C是朋友,那么A和C也是朋友。 给定人数和他们的关系,问至少需要多少个房间;
思路:画个示意图,瞬间发现是求连通分量个数的问题,直接并查集,最终连通分量的个数即为房间数。用并查集的关键在于实现以下几个函数:Make, Find, Union!下面copy一段关于并查集的定义和使用(如果你对并查集不熟的话,可以看看)
l
一种简单的用途广泛的集合.
l
1、Make_Set(x)
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身(也可以根据情况而变)。
2、Find_Set(x)
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
合并两个集合,也是使一个集合的祖先成为另一个集合的祖先,具体见示意图
3、Union(x,y)
合并两个不相交集合操作很简单:
利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。
l
1、Find_Set(x)时
寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示;可见,路径压缩方便了以后的查找。
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
结合本题的代码,你会有一个更加深刻的理解:
ps:在第二遍做这道题的时候, 犯了一个致命的细节错误,问题时测试用例还没有测出来, 因为数组下标表示的是人的标号 而人的标号是从1开始的 所以再循环时,标号不能从0开始!!
#include <iostream>
#include <cstring>
using namespace std;
struct people //最近对数组无感 发现struct很好用的
{
int ancestor;
}people[102];
int find(int i) //!!关键 find 函数 不断通过 j=people[j].ancestor来找祖先
{
int j = i;
while(1)
{
if(people[j].ancestor == -1)
{
if(j!=i)
people[i].ancestor = j; //在返回祖先时别忘了把该节点的祖先设置为找到的祖先
return j;
}
j = people[j].ancestor;
}
}
int main()
{
int cas;
cin>>cas;
while(cas--)
{
int n,i,m;
cin>>n>>m;
for(i=1;i<=n;i++)
{
people[i].ancestor=-1; //关键 make函数 :初始化每个节点的祖先都是-1,在find函数中判断为-1时,返回祖先本身;
}
for(i=0;i<m;i++)
{
int a,b,x,y;
cin>>a>>b;
x = find(a);
y = find(b);
if(x!=y)
{
people[x].ancestor = y; //关键 union函数: 如果两个节点的祖先不同,因为有朋友关系,所以把一个的祖先设置为另一个节点
}
}
int ans = 0;
for(i=1;i<=n;i++) //下标从1开始啊 魂淡
{
if(people[i].ancestor == -1) //多少个根 跟少个连通分量 即多少间房
ans++;
}
cout<<ans<<endl;
}
return 0;
}