并查集的使用及其实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/The_best_man/article/details/62418823

并查集

概述

详细教程参考之前转载的并查集详解


性质

并查集算法(union_find sets)不支持分割一个集合,求连通子图、求最小生成树

用法

并查集是由一个数组pre[],和两个函数构成的,一个函数为find()函数,用于寻找前导点的,第二个函数是join()用于合并路线的

int find(int x)
{
    int r=x;
    while(pre[r]!=r)
    r=pre[r];//找到他的前导结点
    int i=x,j;
    while(i!=r)//路径压缩算法
    {
        j=pre[i];//记录x的前导结点
        pre[i]=r;//将i的前导结点设置为r根节点
        i=j;
    }
    return r;
}

路径压缩为了加快查找的速度,将x点与其根节点直接相连,构造成类似于只有叶子结点而没有分支结点的树


join()函数

void join(int x,int y)
{
    int a=find(x);//x的根节点为a
    int b=find(y);//y的根节点为b
    if(a!=b)//如果a,b不是相同的根节点,则说明ab不是连通的
    {
        pre[a]=b;//我们将ab相连 将a的前导结点设置为b
    }
}

初始化

我们将每一个结点的前导结点设置为自己,如果在join函数时未能形成连通,将独立成点

for(int i=0;i<n;i++)//n表示输入的结点的个数
{
    pre[i]=i;//将每一个结点的前导点设置为自己

}

用法

试题来自第八届蓝桥杯试题
第三次编辑这道题目
标题:风险度量

X星系的的防卫体系包含 n 个空间站。这 n 个空间站间有 m 条通信链路,构成通信网。
两个空间站间可能直接通信,也可能通过其它空间站中转。

对于两个站点x和y (x != y), 如果能找到一个站点z,使得:
当z被破坏后,x和y无法通信,则称z为关于x,y的关键站点。

显然,对于给定的两个站点,关于它们的关键点的个数越多,通信风险越大。

你的任务是:已知网络结构,求两站点之间的通信风险度,即:它们之间的关键点的个数。

输入数据第一行包含2个整数n(2 <= n <= 1000), m(0 <= m <= 2000),分别代表站点数,链路数。
空间站的编号从1到n。通信链路用其两端的站点编号表示。
接下来m行,每行两个整数 u,v (1 <= u, v <= n; u != v)代表一条链路。
最后1行,两个数u,v,代表被询问通信风险度的两个站点。

输出:一个整数,如果询问的两点不连通则输出-1.

例如:
用户输入:
7 6
1 3
2 3
3 4
3 5
4 5
5 6
1 6
应该输出:
2

我的错误

在进行分析的时候,我考虑了去边去点,去点的话,首先逐个去掉除了询问的点以外的点,同时去点的同时我们同样需要去掉与该点之间关联的边,查找与该点关联的边需要从整个数据中寻找数据量太大,一定会超时,还是需要考虑去边的办法,

//并查集
#include<iostream>
using namespace std;
int pre[1005];//每个点的前导点
int route[2005][2];
//可以配对的路线
int sum = 0;
//符合条件的 即关键点的数量

//查找
int find(int x)
{
    int r = x;
    while (pre[r] != r)
        r = pre[r];
    int i = x, j;
    while (i != r)//路径压缩算法
    {
        j = pre[i];//在改变他的前导点时,存储他的值
        pre[i] = r;
        i = j;//改变他的前导点为根节点
    }
    return r;
}


void join(int x, int y)
//组合
{
    int fx = find(x), fy = find(y);//分别记录x,y的根节点
    if (fx != fy)//如果他们的根节点相同,则说明他们不是连通图
        pre[fx] = fy;//将x的根结点 同 相连接
}

int main()
{
    int n, m;
    cin >> n>>m;//n表示站点的个数,m表示链路的个数

    for (int i = 0; i < m; i++)
    {
        cin >> route[i][0] >> route[i][1];
        join(route[i][0], route[i][1]);//将数据相互连接
    }


    int q1,q2;//待询问的两个点
    cin >> q1 >> q2;


    for (int ii = 0; ii < n; ii++)pre[ii] = ii;
    for (int j = 0; j < m; j++)
    {

            join(route[j][0], route[j][1]);
    }
    int a = find(q1);
    int b = find(q2);
    //如果边全部存在时不可达,则输出 -1;
    if (a != b)
    {
        cout << "-1" << endl;
    }

    else
    {
        for (int i = 1; i <= n; i++)
//枚举每一个点
        {
            if (i == q1 || i == q2)continue;
//如果是被询问的点,跳过,无需遍历   此处是最关键的部分
            for (int j = 1; j <= n; j++)pre[j] = j;
//将每一个初始化

            for (int j = 0; j < m; j++)
            {
                if (route[j][0] == i || route[j][1]==i)continue;
//去除当前点互相关联的边   解决问题的关键
                int a = find(route[j][0]);
                int b = find(route[j][1]);
                if (a > b) { a ^= b; b ^= a; a ^= b; };//交换
                if (a != b)pre[b] = a;
//以较小的点作为父节点
            }
            int a = find(q1);
            int b = find(q2);
            if (a != b)sum++;
            }
            cout<<sum<<endl;
        }
        return 0;
}

