【夯实算法基础】 并查集

简介: 【夯实算法基础】 并查集

@[toc]

AcWing 1250. 格子游戏

:sailboat: 思路

什么时候会围成一个封闭的圈呢,可以这样想:如果我们要连的那一条边已经连起来了,那么我在连一次不就成了一个圈了吗。可以使用并查集将连起来的点都整合到一个集合当中去,这样就可以很快的查询两个点是不是同属于一个集合了。

:cactus:代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 1e6 + 10, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
int p[N];
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
// 这种hash操作只适用于行和列相等的情况
// 如果行和列不等,可以使用给每个点设置一个标记
int get(int x, int y)
{
    return x * n + y;
}
void solve()
{
    cin >> n >> m;
    // 最多有 n * n + n 个
    for (int i = 1; i <= n * n + n; i++)
        p[i] = i;
    for (int i = 1; i <= m; i++)
    {
        int x, y;
        char c;
        cin >> x >> y >> c;
        int a = get(x, y);
        if (c == 'D')
            x++;
        else
            y++;
        int b = get(x, y);
        int pa = find(a), pb = find(b);
        if (pa == pb)
        {
            cout << i;
            return;
        }
        else
            p[pa] = pb;
    }
    cout << "draw";
    return;
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    while (T--)
        solve();
    return 0;
}

AcWing 1252. 搭配购买

:banana:思路 :并查集+01背包

将捆绑销售的一些物品整合成一个物品,这一步用并查集来实现每个物品的价钱和价值总和。

现在问题就变成了有w块钱可以选择买一些物品并实现价值最大,很容易就想到了01背包。(关于背包问题可以参考我的这篇博客:背包问题)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pb push_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 1e6 + 10, inf = 0x3f3f3f3f, mod = 998244353;
int n, m, V;
int v[N]; // 价钱
int w[N]; // 价值
int f[N]; //花i钱能买到花的最大价值
int p[N]; //并查集数组
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
void solve()
{
    cin >> n >> m >> V;
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
        p[i] = i;
    }
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        a = find(a);
        b = find(b);
        if (a != b)
        {
            p[a] = b;
            v[b] += v[a];
            w[b] += w[a];
        }
    }
    for (int i = 1; i <= n; i++)
    {
        if (p[i] != i)
            continue;
        for (int j = V; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    }
    cout << f[V];
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    while (T--)
        solve();
    return 0;
}

AcWing 237. 程序自动分析

:lantern: 思路

先考虑所有相等的条件,如果两个元素相等,那么就将它俩放到同一个集合中去,在考虑所有不等的条件,如果两个元素在同一个集合中,说明它俩之前是相等的,这样就产生了矛盾。

这题数据范围比较大,所以我们需要对元素进行离散化,离散化方式有两种:

  1. 保序:排序+判重+二分
  2. 不保序:map / hash

:kiss:代码1(保序)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pb push_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 1e6 + 10, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
int p[N];
vector<PII> v1, v2;
vector<int> v;
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
void merage(int a, int b)
{
    p[find(a)] = find(b);
}
int get(int x)
{
    return lower_bound(all(v), x) - v.begin() + 1;
}
void solve()
{
    v1.clear();
    v2.clear();
    v.clear();
    cin >> m;
    while (m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        v.pb(a);
        v.pb(b);
        if (c == 1)
            v1.pb({a, b});
        else
            v2.pb({a, b});
    }
    sort(all(v));
    v.erase(unique(all(v)), v.end());
    int n = v.size();
    for (int i = 1; i <= n; i++)
        p[i] = i;
    for (int i = 0; i < v1.size(); i++)
    {
        int a = get(v1[i].x);
        int b = get(v1[i].y);
        merage(a, b);
    }
    bool ok = true;
    for (int i = 0; i < v2.size(); i++)
    {
        int a = get(v2[i].x);
        int b = get(v2[i].y);
        if (find(a) == find(b))
        {
            ok = false;
            break;
        }
    }
    if (ok)
        YES;
    else
        NO;
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

:kissing: 代码2(不保序)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
const int N = 1e5 + 10;
unordered_map<int, int> f;
int find(int x)
{
    if (f[x] != x)
        f[x] = find(f[x]);
    return f[x];
}
signed main()
{
    // freopen("in.in", "r", stdin);
    // freopen("out.out", "w", stdout);
    int t;
    cin >> t;
    while (t--)
    {
        f.clear();
        int n;
        cin >> n;
        vector<PII> v;
        int k = 1;
        while (n--)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            if (f.count(a) == 0)
            {
                f[a] = a;
            }
            if (f.count(b) == 0)
            {
                f[b] = b;
            }
            int l = find(a);
            int r = find(b);
            if (c == 1)
            {
                f[l] = r;
            }
            else
            {
                v.push_back({l, r});
            }
        }
        for (int i = 0; i < v.size(); i++)
        {
            int l = v[i].x;
            int r = v[i].y;
            l = find(l);
            r = find(r);
            if (l == r)
            {
                k = 0;
                break;
            }
        }

        if (k)
            puts("YES");
        else
            puts("NO");
    }
    return 0;
}

