第二章 数据结构(2)

本文详细介绍了数据结构中的并查集,包括合并集合、查询元素所属集合、路径压缩和按秩合并等操作。接着讲解了树状数组的基本思想和应用,如单点修改和区间查询。此外,还探讨了线段树的建立、单点修改、区间查询等操作,并举例说明了线段树在解决区间最大值、最大公约数等问题中的应用。
摘要由CSDN通过智能技术生成

9 并查集

基本思想:

每一个集合用一个树来维护,每个集合的编号是根结点的编号,对于树的每个结点,都存储它的父结点的信息。当用于查找某个结点属于哪一个集合的时候,就用当前结点的父结点不断向上递归查找,直到找到根结点,根据根结点编号就可以知道它属于哪个节点。

用于快速处理:

  • 将两个集合合并;
  • 询问两个元素是否在一个集合中;
  • 并查集可以维护额外信息,详见 9.2 维护size的并查集, 9.3 维护到祖宗结点距离的并查集。

假设使用p[x]存储x结点的父结点。

三个问题:

  1. 如何判断树根?if (p[x] == x),除了根结点,其余结点p[x] != x
  2. 如何求 x的结合编号?while (p[x] != x) x = p[x]
  3. 如何合并两个集合?假设pxx的集合编号,pyy的集合编号,将x集合并入y集合:p[x] = y

并查集的两个优化

  • 路径压缩( O ( l o g n ) O(log^n) O(logn))
  • 按秩合并( O ( l o g n ) O(log^n) O(logn))。合并集合的时候,将树的深度较小的一个合并到树的深度较大的一个
  • 如果两个优化都做,并查集时间复杂度会降到 O ( α ( n ) ) O(\alpha (n)) O(α(n))

并查集的扩展

  • 记录每个集合的大小,大小数值绑定到集合根节点中
  • 每个点到根结点的距离,大小数值绑定到每个元素上。比如:9.7 奇偶游戏、9.8 食物链

核心操作

int find(int x) {
    // 返回x所在集合的编号 + 路径压缩 
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

并查集类模板

class UnionFind {
   
    private:
        vector<int> p;
        vector<int> cnt;
    
    public:
        UnionFind(int n) : p(n), cnt(n, 1) {
   
            iota(p.begin(), p.end(), 0);
        }

        int Find(int x) {
   
            if (x != p[x]) p[x] = Find(p[x]);
            return p[x];
        }

        void Union(int x, int y) {
   
            int px= Find(x), py = Find(y);
            if (px == py) return;
            if (cnt[px] > cnt[py]) p[py] = px, cnt[px] += cnt[py];
            else p[px] = py, cnt[py] += cnt[px];
        }

        int GetSize(int x) {
   
            return cnt[Find(x)];
        }
};

9.1 合并集合

ACWing 836

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int n, m;
int p[N];

int find(int x) {
   
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
   
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) p[i] = i;
    while (m--) {
   
        char op[2]; int a, b;
        scanf("%s%d%d", op, &a, &b);
        int pa = find(a), pb = find(b);
        if (*op == 'M') p[pa] = pb;
        else pa == pb ? puts("Yes") : puts("No");
    }
    return 0;
}

9.2 格子游戏

ACwing 1250

某一步会形成环,等价于对两个点加边之前,这两个点已经在同一个连通块中。

这个题就等价于:从前往后合并,直到合并到第一次出现环就停止合并。

为了方便,需要先将二维坐标转换为一维坐标:(x, y) → x * n + y。注意这里要求横纵坐标都是从0开始!

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 40010;

int n, m;
int p[N];

int get(int x, int y) {
   
    return x * n + y;
}

int find(int x) {
   
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
   
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n * n; i++) p[i] = i;
    for (int i = 1; i <= m; i++) {
   
        int x, y; char d[2]; scanf("%d%d%s", &x, &y, d);
        x--, y--;
        int a = get(x, y), b;
        if (*d == 'D') b = get(x + 1, y);
        else b = get(x, y + 1);
        int pa = find(a), pb = find(b);
        if (pa == pb) {
   
            printf("%d\n", i); return 0;
        }
        p[pa] = pb;
    }
    puts("draw");
    return 0;
}

9.3 程序自动分析

ACwing 237

离散化

  • 保序:排序、判重、二分,第一章《基础算法》中的离散化一节。
  • 不要求保序:直接map / hash表

对于这个题目:
因为约束条件的顺序不影响结果。可以先考虑所有的相等约束,这里不可能有任何矛盾,可以放进同一个集合中;在考虑所有不相等的条件,可能存在矛盾,即两个数已经在同一个集合中,且两个不相等,则矛盾。