看到网上好多人在写并查集时,使用while(~sacnf("%d",&a)) scanf()函数的返回值是正确获得变量的个数
~scanf()函数就是没有得到正确的输入,总体上讲如果有正确结果输入,就退出循环,如果没有正确输入,就执行循环
看似没有什么区别,其实这种while()循环更加安全,保证不会因为非法的数字的输入执行程序的使用


测试数据

按照我之前对于数据的统计,现在给大家提供几组数据

  1. 4 0
    1 2
    测试结果 -1
    1. 4 3
      2 3
      3 4
      2 4
      1 4
      测试结果 -1
    2. 3 2
      1 2
      2 3
      1 3
      测试结果 1
    3. 使用题目的数据 以及评论区的那个数据

测试数据的分析,对于第一组测试数据,有且仅有四个点 ,测试程序是否会进行连通性判断;第二组数据 1 是独立点 234是三个连通分量,程序需要判断是否1与其他的点能构成连通图;第三组数据,我们测试一条直线,所有的关键点全部在该直线上,判断程序记录的到底是关键点的个数还是边的个数;第四组我们测试任意情况下对于数据的处理我们可以打开画板,对我们的数据进行验证即可


反向并查集

题目


来自蓝桥杯系统历届试题库中的试题

问题描述
  C国由n个小岛组成,为了方便小岛之间联络,C国在小岛间建立了m座大桥,每座大桥连接两座小岛。两个小岛间可能存在多座桥连接。然而,由于海水冲刷,有一些大桥面临着不能使用的危险。

  如果两个小岛间的所有大桥都不能使用,则这两座小岛就不能直接到达了。然而,只要这两座小岛的居民能通过其他的桥或者其他的小岛互相到达,他们就会安然无事。但是,如果前一天两个小岛之间还有方法可以到达,后一天却不能到达了,居民们就会一起抗议。

  现在C国的国王已经知道了每座桥能使用的天数,超过这个天数就不能使用了。现在他想知道居民们会有多少天进行抗议。
输入格式
  输入的第一行包含两个整数n, m,分别表示小岛的个数和桥的数量。
  接下来m行,每行三个整数a, b, t,分别表示该座桥连接a号和b号两个小岛,能使用t天。小岛的编号从1开始递增。
输出格式
  输出一个整数,表示居民们会抗议的天数。
样例输入
4 4
1 2 2
1 3 2
2 3 1
3 4 3
样例输出
2
样例说明
  第一天后2和3之间的桥不能使用,不影响。
  第二天后1和2之间,以及1和3之间的桥不能使用,居民们会抗议。
  第三天后3和4之间的桥不能使用,居民们会抗议。
数据规模和约定
  对于30%的数据,1<=n<=20,1<=m<=100;
  对于50%的数据,1<=n<=500,1<=m<=10000;
  对于100%的0<=n<=10000,1<=m<=10000。
  ,1<=a, b<=n, 1<=t<=100000。

#include<iostream>
#include<algorithm>
using namespace std;
struct node
{
    int x, y, d;//d表示剩余的时间  x,y分别表示桥的两端的端点
}bridge[10005];//创建桥的数量
int pre[10005];//前导结点的个数

bool cmp(node a, node b)//时间比较   如果第一个参数大于第二个参数,则返回true
{
    return a.d>b.d;
}

int find(int x)//查找根节点
{
    int r=x;
    while (pre[r] != r)
        r = pre[r];

    //尝试尝试路径压缩算法

    return r;
}
bool join(int x, int y)//合并链路
{
    int fx = find(x);
    int fy = find(y);
    if (fx != fy)
    {
        pre[fx] = fy;
        return 1;//没有桥,我们将直接构造形成桥
    }
    return 0;//有桥无需构造
}
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        cin >> bridge[i].x >> bridge[i].y >> bridge[i].d;//输入桥头两侧 使用天数
    }
    //
    for (int i = 0; i < n; i++)
    {
        pre[i] = i;//初始化每个小岛,使其独立
    }


    //按照使用时间排序进行整合
    sort(bridge, bridge + m, cmp);//天数从大到小排列
    int fight = 0;//表示反抗的日子
    int pre = -1;//
    for (int i = 0; i < m; i++)//此时的时间已经是从大到小的排序
    {
        int way = join(bridge[i].x, bridge[i].y);//从时间从大到小重新构造桥
        if (way == 1 && bridge[i].d != pre)//如果系统构造的桥并且天数不等于-1
        {
            fight++;
            pre = bridge[i].d;
        }
    }
    cout << fight << endl;

    return 0;
}

这是我的代码在通过系统的时候由于超出限制时间,只有40%的分数,其中由于一次需要输入三个变量,并且我们在之后的操作中需要对时间进行排序,所以我们采取结构体命名变量我们建时间按照从大到小的顺序排列,将每个岛屿全部独立分开重新构建来连通图,我们将按照时间天数优先,针对测试样例,以及之前的顺序优先准则来重新构建,从第一条边开始,如果不是连通图,就呼叫一次fight++,直到所有的结点全部构成连通图,结束执行,无论后面还有多少条未加入的边


未完待续

阅读更多

没有更多推荐了,返回首页