并查集

2023/04/01 数据结构与算法 共 2762 字,约 8 分钟

上机实践报告

一、目的

1.熟悉算法设计的基本思想

2.熟悉并查集

二、内容与设计思想

  1. 解决OJ上的练习题。

  2. 简单描述优化复杂度的方案,比如按秩合并,路径压缩。

*注1:练习题只有正确性测试点,请至少列出不同实现的代码的时间(多次求平均)。

*注2:可以自己设计测试方式和数据进行比较。

*注3:可以自己额外找编程问题来进行比较。

有$N$个城镇,现有城镇道路统计表,表中列出了每条道路直接连通的城镇。目标是使任何两个城镇间都可以连通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

每个测试点都有多组输入,对于每组输入:

第一行输入两个正整数$N(1 <= N<= 20000)$和$M(1<= M <= 20000)$, $N$表示城镇个数,$M$表示道路个数。

接下来$M$行,每行两个正整数$a,b ( 1<= a,b <= N)$,用两个城镇表示一条道路。

在所有输入的结尾,有一个0单独成行,表示输入文件的结束。

保证每个测试点的输入组数不超过50组。

三、使用环境

推荐使用C/C++集成编译环境。

练习地址:**[oj《算法设计与分析》第七次实验课](http://47.100.233.213/contest/96/problems)**(密码40088860)

截止时间:2021年11月28日 23:30

提交地址:算法设计与分析-实践报告7 (jianguoyun.com)

不建议随意改变本文档以上部分的结构,以下部分可以按需扩充。但不要缺少“实验过程”和“总结”。

四、实验过程

1.并查集最基础算法实现

源代码

#include <bits/stdc++.h>
using namespace std;
int bin[20000];
//构造并查集——一开始,我们先将它们的父节点设为自己。
void make_set(int n)
{
    for (int i = 1; i <= n; i++)
    {
        bin[i] = i;
    }
}
//寻找连接的城市——也就是查询树根——我们用递归的写法实现对代表元素的查询:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。
int Find(int x)
{
    if (x == bin[x])
    {
        return x;
    }
    else
    {
        return Find(bin[x]);
    }
}
//将两点(城市)连接在一起——合并操作也是很简单的,先找到两个集合的代表元素,然后将前者的父节点设为后者即可。当然也可以将后者的父节点设为前者
void UNION(int x, int y)
{
    bin[Find(x)] = Find(y);
}
int main()
{
    int n, m;
    //int bin[n];
    cin >> n;
    while (n > 0)
    {
        cin >> m;
        make_set(n);
        int x, y;
        for (int i = 1; i <= m; i++)
        {
            cin >> x >> y;
            UNION(x, y);
        }
        int counts = 0;
        for (int i = 1; i <= n; i++)
        {
            if (Find(i) == i)
                counts++;
        }
        cout << counts - 1 << endl;
        cin >> n;
    }
    return 0;
}

2.用路径压缩进行优化

在基础算法的基础上需要修改的只有Find函数

//改进就是在找的过程中把每一个节点的父节点都设置为根节点,这样下一次再查询的时候会省去很多事情
int Find(int x)
{
    if(x ==bin[x])
    {
        return x;
    }
    else
    {
        return bin[x]=Find(bin[x]);
    }
}

3.用按秩合并进行优化

在基础算法的基础上需要修改的有make_set函数和union函数

void make_set(int n)
{
    for (int i = 1; i <= n; i++)
    {
        bin[i] = i;
        Rank[i]=1;//一开始将每个点的rank设为1
    }
}
void UNION(int i, int j)
{
    int x = Find(i), y = Find(j);    //先找到两个根节点
    if (Rank[x] <= Rank[y])
        bin[x] = y;
    else
        bin[y] = x;
    if (Rank[x] == Rank[y] && x != y)
        Rank[y]++;                   //如果深度相同且根节点不同,则新的根节点的深度+1
}

4.同时使用路径压缩和按秩合并

就是find, make_set,union函数都修改,修改如上就不再赘述了

5.画图比较几种算法的运行时间

ID普通算法路径压缩优化按秩合并优化路径压缩+按秩合并
数据119ms20ms22ms9ms
数据21455ms30ms31ms20ms

6.对并查集的进一步运用

运用1-亲戚问题

这道题就可以运用并查集来解,我们可以把每个人分到不相交的集合,每个集合里的人彼此是亲戚,判断两个人是否是亲戚,即看他们是否属于一个集合即可

与道路问题不同的地方在于只用改其主函数即可

int main()
{
    int n, m, p;
    int x,y;
    cin>>n>>m>>p;
    make_set(n);
    for (int i = 0; i < m; ++i)
    {
        cin>>x>>y;
        union(x, y);
    }
    for (int i = 0; i < p; ++i)
    {
        cin>>x>>y;
        if(Find(x)==Find(y))
          cout<<"Yes";
        else
          cout<<"No";
    }
    return 0;
}

运用二-奶酪问题

思路::经过思考后我发现其实这道题也可以用并查集来实现,把所有空洞划分为若干个集合,当两个空洞相交或相切,就把它们放到同一个集合中,也就是union一下。然后再划出2个特殊元素,分别表示底部和顶部,如果一个空洞与底部接触,则把它与表示底部的元素放在同一个集合中,顶部同理。最后,只需要看顶部和底部是不是在同一个集合中即可。

五、总结

​ 关于本次实验,进一步加深了对并查集的理解,我们可以看到当对算法进行优化时,数据二的运行时间发生了明显的缩短。寻找根节点时,基本算法就是采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find(x)都是O(n)的复杂度。路径压缩就是说把沿途的每个节点的父节点都设为根节点即可,这样以后再次Find(x)时复杂度就变成O(1)了。按秩合并就是说使具有较少结点的树的根指向较多结点的树的根。路径压缩和按秩合并如果一起使用,对n个元素上的m个不相交集合操作,联合使用按秩合并与路径压缩启发式策略后的运行时间是O(mAlpha(n)),这里Alpha是Ackerman函数的某个反函数,在很大的范围内,这个函数的值可以看成是不大于4的,所以并查集的操作可以看作是线性的。

文档信息

Search

    Table of Contents