并查集初步理解

版权声明:转载请注明 https://blog.csdn.net/li13168690086/article/details/81316187

    写在之前:本文用来记录自己对并查集的初步理解。

  实质

    并查集是一种对元素进行分组管理的树型数据结构,高效地进行合并及查找。

    下面我将用图来表达。

    首先是图一,我们最初得到一些离散的点,而我们的目的就是将它们建成几颗不同的树。

图1
图1

一、将它们看做是独立的,并且每个点的根结点是自己。

二、然后按照所需的规则,合并它们,变成图二。

图二
图2

  可以从图二看出,最后合并的大概模样是几颗相互独立的树,特点就是拥有相同特点的元素汇聚在一起(这里以根结点作为相同特点)

  下面引用例题来更进一步理解。

例题

  出自HDU1232(https://cn.vjudge.net/problem/HDU-1232

  畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路? 

  测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。 注意:两个城市之间可以有多条道路相通。当N为0时,输入结束,该用例不被处理。

  对每个测试用例,在1行里输出最少还需要建设的道路数目。

样例输入

4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0

输出

1
0
2
998

一、题目理解

  问题是需要再建多少条道路,才能打通所有城镇。给出城镇的数量,连通的道路,即现在已连通的城镇。

  那么我们可以将能够相互走通的城镇划进一个集合,然后看能够得到多少个独立的集合,然后在每个集合之间连一条路,那么就可以实现所有道路的连通了(参照图一和图二进行理解),所以需要再建的道路数量就是集合数量减1(两个集合连1条、三个集合连2条、四个集合连3条......)。

  如果用并查集的思想来看就是,一开始将城镇看做是独立的元素,然后每个城镇连通自己,下面通过输入哪两个城市相连,就将它们合并,拥有同一根结点。完成所有合并过程后,根据根结点的数目就可以看做是有多少个集合,那么根结点数-1就是最后结果了。

  值得注意的是,这里的合并规则就是城镇的连通,如果两个城镇连通,那就可以合并成一个集合,代码在最后给出

并查集基础代码分析

  

#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
const int MAX_N (1010);
int par[MAX_N];    //这是存储每个元素所指向自己根结点的数组

void init(int n){
    for(int i = 1; i <= n; i++){
        par[i] = i;    //一开始将所有元素的根结点指向自己
    }
}

int find(int x){    //寻找根结点
    if(par[x] == x){ //如果找到的根结点是自己,就代表找到了最终的根结点。
        return x;
    }
    else
        return par[x]; //如果不是自己,就顺着自己所指向的结点往上找。
}


void unite(int x, int y){  //合并元素
    xx = find(x);    //找X的根结点
    yy = find(y);    //找Y的根结点
    if(xx == yy)    //如果相同,则代表是属于同一集合,就退出。
        return;
    par[yy] = xx;    //如果不是,就将其中一者指向另一个,形成一条链。
    return;
}

bool same(int x, int y){    //判断是否属于同一集合
    return find(x) == find(y);
}

    通过代码,我们可以直观看出并查集基础的实现原理,就是通过数组存储每个元素的根结点,然后将属于同一集合的元素相连,使其最终指向同一个根结点,主要函数包括init()初始化、find()寻找根结点、unite()合并元素,自己可以用实际例子运作一遍。待我们熟悉了基础代码后,我们再看进行优化后的并查集代码。

并查集优化代码分析

  优化主要针对寻找根结点find()和合并元素unite()两部分。

  如果按照基础代码进行运作,很有可能出现图3这种现象。

图3
图3

   如果发生这样的现象,就会导致寻找根结点的时间很长(如果从3找,就要往上找两次)。所以我们想,要是只查一次就好了。那我们需要树设计成图4的形状。

图4
图4

  所以这里将运用路径压缩的思想,就是将归属于同一根结点的元素,直接连接最终的根结点。下面是用递归优化find()函数的代码

int find(int x){    //寻找根结点
    if(par[x] == x)    //如果找到的根结点是自己的话,那么就表示找到了最终的根结点。
        return x;
    else
        return par[x] = find(par[x]);    //如果不是就进行递归,往上找;同时将par[X]即当前点的根结点上移,直到上移成最终的根结点,就完成了所谓的路径压缩。
}

  下面是合并方面,直接从图5观察,如果出现不同高度的树,又要对其进行合并,那我们应该怎么操作呢?

图5

  这时,我们会将矮树,移到高树下面,这种思路被称作启发式策略——按秩合并,合并效果如图6所示。

图6
图6

这种优化的好处是,使得寻找最终根结点迭代次数减少,优化代码如下。

void unite(int x, int y){    //合并元素
    xx = find(x);    //查找点X根结点
    yy = find(y);    //查找点Y根结点
    if(xx == yy)    //如果两者寻得结点相同,即已归于同一集合当中
        return;
    if(ran[xx] < ran[yy])    //矮树归并到高树之下
        par[xx] = yy;    //归并
    else{
        par[yy] = xx;    //同理,矮树归并到高树之下
        if(ran[xx] == ran[yy]) //若两树高度相同,将Y合并进XX后,XX的树高加1
            ran[xx]++;
    }
    return;
}

  这里新增了一个数组用来存储每个结点所在的树高,对应的在初始化时,要将数组ran全部赋值为0,默认大小与根数组par一致。

  好了,这就是全部的理解了,最后我将贴出例题HDU1232通畅工程的AC代码。

AC代码

#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
const int MAX_N (1010);
int par[MAX_N];
int ran[MAX_N];

void init(int n){
    for(int i = 1; i <= n; i++){
        par[i] = i;    //初始化par将所有元素的根结点设置为自己
        ran[i] = 0;    //初始化ran赋值为零,用于秩优化
    }
}

int find(int x){
    if(par[x] == x){
        return x;
    }
    else
        return par[x] = find(par[x]);
//    return par[x] == x ? x : par[x] = find(par[x]);   //简化写法,随意。
}

//int find (int x)  //用迭代的方式代替递归
//{
//    int root = x;
//    while (root != par[root])
//    {
//        root = par[root];
//    } //找到根结点
//    int y;
//    while (x != root) {  //压缩路径,从点X开始往上找值,并将其赋值为根结点的值。
//        y = par[x];
//        par[x] = root;
//        x = y;
//    }
//    return root;
//}

void unite(int x, int y){
    x = find(x);
    y = find(y);
    if(x == y)
        return;
    if(ran[x] < ran[y])
        par[x] = y;
    else{
        par[y] = x;
        if(ran[x] == ran[y])
            ran[x]++;
    }
    return;
}

bool same(int x, int y){
    return find(x) == find(y);
}

int main(int argc, const char * argv[]) {
    int n,m;
    while(1){
        cin >> n;   //读入城镇数量
        if(n == 0)  //为0则退出
            break;
        cin >> m;   //读入道路数量
        init(n);    //初始化数组
        for(int i = 0; i < m; i++){ //读入,合并连通的城镇
            int c1,c2;
            cin >> c1 >> c2;    //读入
            unite(c1, c2);  //合并
        }
        int t[MAX_N];   //用于存放每个集合的最终根结点
        memset(t, 0, sizeof(t));
        for(int i = 1; i <= n; i++){
            t[find(i)] = 1;     //寻找最终根结点
        }
        int ans = 0;
        for(int i = 1; i <= n; i++){    //统计个数
            if(t[i] == 1)
                ans++;
        }
        cout << ans-1 << endl;      //注意最后要减1
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/li13168690086/article/details/81316187
今日推荐