AcWing 238. 银河英雄传说

:camel: 思路

并查集可以用于维护具有传递性关系的作用,每个集合的大小,绑定到根节点上,每个点到根节点的距离,绑定到每个元素的节点上。

我们直接用并查集就可以查询两个点是不是在一个队列中,但是要记录每个点之间的距离就不好求了。求两个点之间的距离我们可以用到前缀和的思想。

d[x] 表示每个点到根节点的距离

size[x]表示每个集合的节点个数

p[x]表示每个点的祖先节点是谁

我们从一般出发,先来看合并两个点的情况:

image-20220807100243923

接下来我们再看合并两个集合大小为2的集合的情况:

image-20220807102017572

通过这两个例子我们可以发现一些性质,当我们要合并a,b两个集合时

需要三步:

  1. d[p[a]] = size[p[b]] :a的根节点到b的根节点的距离更新
  2. size[p[b]] += size[p[a]] :合并后每个集合的大小要更新
  3. p[p[a]] = p[p[b]] :更新祖先节点,每一次合并都要更新

求每个点到根节点的距离时在路径压缩中完成的,也需三步:

  1. root = find(p[x]):比如上图中的3号点的根节点1号点
  2. d[x] += d[p[x]]:一个点到它的根节点的距离加上它的根节点到根节点的根节点的距离。比如上图中的4号点
  3. p[x] = root , 更新祖先节点

:horse: 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 3e4 + 10, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
int p[N], s[N], d[N]; // d是每个点到p[x]的距离
int find(int x)
{
    if (p[x] != x)
    {
        int root = find(p[x]);
        d[x] += d[p[x]];
        p[x] = root;
    }
    return p[x];
}
void solve()
{
    cin >> m;
    for (int i = 1; i < N; i++)
    {
        p[i] = i;
        s[i] = 1;
    }
    while (m--)
    {
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        int pa = find(a), pb = find(b);
        if (op[0] == 'M' && pa != pb)
        {
            d[pa] = s[pb];
            s[pb] += s[pa];
            p[pa] = pb;
        }
        if (op[0] == 'C')
        {
            if (pa != pb)
                cout << -1 << endl;
            else
                cout << max(0, abs(d[a] - d[b]) - 1) << endl;
        }
    }
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    while (T--)
        solve();
    return 0;
}

AcWing 239. 奇偶游戏

:pager: 思路1:边带权

img

img

