定义:
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
集就是让每个元素构成一个单元素的集合,并就是按一定顺序将属于同一组的元素所在的集合合并。
主要操作:
初始化:
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
查找:
查找元素所在的集合,即根节点。
合并:
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
最近几天都在做关于“并查集”应用的题,下面是我在OJ刷过的,把代码贴出来和大家看下,最后再总结下应用并查集的心得:(难度由简单到难)
POJ2524:http://poj.org/problem?id=2524
#include<stdio.h>
int father[50001];
int Find(int x)
{
if(father[x]==x)
return x;
else
return father[x]=Find(father[x]);
}
void Union(int x,int y)
{
int rx,ry;
rx=Find(x);
ry=Find(y);
father[rx]=ry;
}
void main()
{
int x,y,rx,ry;
int n,m,num=0;
int i;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0 && m==0)
break;
num++;
for(i=0;i<n;i++)
father[i]=i;
while(m--)
{
scanf("%d%d",&x,&y);
rx=Find(x);
ry=Find(y);
if(rx!=ry)
{
Union(x,y);
n--;
}
}
printf("Case %d: %d\n",num,n);
}
}
该题是最简单的并查集应用:其中函数
int Find(int x)
{
if(father[x]==x)
return x;
else
return father[x]=Find(father[x]);
}
在查找最终根节点时运用递归函数在第一次查找时进行了路径优化,即第一次查找是的时间复查度是O(n),n代表查找节点到根节点路径上的节点数,以后再次查找该路径上节点的根节点时的时间复杂度是O(1)。
poj1611: http://poj.org/problem?id=1611
#include<stdio.h>
int father[30001],a[30001];
int getFather(int x)
{
if(father[x]==x)
return x;
else
return father[x]=getFather(father[x]);
}
void Union(int x,int y)
{
int rx,ry;
rx=getFather(x);
ry=getFather(y);
father[rx]=ry;
}
void main()
{
int n,m,num;
int i,ans;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0 && m==0)
break;
ans=0;
for(i=0;i<n;i++)
{
father[i]=i;
}
while(m--)
{
scanf("%d",&num);
for(i=0;i<num;i++)
scanf("%d",&a[i]);
for(i=1;i<num;i++)
Union(a[i-1],a[i]);
}
for(i=0;i<n;i++)
{
if(getFather(i) == getFather(0))
ans++;
}
printf("%d\n",ans);
}
}
该题和上题一样,注意一下怎么进行节点合并就好了。题目意思不难懂代码也易懂!
POJ2492:http://poj.org/problem?id=2492
#include<stdio.h>
int father[2001],rank[2001];
int Find(int x)
{
int temp;
if(father[x]==x)
return x;
temp=father[x];
father[x]=Find(father[x]);
rank[x]=(rank[x]+rank[temp])%2;
return father[x];
}
void Union(int x,int y)
{
int rx,ry;
rx=Find(x);
ry=Find(y);
father[rx]=ry;
rank[rx]=(rank[y]-rank[x]+1)%2;
}
void main()
{
int Case=0,n,m,N;
int i,flag;
int x,y,rx,ry;
scanf("%d",&N);
while(N--)
{
Case++;
flag=0;
scanf("%d%d",&n,&m);
for(i=0;i<=n;i++)
{
father[i]=i;
rank[i]=0;
}
for(i=0;i<m;i++)
{
scanf("%d%d",&x,&y);
rx=Find(x);
ry=Find(y);
if(rx==ry)
{
if(rank[x]==rank[y])
{
flag=1;
}
}
else
Union(x,y);
}
if(flag==1)
{
printf("Scenario #%d:\n",Case);
printf("Suspicious bugs found!\n\n");
}
else
{
printf("Scenario #%d:\n",Case);
printf("No suspicious bugs found!\n\n");
}
}
}
对于该题,我们看下这两个函数:
int Find(int x)
{
int temp;
if(father[x]==x)
return x;
temp=father[x];
father[x]=Find(father[x]);
rank[x]=(rank[x]+rank[temp])%2;
return father[x];
}
void Union(int x,int y)
{
int rx,ry;
rx=Find(x);
ry=Find(y);
father[rx]=ry;
rank[rx]=(rank[y]-rank[x]+1)%2;
}
一个是合并函数,和上面两题不同:
这一题,增加一个rank[]数组,该数组用来记忆节点i与根节点的关系,这是这两天来做并查集,觉得并查集应用最重要的一点,因为一般比较复杂一点的题,就是要找到节点与根节点的关系(这是难点)。另外,就是当我们合并两棵树的时候,将一棵树接到另一棵树上时,就必须要更新这棵树(被合并树)上的节点与新的根节点的关系
,这又是一个难点,而通过这两天的学习,总结了那么一点点,请看下面:
rank[rx]=(rank[y]-rank[x]+1)%2;
在和并时,我们必须更新好被合并树根节点与新的根节点的关系,而接下来:
rank[x]=(rank[x]+rank[temp])%2;
我们在再次查找被合并数上的节点的根节点时,在压缩该节点与新的根节点路径上的节点时,同时跟新该节点与新的根节点的关系。
上面两点往往是并查集应用的难点:
1、寻找节点与根节点的关系;
2、合并一棵树后更新被合并树上节点与新的根节点的关系;
最后,接下来是并查集应用的一道难题:验证了上面的难点,该题的难点就是寻找节点与根节点的关系,唉,本人在做这道题时寻找关系的艰辛,就不说了,一言难尽啊!
POJ1182:http://poj.org/problem?id=1182
#include<stdio.h>
int father[50001],rank[50001];
int Find(int x)
{
int temp;
if(father[x]==x)
return x;
temp=father[x];
father[x]=Find(father[x]);
rank[x]=(rank[temp]+rank[x])%3;
return father[x];
}
void Union(int a,int b,int len)
{
int ra=Find(a);
int rb=Find(b);
father[ra]=rb;
rank[ra]=(rank[b]-rank[a]+3+len)%3;
}
int main()
{
int i,n,k;
int d,x,y;
int rx,ry;
int ans=0;
scanf("%d%d",&n,&k);
for(i=1;i<=n;i++)
father[i]=i;
while(k--)
{
scanf("%d%d%d",&d,&x,&y);
if(x>n || y>n || (d==2 && x==y))
ans++;
else
{
rx=Find(x);
ry=Find(y);
if(rx==ry)
{
if((rank[x]-rank[y]+3)%3 != d-1)
ans++;
}
else
Union(x,y,d-1);
}
}
printf("%d\n",ans);
return 0;
}
好吧,本次并查集的学习,到此就告一段落,重点内容在上面一点点,感兴趣的同学可以稍稍滑动鼠标,上次看看!