这个题目的做法为:

  1. 先离散化
  2. 将所有的相等条件合并,放入一个并查集中
  3. 依次判断所有的不相等条件,判断每一个不相等条件的 x i 、 x j x_i、x_j xixj是否在同一个集合中,如果在,则矛盾,反之,则不矛盾。
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;

const int N = 2e5 + 10;

int n, m, p[N];
unordered_map<int, int> S;
struct Query {
   
    int x, y, e;
} query[N];

int get(int x) {
   
    if (!S.count(x)) S[x] = ++n;
    return S[x];
}

int find(int x) {
   
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
   
    int T; scanf("%d", &T);
    while (T--) {
   
        n = 0; S.clear(); scanf("%d", &m);
        for (int i = 0; i < m; i++) {
   
            int x, y, e; scanf("%d%d%d", &x, &y, &e);
            query[i] = {
   get(x), get(y), e};
        }
        for (int i = 1; i <= n; i++) p[i] = i;
        for (int i = 0; i < m; i++)
            if (query[i].e) 
                p[find(query[i].x)] = find(query[i].y);
        bool has_conflict = false;
        for (int i = 0; i < m; i++)
            if (!query[i].e)
                if (find(query[i].x) == find(query[i].y)) {
   
                    has_conflict = true; break;
                }
        if (has_conflict) puts("NO"); else puts("YES");
    }
    return 0;
} 

9.4 连通块中点的数量

ACWing 837

数组 c n t cnt cnt 表示每个集合的大小,保存于根结点中。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int n, m;
int p[N], cnt[N];

int find(int x) {
   
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
   
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) p[i] = i, cnt[i] = 1;
    while (m--) {
   
        char op[5]; int a, b;
        scanf("%s", op);
        if (*op == 'C') {
   
            scanf("%d%d", &a, &b);
            int pa = find(a), pb = find(b);
            if (pa == pb) continue;
            cnt[pb] += cnt[pa];
            p[pa] = pb;
        } else if (op[1] == '1') {
   
            scanf("%d%d", &a, &b);
            find(a) == find(b) ? puts("Yes") : puts("No");
        } else {
   
            scanf("%d", &a);
            printf("%d\n", cnt[find(a)]);
        }
    }
    return 0;
}

9.5 搭配购买

ACwing 1252

因为每一件物品需要将与其相关的所有物品全部购买,故将这些有关联的物品看成是一个连通块,只要购买连通块内部任一物品,那整个连通块都必须一起购买。所以将连通块看成是一个物品,价钱看成体积,价格看成价值,这就是一个01背包问题

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 10010;

int n, m, val; // val总共的钱
int v[N], w[N], f[N];
int p[N];

int find(int x) {
   
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
   
    cin >> n >> m >> val;
    for (int i = 1; i <= n; i++) p[i] = i;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    while (m--) {
   
        int a, b; cin >> a >> b;
        int pa = find(a), pb = find(b);
        if (pa != pb) 
            v[pb] += v[pa], w[pb] += w[pa], p[pa] = pb;
    }
    for (int i = 1; i <= n; i++)
        if (p[i] == i)
            for (int j = val; j >= v[i]; j--)
                f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[val] << endl;
    return 0;
}

9.6 银河英雄传说

ACwing 238

代码中find函数的图解:
在这里插入图片描述

代码中:

  • d [ N ] d[N] d[N]:每个点到当前并查集根结点的距离
  • s i z e _ p [ N ] size\_p[N] size_p[N]:并查集大小

注意最后题目是问间隔多少个元素,等于长度 − 1 -1 1

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 30010;

int p[N], d[N], size_p[N];

int find(int x) {
   
    if (p[x] != x) {
   
        int root = find(p[x]);
        d[x] += d[p[x]], p[x] = root;
    }
    return p[x];
}

int main() {
   
    int T; scanf("%d", &T);
    for (int i = 1; i < N; i++) p[i] = i, size_p[i] = 1;
    while (T--) {
   
        char op[2]; int a, b; scanf("%s%d%d", op, &a, &b);
        int pa = find(a), pb = find(b);
        if (*op == 'M') {
   
            if (pa != pb) {
   
                d[pa] = size_p[pb];
                size_p[pb] += size_p[pa];
                p[pa] = pb;
            }
        } else {
   
            if (pa != pb) puts("-1");
            else printf("%d\n", max(0, abs(d[a] - d[b]) - 1));
        }
    }
    return 0;
}

9.7 奇偶游戏

ACwing 239

利用前缀和, S i = a 1 + a 2 + a 3 + . . . + a i S_i = a_1 + a_2 + a_3 + ... + a_i Si=a1+a2+a3+...+ai,表示前 i i i个数中 1 1 1的个数。

S [ L , R ] S_{[L,R]} S[L,R]中有奇数个 1 1 1
⇒ \Rightarrow S R − S L − 1 S_R - S_{L-1} SRSL1为奇数;
⇒ \Rightarrow S R S_R SR S L − 1 S_{L-1} SL1奇偶性不同。