:kaaba: 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 1e5 + 10, M = N / 2, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
int p[N];
int d[N]; // 0 代表相同,1代表不同
unordered_map<int, int> ma;
int get(int x)
{
    if (ma.count(x) == 0)
        ma[x] = ++n;
    return ma[x];
}
int find(int x)
{
    if (p[x] != x)
    {
        int root = find(p[x]);
        d[x] ^= d[p[x]];
        p[x] = root;
    }
    return p[x];
}
void solve()
{
    cin >> n >> m;
    n = 0;
    for (int i = 1; i < N; i++)
        p[i] = i;
    int ans = m;
    for (int i = 1; i <= m; i++)
    {
        int a, b;
        string s;
        cin >> a >> b >> s;
        a = get(a - 1);
        b = get(b);
        int pa = find(a), pb = find(b);
        int t = 0;
        if (s == "odd")
            t = 1;
        if (pa == pb)
        {
            if (d[a] ^ d[b] != t)
            {
                ans = i - 1;
                break;
            }
        }
        else
        {
            p[pa] = pb;
            d[pa] = t ^ d[a] ^ d[b];
        }
    }
    cout << ans;
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    while (T--)
        solve();
    return 0;
}

:dagger: 思路2:扩展域

img

img

:fallen_leaf: 代码2

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 1e5 + 10, M = N / 2, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
int p[N];
unordered_map<int, int> ma;
int get(int x)
{
    if (ma.count(x) == 0)
        ma[x] = ++n;
    return ma[x];
}
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
void solve()
{
    cin >> n >> m;
    n = 0;
    for (int i = 1; i < N; i++)
        p[i] = i;
    int ans = m;
    for (int i = 1; i <= m; i++)
    {
        int a, b;
        string s;
        cin >> a >> b >> s;
        a = get(a - 1);
        b = get(b);
        if (s == "even")
        {
            if (find(a) == find(b + M))
            {
                ans = i - 1;
                break;
            }
            p[find(a)] = find(b);
            p[find(a + M)] = find(b + M);
        }
        else
        {
            if (find(a) == find(b))
            {
                ans = i - 1;
                break;
            }
            p[find(a)] = find(b + M);
            p[find(b)] = find(a + M);
        }
    }
    cout << ans;
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    while (T--)
        solve();
    return 0;
}

AcWing 240. 食物链

:ice_cream: 思路:边带权

d[x] 表示x到它父亲的距离,以来表示几个点之间的关系

d[x] % 3 == 0,表示它和父节点是同类

d[x] % 3 == 1表示它和可以吃父节点

d[x] % 3 == 2表示可以被父节点吃

这样我们就可以用点到父节点的距离来表示关系,所有的距离都以根节点为基础。

如果x和y是同类关系,但此时不在同一个集合中,那么

p[find(a)] = find(b)
(d[x] + d[p[x]]) % 3 == d[y] % 3

由于p[x]的父节点发生了变化,那么它到父节点的距离也要变化:

d[p[x]] = (d[y] - d[x]) % 3

如果x吃y,而且它俩不在同一个集合中,那么

p[find(a)] = find(b)
(d[x] + d[p[x]]) % 3 == (d[y] + 1) % 3

d[p[x]] = (d[y] - d[x] + 1) % 3

我们要在find函数中既要更新父节点是谁,还要更新d[x]

我们知道d[x]等于它到父节点的距离,此时父节点已经发生了变化,那么d[x]就等于它到父节点的距离加上父节点到它的父节点的距离,然后在更新x的父节点(此时已经是根结点)了。

