初学者最好的并查集入门教程!!!

并查集

1 概念及其应用

1.1 基础概念

并查集

一种用于处理集合合并和查找问题的数据结构,它支持两种基本操作:

1.查找(Find):确定某个元素属于哪个子集。

2.合并(Merge):将两个子集合并为一个集合。

1.2 常见应用

并查集主要用于求解图论中的连通性问题

比如 求两个元素是否在同一集合,求有多少个连通块。

利用这种特性,可以使用并查集来解决最小生成树问题最近公共祖先问题等。 例如, 在最小生成树问题中,可以使用并查集来判断两个节点是否在同一个连通块中,从而避免形成环路; 在最近公共祖先问题中,可以使用并查集来维护每个节点的祖先节点,从而快速找到两个节点的最近公共祖先。

2 代码实现

2.1引入

2.1.1 实现方法
  • 用编号最小的元素标记所在集合;

  • 定义一个数组Set[1..n],其中Set[i]表示i所在的集合;

效率分析
find1(x)
{
    return Set[x];
}
//O(1)
​
Merge(a,b)
{
    i = min(a,b);
    j = max(a,b);
    for(k = 1;k <= N;k ++)
    {
        if(Set[k] == j)
        {
            Set[k] = i;
        }
    }
}
// O(n)

缺点:合并操作必须搜索所有元素

2.1.2 实现方法
  • 每个集合用一颗“有根树”表示

  • 定义数组Set[1..n]

    • Set[i] = i,则表示本集合,并是集合对应树的根

    • Set[i] = j,若i != j,则 j 是 i 的父节点

效率分析4
find2(x)
{
    r = x;
    while(Set[r] != r)
    {
        r = Set[r];
    }
    return r;
}
// 最坏情况O(n)
// 一般情况...O(logn)
​
merge2(a,b)
{
    Set[a] = b;
}
// O(1)
2.1.3为了避免2.1.2的查找的最坏情况
  • 方法:将深度小的数合并到深度最大的树

  • 实现:假设两棵树的深度分别是h_1和h_2,则合并后的树高h是:

    • max(h_1,h_2), if h_1<>h_2

    • h_1 + 1, if h_1= h_2

  • 效果:任意顺序的合并操作以后,包括k个节点的树的最大高度不超过[lgk]

find2(x)
{
    r = x;
    while(Set[r] != r)
    {
        r = Set[r];
    }
    return r;
}
// 最坏O(logn)
​
merge3(a,b)
{
    if(height(a) == height(b))
    {
        height[a] ++;
        Set[b] = a;
    }
    else if(height(a) < height(b))
    {
        Set[a] = b;
    }
    else
    {
        height[b] = a;
    }
}
// O(1)

*2.2 实际使用(路径压缩)

上面的都不重要,实际使用只需使用带路径压缩的并查集即可。

  • 具体方案:

1)找到根节点

2)修改查找路径上的是所有节点,将他们都指向根节点

  • 代码如下:

find3(x)
{
    if(Set[x] != x)
    {
        Set[x] = find3(Set[x]); // 递归
    }
    return Set[x];
}
// 路径压缩的查找操作

find3(x){return Set[x] = (Set[x] == x ? x : find3(Set[x]));}

3 题目练习

3.1 P68 联通块问题(0)【模板】

题意:给定无向图,有n个点,m条边(无重边自环),求图中所有联通块的大小。

思路:连通块问题,想到并查集来存储。那么如何计算每个连通块大小呢?用来计数,再存入vector中排序输出。

具体代码实现如下:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 9;
ll pre[N]; // 上一节点
ll c[N]; // 桶 用来计数(大小)
vector<int> v; // 存储各个连通块的大小,且方便排序,以从小到大输出
int find(int x) // 查找
{
    return pre[x] = (pre[x] == x ? x : find(pre[x]));
}
void merge(int a,int b) // 合并
{
    pre[find(a)] = find(b);
}
​
void solve()
{
    int n,m;cin >> n >> m;
    for(int i = 1;i <= n;i ++) pre[i] = i; // 预处理
    for(int i = 1;i <= m;i ++) 
    {
        int x,y;cin >> x >> y;
        merge(x,y);
    }
    for(int i = 1;i <= n;i ++)
    {
        c[find(i)] ++; // 根节点相同即在同一个连通块,大小++
    }
    
    for(int i = 1;i <= n;i ++)
    {
        if(c[i]) v.push_back(c[i]); // 如果>0,说明有以此为根的连通块
    }
    sort(v.begin(),v.end()); // 排序
    for(auto &i :v) cout << i << ' ';
}
int main(void)
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _ = 1;// cin >> _;
    while(_ --)
    {
        solve();
    }
    return 0;
}