S [ L , R ] S_{[L,R]} S[L,R]中有偶数个 1 1 1,同理有 S R S_R SR S L − 1 S_{L-1} SL1奇偶性相同。

这里就将问题转换为了:每一次操作告诉我们两个数 l 、 r l、r lr,它们是同类还是不同类(同类 = 奇偶性相同 = 偶数个1,不同类 = 奇偶性不同 = 奇数个1),并且求出第一次出现矛盾是在第几次操作中。

用带偏移量(边权)的并查集做法

维护的一个相对关系,即每个节点都存储它与根结点的关系,并且关系具有传递性。

每个结点有数组 d [ x ] d[x] d[x],表示它与 p [ x ] p[x] p[x]的关系。

  • d [ x ] = 0 d[x] = 0 d[x]=0:表示它与父结点同类
  • d [ x ] = 1 d[x] = 1 d[x]=1:表示它与父结点不同类

x x x与根结点的关系,可以用从点 x x x到根结点的总距离之和来判断,判断它是奇数还是偶数即可,如果是偶数表示它与根结点同类,如果是奇数,表示不同类。

那么当并查集中同时存在两个点 x 、 y x、y xy时,若 x x x y y y到根结点的距离之和为奇数,那么它们为不同类,如果为偶数,则是同类。

操作

x 、 y x、y xy是同一类:
p [ x ] = p [ y ] p[x] = p[y] p[x]=p[y],计算d[x] ^ d[y]。若其值为0,表示无矛盾;若其值为1,表示有矛盾;
p [ x ] ≠ p [ y ] p[x] \ne p[y] p[x]=p[y],将点 x x x所在的并查集合并到 y y y所在的并查集,如图所示。在这里插入图片描述
所以有d[p[x]] = d[x] ^ d[y]

x 、 y x、y xy不是同一类:
p [ x ] = p [ y ] p[x] = p[y] p[x]=p[y],计算d[x] ^ d[y]。若其值为0,表示矛盾;若其值为1,表示无矛盾;
p [ x ] ≠ p [ y ] p[x] \ne p[y] p[x]=p[y],将点 x x x所在的并查集合并到 y y y所在的并查集,有d[p[x]] = d[x] ^ d[y] ^ 1

模2运算下,加法等价于异或(^)运算

#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;

const int N = 1e4 + 10;

int n, m;
int p[N], d[N];
unordered_map<int, int> S;

inline int get(int x) {
   
    if (!S.count(x)) S[x] = ++n;
    return S[x];
}

inline int find(int x) {
   
    if (p[x] != x) {
   
        int root = find(p[x]);
        d[x] += d[p[x]];
        p[x] = root;
    }
    return p[x];
}

int main() {
   
    scanf("%d%d", &n, &m); n = 0;
    for (int i = 1; i <= N; i++) p[i] = i;
    int res = m; // 最多全部回答正确
    for (int i = 1; i <= m; i++) {
   
        int a, b; char op[5]; scanf("%d%d%s", &a, &b, op);
        a = get(a - 1), b = get(b); // 获取离散化之后的值,这里的 a-1 指 L-1,b 指 R
        int t = 0;
        if (*op == 'o') t = 1;
        int pa = find(a), pb = find(b);
        if (pa == pb) {
   
            if (((d[a] + d[b]) % 2 + 2) % 2 != t) {
   
                res = i - 1; break;
            }
        } else p[pa] = pb, d[pa] = d[b] - d[a] + t;
    }
    printf("%d\n", res);
    return 0;
}

9.8 食物链

ACWing 240

算法思路:

不管两个结点是什么关系,都将它们放入一个集合中。通过维护它们与根结点之间的关系,这里用该结点到根结点的距离来表示。距离d(可以采用模3运算来判断每个结点到根结点的距离,根结点可以看成到自己的距离是0):

  • d=1:表示该结点可以吃根结点;
  • d=2:表示该结点可以被根结点吃,该结点可以吃其父结点;
    • 因为其父结点到根结点的距离为1,该结点可以吃父结点,父结点可以吃根结点,为了维护一个环状关系,则该结点则可以被根结点吃;
    • 注意这里维护的是一个环状关系,不是链式关系,不具有传递性!如y吃根节点,x可以被根结点吃,那么x可以吃y;
  • d=3:表示该结点和根结点是同类。因为模3运算后为0。
    在这里插入图片描述

代码中两段距离的更新:

  • x 、 y x、y xy 是同类,但是不在同一个集合中,合并的时候要求满足 ( d [ x ] + u − d [ y ] ) % 3 = 0 (d[x] + u - d[y]) \% 3 = 0 (d[x]+
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值