并查集详解

Posted han-sy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并查集详解相关的知识,希望对你有一定的参考价值。

并查集(Union-Find)

常用来解决动态连通性问题。

曾有外国网友在StackExchange上发起过投票,选出世界十大有趣算法。“Union-Find算法”以4票的微弱优势战胜排名第二的“KMP算法”,荣登榜首。

原投票网址传送门

投票截止时间:2014年3月7日

基本思想:每个集合用一棵树表示。因为同一棵树上的每一个节点都有相同的根,这样就可以用根节点来标识每一棵树,即用代表元来标识每一个集合。我们可以将树存储在数组中。即S[i]=j。代表j为i的父节点。


并查集的基本应用:

  1. 确定元素属于哪个集合
  2. 判断两个元素是否属于同一集合
  3. 合并两元素
  4. 求一个无向图中连通分量的个数
  5. 判断增加一条边后是否会产生环(可用在kruskal算法中)

生活中的问题:

  1. 朋友圈
  2. 村中的族谱(大伯二婶三叔)
  3. 武林门派
  4. 路由网络

初始化的两种方法:

第一种:刚开始时每个集合中都只有一个元素,每个元素在树中都是根节点。故设S[i]=-1;
i 1 2 3 4 5
j -1 -1 -1 -1 -1
第二种:对于每个节点i。他的组号也是i,即S[i]=i
i 1 2 3 4 5
j 1 2 3 4 5

并集的三种方法:

这里有普通合并和两个启发函数可以选择:

第一种:普通合并(使用第1/2种初始化):无脑合并,两个元素谁合并到谁都行。
第二种:根据节点数量进行合并(使用第1种初始化):将节点少的树合并到节点数量多的树中.
第三种:根据树的高度进行合并(使用第2种初始化):将高度小的树合并到高度高的树中.

假设初始节点为1,2,3,4,5。合并顺序为:1和2、2和3、4和5


查集的两种方法:

第一种:(前提是用的第1种初始化和能使用第1种初始化方法的合并方法)(路径压缩)

J为负数时,代表i为根节点。并且j的绝对值代表这棵树中的节点个数。

J为正数时,代表j是i的父节点

查找x所在的集合

X=a[x] 直到a[x]小于0(等于-1)

则这个小于0(等于-1)的a[x]中的x就是目标集合,节点x就是这棵树的根节点

  • 例:查找元素1所在的集合
  • x=a[1]=2>0
  • x=a[2]=3>0
  • x=a[3]=-3 <0
  • 即:3为目标集合,3为这棵树的根

根据合并顺序,结果为

i 1 2 3 4 5
j 2 3 -3(-1) 5 -2(-1)
第二种:(前提是用的第2种初始化和能使用第2种初始化方法的合并方法)(非路径压缩)

J的值只能为正数,代表j是i的父节点(i=j时代表i是根节点)

查找x所在的集合

x=a[x] 直到a[x]等于x为止

则这个等于a[x]的x就是目标集合,节点x就是这棵树的根节点

  • 例:查找元素1所在的集合
  • a[1]=2!=1
  • a[2]=3!=2,
  • a[3]=3==3 故3为目标集合, 3为这棵树的根

根据合并顺序,结果为

i 1 2 3 4 5
j 2 3 3 5 5

“并“、”查”的基本方法与优化

由上述可知,并与查的操作可以有以下4种方式:

? 技术图片

技术图片


源代码

基于上图四种方式,给出相应的源代码及输入和输出信息。

一、第1种初始化,第1种合并,第1种查询

#include <iostream>
#include <string.h>
#include <stdio.h>
#include<iomanip>
using namespace std;

int father[1002];
int m,n;     //分别代表节点数和关系数
int count;   //统计集合的个数

int Find(int x)
{
    if(father[x]==-1)
        return x;
    return Find(father[x]);  //或者用return father[x]=Find(father[x]);
    //father[t]=Find(father[t]);    //如果用这一句,则报错:Memory Limit Exceeded
}

void Union(int x,int y)
{
    x=Find(x);
    y=Find(y);
    if(x!=y)
    {
        father[x]=y;
        count--;
    }
}

int main()
{
    freopen("input2.txt", "r", stdin);

    memset(father, -1, sizeof(father));  //都设置为-1

    cin>>m>>n;
    count=m;  //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        Union(x,y);
    }


    //显示之间的关系
    cout<<"树状关系如下:"<<endl;
    for(int i=1;i<=m;i++)
        cout<<i<<" --> "<<setw(2)<<father[i]<<endl;

    cout<<endl<<"集合个数:"<<count<<endl;
    for(int i=1;i<=m;i++)
    {
        int flag=0;
        if(father[i]==-1)
        {
            flag=1;
            cout<<"根节点"<<i<<"代表一个集合。其中有:";
            for(int j=1;j<=m;j++)
            {
                if(Find(j)==i)
                    cout<<j<<" ";
            }
        }
        if(flag==1)
             cout<<endl;

    }
}

二、第1种初始化,第2种合并,第1种查询

#include <iostream>
#include <string.h>
#include <stdio.h>
#include<iomanip>
using namespace std;

int father[1002];
int m,n;     //分别代表节点数和关系数
int count;   //统计集合的个数