3.2 F. Microcycle

题意:给定一个无向加权图,图中有 n个顶点和 m条边。

图中每对顶点之间最多有一条边,图中不包含循环(从顶点到自身的边)。该图不一定连通。

找出该图中最轻边的权重最小的简单循环(如果图中的循环不经过同一顶点两次,也不包含相同的边两次,则称为简单循环)。

思路并查集判环 + dfs查找最小的边权

2种做法:targan / 并查集 ==> 判环

这里采用并查集做法

枚举边 i ,对应2个点u , v。find(u) == find(v) 即表示已经在集合中了,加入边i那么会形成一个环。

题目要求我们最小的边权尽可能小,因此:

1)边权从大到小排序

2)枚举边i , 这保证了边权w[i]是当前的并查集中所有边权里面最小

加入第i条边之后,有环,当前边权可能作为答案,枚举直至结束

mn表示最小的在环里的边权

id表示相应mn的编号

mn=>id id=>u , v

建图,去掉第id条边,从起点u-->终点v的路径

具体实现请看代码注释,如下:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 并查集判环 + dfs找最小的边权
ll n,m,root[200005],vis[200005]; // 分别是节点数 边数 根 判断是否搜索过
struct edge{ll u,v,w;}a[200005]; // 存边的信息
vector<ll> e[200005],ans; // 建图 和 路径
​
ll find(ll u) // 并查集查找根节点(路径压缩)
{
    if(root[u] != u) root[u] = find(root[u]);
    return root[u];
}
ll merge(ll u,ll v) // 并查集合并节点 并返回判断是否可成环
{
    u = find(u);v = find(v);
    if(u == v) return 1;
    root[u] = v;
    return 0;
}
void dfs(ll u,ll tag) // u表示节点 tag即目标taget
{
    vis[u] = 1; // 记录搜索过的节点
    ans.push_back(u); // 记录路径
    if(u == tag) // 如果找到目标
    {
        cout << ans.size() << "\n"; // 输出路径大小
        for(ll &x :ans) cout << x << " "; // 输出路径
        cout << "\n";
        return; // 终止递归
    }
    for(ll &v :e[u])
        if(!vis[v]) dfs(v,tag); // 若此节点未被搜索过继续搜索
    ans.pop_back(); // 向上回溯时 将存储在路径的节点删除
}
void solve()
{  
    // input ans pre_work
    cin >> n >> m; 
    ans.clear();
    for(ll i = 1;i <= n;++ i) root[i] = i,e[i].clear(),vis[i] = 0;
    for(ll i = 1;i <= m;++ i)
    {
        ll u,v,w;cin >> u >> v >> w;
        a[i] = {u,v,w};
    }
    // 按边权从大到小排序 保证最后得到的边权是最小的
    sort(a + 1,a + m + 1,[](edge x,edge y){return x.w > y.w;});
    ll mn = 1e9,id = 0; // mn表示最小边权 id表示相应编号
    for(ll i = 1;i <= m;++ i)
    {
        if(merge(a[i].u,a[i].v) && a[i].w < mn) // 如果此边的两节点已经在同一集合里 并且· 该边权更小
        {
            mn = a[i].w,id = i;
        }
    }
    // 建图
    for(ll i = 1;i <= m;++ i) 
    {
        if(i == id) continue; // 不存最小的边权的节点 不然路径会找错 抄近路啦
        ll u = a[i].u,v = a[i].v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    cout << mn << " "; // 输出找到的循环中最小的边权
    dfs(a[id].u,a[id].v);
}   
​
int main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T = 1;cin >> T;
    while(T --) solve();
    return 0;
}
3.3 P9 【模板】可撤销并查集

题解

最后提一嘴:这是b站up:Erik_Tse开发的编程学习网站中的题目。

该网站StarryCoding是面向计算机专业学生的综合学习与刷题平台。由于课程的准备、录制、平台(前后端和评测机)的开发、推广都是由up一人完成,所以成本压的非常非常低,算法基础课仅售39元,此外,up还推出了前端Web的课程,,每周还有多种比赛,希望大家来玩一玩。

官网:starrycoding点com

  • 26
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值