引用文章:(AcWing 240. 食物链---数组d的真正含义以及find()函数调用过程)[https://www.acwing.com/solution/content/15938/]

img

:game_die: 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
typedef priority_queue<int, vector<int>, less<int>> Q;
#define x first
#define y second
#define endl '\n'
#define ppb pop_back
#define pf push_front
#define YES cout << "YES" << endl
#define Yes cout << "Yes" << endl
#define yes cout << "yes" << endl
#define NO cout << "NO" << endl
#define No cout << "No" << endl
#define no cout << "no" << endl
#define all(x) x.begin(), x.end()
#define rall(x) x.rbegin(), x.rend()
#define mset(x, a) memset(x, a, sizeof(x))
#define rep(i, l, r) for (LL i = l; i <= (r); ++i)
#define per(i, r, l) for (LL i = r; i >= (l); --i)
const int N = 1e5 + 10, inf = 0x3f3f3f3f, mod = 998244353;
int p[N];
int n, m;
int d[N];
int ans;
int find(int x)
{
    if (p[x] != x)
    {
        int root = find(p[x]);
        d[x] += d[p[x]];
        p[x] = root;
    }
    return p[x];
}
void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        p[i] = i;
    for (int i = 1; i <= m; i++)
    {
        int op, x, y;
        cin >> op >> x >> y;
        // 假话
        if (x > n || y > n)
        {
            ans++;
            continue;
        }
        if (op == 1)
        {
            int pa = find(x), pb = find(y);
            if (pa == pb)
            {
                // 如果它俩相差求余3结果不同,表示不是同类关系
                if ((d[x] - d[y] + 3) % 3)
                {
                    ans++;
                    continue;
                }
            }
            else
            {
                p[pa] = pb;
                d[pa] = (d[y] - d[x] + 3) % 3;
            }
        }
        else
        {
            if (x == y)
            {
                ans++;
                continue;
            }
            int pa = find(x), pb = find(y);
            if (pa == pb)
            {
                // 如果它俩求余结果不是差1的关系,表示不是x吃y的关系
                if ((d[x] - d[y] - 1) % 3)
                {
                    ans++;
                    continue;
                }
            }
            else
            {
                p[pa] = pb;
                d[pa] = (d[y] - d[x] + 1) % 3;
            }
        }
    }
    cout << ans;
}
signed main()
{
#ifdef Xin
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int T = 1;
    while (T--)
        solve();
    return 0;
}
刚开始学基础课的时候这点怎么也不能理解,后来在学的时候觉得醍醐灌顶,内心想当时为啥学不会呢,这就是一个学习的过程吧。
相关文章
|
6月前
|
机器学习/深度学习 存储 算法
【算法沉淀】刷题笔记:并查集 带权并查集+实战讲解
【算法沉淀】刷题笔记:并查集 带权并查集+实战讲解
|
6月前
|
算法
并查集的实现【学习算法】
并查集的实现【学习算法】
41 0
|
2月前
|
算法 JavaScript 前端开发
第一个算法项目 | JS实现并查集迷宫算法Demo学习
本文是关于使用JavaScript实现并查集迷宫算法的中国象棋demo的学习记录,包括项目运行方法、知识点梳理、代码赏析以及相关CSS样式表文件的介绍。
第一个算法项目 | JS实现并查集迷宫算法Demo学习
|
1月前
|
存储 算法 Python
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
35 1
|
2月前
|
算法 计算机视觉 Python
Python并查集大揭秘:让你在算法界呼风唤雨,秒杀一切复杂场景!
在编程与算法的广袤天地中,总有一些工具如同神兵利器,能够助你一臂之力,在复杂的问题前游刃有余。今天,我们就来深入探讨这样一件神器——Python并查集(Union-Find),看看它是如何让你在算法界呼风唤雨,轻松应对各种复杂场景的。
64 2
|
4月前
|
算法 计算机视觉 Python
Python并查集大揭秘:让你在算法界呼风唤雨,秒杀一切复杂场景!
【7月更文挑战第18天】并查集是Python中解决集合动态合并与查询的利器,常用于复杂问题。例如,在社交网络中快速判断用户是否在同一朋友圈,通过路径压缩优化的`UnionFind`类实现。另外,计算图像中岛屿数量也可借助并查集,将相邻像素合并成集合。并查集的应用显示了其在算法中的高效和灵活性,是提升编程技能的关键工具。
46 2
|
4月前
|
存储 算法 Python
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
【7月更文挑战第17天】并查集,高效解决集合合并查询问题,常用于图的连通性判断。Python实现关键包含查找和合并操作。初始化时,元素各自为集合。查找使用路径压缩优化,合并则可选按秩策略保持平衡。例如,检测无向图环路,遍历边,若并查集发现边两端已在同一集合,则存在环。掌握并查集,提升算法能力,助你在问题解决中一飞冲天!动手实践,成为算法达人!
57 2
|
6月前
|
算法 C++
c++算法学习笔记 (16) 并查集
c++算法学习笔记 (16) 并查集
|
5月前
|
算法
【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏
【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏
|
6月前
|
算法 测试技术
并查集算法
并查集算法