int Find(int x)
{
    if(father[x]<0)
        return x;
    return Find(father[x]);  //或者用return father[x]=Find(father[x]);
    //father[t]=Find(father[t]);    //如果用这一句,则报错:Memory Limit Exceeded
}

void Union(int x,int y)
{
    x=Find(x);
    y=Find(y);
    if(x==y)
    {
        return;
    }

    if(father[x]<father[y])
    {
        father[x]+=father[y];
        father[y]=x;
        count--;
    }
    else                       //默认两树中节点相同时,让x并到y上。当然,也可以默认y并到x上
    {
        father[y]+=father[x];
        father[x]=y;
        count--;
    }
}

int main()
{
    freopen("input2.txt", "r", stdin);

    memset(father, -1, sizeof(father));  //都设置为-1

    cin>>m>>n;
    count=m;  //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        Union(x,y);
    }


    //显示之间的关系
    cout<<"树状关系如下:"<<endl;
    for(int i=1;i<=m;i++)
        cout<<i<<" --> "<<setw(2)<<father[i]<<endl;

    cout<<endl<<"集合个数:"<<count<<endl;
    for(int i=1;i<=m;i++)
    {
        int flag=0;
        if(father[i]<0)
        {
            flag=1;
            cout<<"根节点"<<i<<"代表一个集合。其中有:";
            for(int j=1;j<=m;j++)
            {
                if(Find(j)==i)
                    cout<<j<<" ";
            }
        }
        if(flag==1)
             cout<<endl;

    }
}

三、第2种初始化,第1种合并,第2种查询

#include <iostream>
#include <stdio.h>
#include<iomanip>
using namespace std;

int father[1002];
int m,n;     //分别代表节点数和关系数
int count;   //统计集合的个数

void Init()
{
    for(int i=1;i<=m;i++)
    {
        father[i]=i;
    }

}
int Find(int x)
{
    if(x==father[x])
        return x;
    return Find(father[x]);  //或者用return father[x]=Find(father[x]);
    //father[t]=Find(father[t]);    //如果用这一句,则报错:Memory Limit Exceeded
}

void Union(int x,int y)
{
    x=Find(x);
    y=Find(y);
    if(x!=y)
    {
        father[x]=y;
        count--;
    }
}

int main()
{
    freopen("input2.txt", "r", stdin);

    cin>>m>>n;
    Init();
    count=m;  //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        Union(x,y);
    }


    //显示之间的关系
    cout<<"树状关系如下:"<<endl;
    for(int i=1;i<=m;i++)
        cout<<i<<" --> "<<setw(2)<<father[i]<<endl;

    cout<<endl<<"集合个数:"<<count<<endl;
    for(int i=1;i<=m;i++)
    {
        int flag=0;
        if(i==father[i])
        {
            flag=1;
            cout<<"根节点"<<i<<"代表一个集合。其中有:";
            for(int j=1;j<=m;j++)
            {
                if(Find(j)==i)
                    cout<<j<<" ";
            }
        }
        if(flag==1)
             cout<<endl;

    }
}

四、第2种初始化,第3种合并,第2种查询

#include <iostream>
#include <stdio.h>
#include<iomanip>
using namespace std;

int father[1002];
int height[1002];
int m,n;     //分别代表节点数和关系数
int count;   //统计集合的个数

void Init()
{
    for(int i=1;i<=m;i++)
    {
        father[i]=i;
        height[i] = 1;
    }

}
int Find(int x)
{
    if(x==father[x])
        return x;
    return Find(father[x]);  //或者用return father[x]=Find(father[x]);
    //father[t]=Find(father[t]);    //如果用这一句,则报错:Memory Limit Exceeded
}



void Union(int x,int y)
{
    x=Find(x);
    y=Find(y);
    if(x==y)
    {
        return;
    }

    if(height[x]==height[y])
    {
        height[x]++;       //当两棵树一样高的时候,则增加其中一棵的高度
        father[x]=y;       //因为是让x所在的树高度自增的,所以此时x树要比y树高了,所以要把y并到x树上。当然,也可以让y树自增然后把x并到y树上
        count--;
    }
    else if(height[x]<height[y])
    {
        father[x]=y;
        count--;
    }
    else
    {
        father[y]=x;
        count--;
    }
}

int main()
{
    freopen("input2.txt", "r", stdin);


    cin>>m>>n;
    Init();
    count=m;  //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
    for(int i=1;i<=n;i++)
    {
        int x,y;
        cin>>x>>y;
        Union(x,y);
    }

    //显示之间的关系
    cout<<"树状关系如下:"<<endl;
    for(int i=1;i<=m;i++)
        cout<<i<<" --> "<<setw(2)<<father[i]<<endl;

    cout<<endl<<"集合个数:"<<count<<endl;
    for(int i=1;i<=m;i++)
    {
        int flag=0;
        if(i==father[i])
        {
            flag=1;
            cout<<"根节点"<<i<<"代表一个集合。其中有:";
            for(int j=1;j<=m;j++)
            {
                if(Find(j)==i)
                    cout<<j<<" ";
            }
        }
        if(flag==1)
             cout<<endl;

    }


}

输入:技术图片

输出:技术图片

以上是关于并查集详解的主要内容,如果未能解决你的问题,请参考以下文章

详解并查集

详解并查集

详解并查集

并查集算法详解

并查集详解

详解并查集