Description
You know that there are n students in your university (0 < n <= 50000). It is infeasible for you to ask every student their religious beliefs. Furthermore, many students are not comfortable expressing their beliefs. One way to avoid these problems is to ask m (0 <= m <= n(n-1)/2) pairs of students and ask them whether they believe in the same religion (e.g. they may know if they both attend the same church). From this data, you may not know what each person believes in, but you can get an idea of the upper bound of how many different religions can be possibly represented on campus. You may assume that each student subscribes to at most one religion.
Input
Output
Sample Input
10 9 1 2 1 3 1 4 1 5 1 6 1 7 1 8 1 9 1 10 10 4 2 3 4 5 4 8 5 8 0 0
Sample Output
Case 1: 1Case 2: 7
题目大意:
有n个人,m对人有相同的宗教信仰,求最多有几种宗教信仰。
解题思路:
将相同宗教信仰的人合并,用到并查集的知识。
将近一个月没敲代码了TT。知识点都忘得差不多了。只好刷刷水题来练练手了。看完题就知道用并查集做了。但是代码敲不出来(太弱了),可能是之前学并查集也没亲自敲击此代码。顺便复习一下:
并查集支持以下三种操作:
1、Make_Set(x) 把每一个元素初始化为一个集合
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身。
2、Find_Set(x) 查找一个元素所在的集合
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。 合并两个集合,也是使一个集合的祖先成为另一个集合的祖先,具体见示意图
3、Union(x,y) 合并x,y所在的两个集合
合并两个不相交集合操作很简单: 利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。
实现方法
1.用编号最小的元素标记所在集合;
2.定义一个数组 set[1..n] ,其中set[i] 表示元素i 所在的集合;
i: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Set[i] : 1 |
2
1
4
2
6
1
6
2
2
不相交集合:{1,3,7}, {4}, {2,5,9,10}, {6,8}
并查集的优化
1、Find_Set(x)时 路径压缩 寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度。路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了。
朴素查找的代码,适合数据量不大的情况:
int findx(int x) {
int r=x;
while(parent[r] !=r) r=parent[r];
return r; }
下面是采用路径压缩的方法查找元素:
int find(int x) //查找x元素所在的集合,回溯时压缩路径
{
if (x != parent[x]) { parent[x] = find(parent[x]); //回溯时的压缩路径
} //从x结点搜索到祖先结点所经过的结点都指向该祖先结点 return parent[x]; }
上面是一采用递归的方式压缩路径, 但是,递归压缩路径可能会造成溢出栈,下面我们说一下非递归方式进行的路径压缩:
int find(int x)
{
int k, j, r;
r = x;
while(r != parent[r]) //查找跟节点
r = parent[r]; //找到跟节点,用r记录下
k = x;
while(k !=r) //非递归路径压缩操作
{
j = parent[k]; //用j暂存parent[k]的父节点
parent[k] = r; //parent[x]指向跟节点
k = j; //k移到父节点
}
return r; //返回根节点的值
}
附上代码:
#include<iostream>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define M 50001
int pre[M];
int t[M];
void makeset(int n)
{
int i;
for(i=1;i<=n;i++)
pre[i]=i;
}
int find(int x)
{
int r=x;
while(r!=pre[r])
r=pre[r];
int i=x,j;
while(pre[i]!=r)
{
j=pre[i];
pre[i]=r;
i=j;
}
return r;
}
void mix(int p,int q)
{
int x=find(p),y=find(q);
if(x!=y)
{
pre[y]=x;
}
}
int main()
{
int n,m,i,p,q;
int ans=0;
while(scanf("%d%d",&n,&m)!=EOF&&n&&m)
{
ans++;
int count =0;
makeset(n);
for(i=1;i<=m;i++)
{
scanf("%d%d",&p,&q);
mix(p,q);
}
memset(t,0,sizeof(t));
for(i=1;i<=n;i++)
{
t[find(i)]=1;
}
for(i=1;i<=n;i++)
{
if(t[i])
count++;
}
printf("Case %d: %d\n",ans,count);
}
return 0;
}
推荐做HDU 1232 畅通工程点击打开链接