数据结构之并查集

1.概述

英文:DisjointSet or(Union-find set),即“不相交集合”将编号分别为1…N的N个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。

常见两种操作:

(1)两个集合      (2) 找某元素属于哪个集合
所以也成称为并查集。。

2.导引问题

畅通工程:HDU1232 

Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路? 
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。 
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。 
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。 

如果遇到这题,我们该如何去做呢?

3.并查集引入

初始化操作:

我们需要一个 void makeset (int  n)n表示初始化的范围

void makeset(int n)
{
     for(int i = 0; i <= n; i++)
        father[i] = i;
}

查找操作:

假如我们现在有要在这个集合中寻找一个元素x所处的集合(该元素的符节点),那么我们只需要一个 int findset(int a)函数

//递归写法
int findset(int a){
     if(a==father[a])  return a;
     return findset(father[a]);
}
//迭代写法
int findset(int x){
    int p = x;
    while(p!=father[p]) p = father[p];
    return p;
}
findset返回的值为该元素根节点的下标

合并操作:

假如 x 和 y开始是属于不同集合的元素,现在要把它们并到一起,那么需要一个 void unionset (int x , int y)函数

void unionset(int x, int y)
{
     int a = findset(x);
     int b = findset(y);
     if(a != b)  father[a] = b;
}

有了以上关于并查集的基础,相信都可以写HDU1232这道题了。。

#include <iostream>
#define SIZE 10005
using namespace std;

int father[SIZE];
void makeset(int n)
{
     for(int i = 0; i <= n; i++)
        father[i] = i;
}
int findset(int a){
     if(a == father[a])  
        return a;
     return findset(father[a]);
}
void unionset(int x, int y)
{
     int a = findset(x);
     int b = findset(y);
     if(a != b)  father[a] = b;
}
int main()
{
    int n, m;
    int x, y;
    int ans = 0;
    while(cin>>n>>m)
    {
       makeset(n);
       for(int i = 1; i <= m; i++)
       {
         cin>>x>>y;
         unionset(x,y);
       }
       for(int i = 1; i <= n; i++)
       {
         if(father[i] == i)
            ans++;
       }
       ans--;
       cout<<ans<<endl;
       ans = 0;
    }
}

4.并查集的优化

Find_Set(x)时 路径压缩
寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示;可见,路径压缩方便了以后的查找。

Union(x,y)时 按秩合并
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。

int findset (int a)

//递归写法
int findset(int a){
     if(a==father[a])  
        return a;
     else {
       int temp = father[a];
       father[a] = findset(father[a]);   
     } 
     return father[a]; //father[a] 已经是根节点,直接返回就行了
}

//非递归的方式进行路径压缩,更加直观一些
int findset(int x){
    int p = x,temp;
    while(p!=father[p]) p = father[p];
    //fatherpath comfatherression
    while(x != p){
        temp = father[x];
        father[x] = p;
        x = temp;
    }
    return p;
}
5.最终模板

void makeset(int n){
    int i;
    for(i=1;i<=n;i++){
       father[i] = i; 
       rank[i] = 1;                 
    }
}

int findset(int a){
     if(a==father[a])  
        return a;
     else {
       int temp = father[a];
       father[a] = findset(father[a]);   
       rank[a] = (rank[temp]+rank[a]+1)%2 ; 
    //必须有,更新路径压缩之后a与根结点之间的关系; father改变,rank就必须要跟着改变 
     } 
     return father[a];
}

void unionset(int a,int b){
     int fa,fb;
     fa = findset(a);
     fb = findset(b);
     if(fa!=fb){
         father[fa] = fb;   
         rank[fa] = (rank[a]+rank[b])%2 ;     //fa结点以下的结点的rank不需要改               
     }
}

6相关题目练习

待更新

参考目录:

http://www.cnblogs.com/cherish_yimi/archive/2009/10/11/1580839.html






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值