【数据结构】带权并查集

文章目录

  • 上文链接
  • 一、带权并查集
    • [1. 概念](#1. 概念)
    • [2. 实现距离问题](#2. 实现距离问题)
      • [(1) 初始化](#(1) 初始化)
      • [(2) 查询操作](#(2) 查询操作)
      • [(3) 合并操作](#(3) 合并操作)
      • [(4) 查询操作](#(4) 查询操作)
  • [二、OJ 练习](#二、OJ 练习)
    • [1. 食物链 ⭐⭐⭐](#1. 食物链 ⭐⭐⭐)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)
    • [2. 银河英雄传说 ⭐⭐⭐](#2. 银河英雄传说 ⭐⭐⭐)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)

上文链接

一、带权并查集

1. 概念

带权并查集在普通并查集的基础上,为每个结点增加了一个权值。这个权值可以表示当前结点与父结点之间的关系、距离或其他信息(注意,由于我们有路径压缩操作,所以最终这个权值表示的是当前结点相对于根结点的信息)。有了这样一个权值,就可以推断出集合中各个元素之间的相互关系。


2. 实现距离问题

我们以最简单的距离问题为例,给定数轴上很多个点,点的位置未知,但是告诉你这些点与点之间的距离,于是我们可以用带权并查集来维护这些距离,最后做到可以快速查询任意两个点之间的距离 。实现带权并查集的核心是在进行 FindUnion 操作时,不仅要维护集合的结构,还要维护结点的权值。

注意:带权并查集的实现是多种多样的,基本上换一道题,实现的代码就要更改。因此一定要重点关注实现过程的思考方式,这才是通用的。

(1) 初始化

cpp 复制代码
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
int n;
int pa[N], d[N];  // d[i] 表示点 i 到根节点对应的点的距离
void init()
{
	for(int i = 1; i <= n; i++)
    {
        pa[i] = i;
        d[i] = 0; // 根据题目要求来初始化: 自己到自己的距离为0
	}
}

(2) 查询操作

查询某个点到根节点所对应的点之间的距离。

cpp 复制代码
int find(int x)
{
    if(pa[x] == x) return x;
    int t = find(pa[x]); // 这句代码一定要先执行,先让父节点挂在根节点的后面
    d[x] += d[pa[x]]; // 注意,可能会根据权值的意义有所改变
    return pa[x] = t;
}

(3) 合并操作

合并 x x x 所在集合与 y y y 所在集合,已知 x x x 与 y y y 之间的距离是 w w w。

cpp 复制代码
void un(int x, int y, int w)
{
	int fx = find(x), fy = find(y);
    if(fx != fy) // 不在同一个集合中
    {
        pa[fx] = fy;
        d[fx] = d[y] + w - d[x]; // 注意,可能会根据权值的意义有所改变
	}
}

我们会发现这里我们并没有更新 x x x 或者 B B B 到根节点的距离信息,这个信息是等到下次 find 它们的时候去维护的,所以在 union 中不去管它。

(4) 查询操作

查询 x x x 到 y y y 的距离。

cpp 复制代码
int query(int x, int y)
{
    int fx = find(x), fy = find(y);
    if(fx != fy) return INF; // 如果不在同一个集合中,说明距离未知
    return d[y] - d[x];
}

二、OJ 练习

1. 食物链 ⭐⭐⭐

【题目链接】

P2024 [NOI2001\] 食物链 - 洛谷](https://www.luogu.com.cn/problem/P2024)

【题目描述】

动物王国中有三类动物 A , B , C A,B,C A,B,C,这三类动物的食物链构成了有趣的环形。 A A A 吃 B B B, B B B 吃 C C C, C C C 吃 A A A。

现有 N N N 个动物,以 1 ∼ N 1 \sim N 1∼N 编号。每个动物都是 A , B , C A,B,C A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N N N 个动物所构成的食物链关系进行描述:

  • 第一种说法是 1 X Y,表示 X X X 和 Y Y Y 是同类。
  • 第二种说法是 2 X Y,表示 X X X 吃 Y Y Y。

此人对 N N N 个动物,用上述两种说法,一句接一句地说出 K K K 句话,这 K K K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话;
  • 当前的话中 X X X 或 Y Y Y 比 N N N 大,就是假话;
  • 当前的话表示 X X X 吃 X X X,就是假话。

你的任务是根据给定的 N N N 和 K K K 句话,输出假话的总数。

【输入格式】

第一行两个整数, N , K N,K N,K,表示有 N N N 个动物, K K K 句话。

第二行开始每行一句话。格式见题目描述与样例。

【输出格式】

一行,一个整数,表示假话的总数。

【示例一】

输入

复制代码
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5

输出

复制代码
3

【说明/提示】

对于全部数据, 1 ≤ N ≤ 5 × 1 0 4 1\le N\le 5 \times 10^4 1≤N≤5×104, 1 ≤ K ≤ 1 0 5 1\le K \le 10^5 1≤K≤105。


(1) 解题思路

假设 A A A 类动物有 A 1 , A 2 , A 3 ⋯ A_1, A_2,A_3\cdots A1,A2,A3⋯ 这么多个, B , C B,C B,C 同理,那么我们由它们的关系可以得到

cpp 复制代码
A1--->B1--->C1--->A2--->B2--->C2--->A3... (箭头表示捕食)

那么如果我们给它们每个动物都给一个编号如下:

cpp 复制代码
A1--->B1--->C1--->A2--->B2--->C2--->A3...
0     1     2     3     4     5     6 

那么我们可以发现,如果有两个动物的编号是 i , j i,j i,j,那么

  • 如果 ( i − j )   m o d   3 = 0 (i - j)\bmod 3 = 0 (i−j)mod3=0,则 i i i 和 j j j 是同类;
  • 如果 ( i − j )   m o d   3 = 1 (i - j)\bmod 3 = 1 (i−j)mod3=1,则 i i i 被 j j j 捕食;
  • 如果 ( i − j )   m o d   3 = 2 (i - j)\bmod 3 = 2 (i−j)mod3=2,则 i i i 捕食 j j j。

这里的 i − j i - j i−j 其实就可以理解为 "距离",我们把真话里面的相互关系,用带权并查集维护起来,权值表示当前节点相对于根节点的距离。如果两节点间的距离除 3 3 3 余 0 0 0,则它们是同类,其他情况同上。

当我们要合并两个节点 x x x 和 y y y 的时候,如果它们是同类,我们就把它们的距离设置为 0 0 0(或者 3 3 3 的倍数),传给 uni 函数;如果 x x x 捕食 y y y,那么 x x x 相对于 y y y 的距离就是 − 1 -1 −1(或者 − 1 + 3 k -1 + 3k −1+3k),传给 uni 函数。

剩下的查找和合并中维护权值的操作和上面实现距离问题的逻辑一样。


(2) 代码实现

cpp 复制代码
#include <iostream>

using namespace std;

const int N = 5e4 + 10;
int n, k;
int pa[N], d[N]; // 带权并查集

int find(int x)
{
    if (pa[x] == x) return x;
    int t = find(pa[x]);
    d[x] += d[pa[x]];
    return pa[x] = t;
}

void uni(int x, int y, int w)
{
    int fx = find(x), fy = find(y);
    if (fx != fy)
    {
        pa[fx] = fy;
        d[fx] = d[y] + w - d[x];
    }
}

int main()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i++) pa[i] = i;
    int cnt = 0;

    while (k--)
    {
        int op, x, y;
        cin >> op >> x >> y;
        int fx = find(x), fy = find(y);
        
        if (x > n || y > n) cnt++;
        else if (op == 1) // 同类
        {
            // 如果此时它们在一个集合中但是发现它们并不是同类关系, 说明是假话
            if (fx == fy && ((d[y] - d[x]) % 3 + 3) % 3 != 0) cnt++;
            // 否则合并两个节点
            else uni(x, y, 0);
        }
        else // x -> y
        {
            // 如果此时它们在一个集合中但是发现它们并不是 x 捕食 y 的关系, 说明是假话
            if (fx == fy && ((d[y] - d[x]) % 3 + 3) % 3 != 1) cnt++;
            else uni(x, y, 2);
        }
    }
    cout << cnt << endl;
    return 0;
}

2. 银河英雄传说 ⭐⭐⭐

【题目链接】

P1196 [NOI2002\] 银河英雄传说 - 洛谷](https://www.luogu.com.cn/problem/P1196)

【题目背景】

公元 5801 5801 5801 年,地球居民迁至金牛座 α \alpha α 第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。

宇宙历 799 799 799 年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。

【题目描述】

杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成 30000 30000 30000 列,每列依次编号为 1 , 2 , ... , 30000 1, 2,\ldots ,30000 1,2,...,30000。之后,他把自己的战舰也依次编号为 1 , 2 , ... , 30000 1, 2, \ldots , 30000 1,2,...,30000,让第 i i i 号战舰处于第 i i i 列,形成"一字长蛇阵",诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为 M i j,含义为第 i i i 号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第 j j j 号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。

然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。

在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第 i i i 号战舰与第 j j j 号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。

作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。

最终的决战已经展开,银河的历史又翻过了一页......

【输入格式】

第一行有一个整数 T T T( 1 ≤ T ≤ 5 × 1 0 5 1 \le T \le 5 \times 10^5 1≤T≤5×105),表示总共有 T T T 条指令。

以下有 T T T 行,每行有一条指令。指令有两种格式:

  1. M i j: i i i 和 j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1≤i,j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第 i i i 号战舰与第 j j j 号战舰不在同一列。

  2. C i j: i i i 和 j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1≤i,j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。

每条指令中都保证 i ≠ j i\neq j i=j。

【输出格式】

依次对输入的每一条指令进行分析和处理:

  • 如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息。
  • 如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第 i i i 号战舰与第 j j j 号战舰之间布置的战舰数目。如果第 i i i 号战舰与第 j j j 号战舰当前不在同一列上,则输出 − 1 -1 −1。

【示例一】

输入

复制代码
4
M 2 3
C 1 2
M 2 4
C 4 2

输出

复制代码
-1
1

【说明/提示】

样例解释

战舰位置图:表格中阿拉伯数字表示战舰编号。


(1) 解题思路

很明显要用到并查集,并且我们还要知道第 i i i 号战舰与第 j j j 号战舰之间布置的战舰数目,所以我们需要用到带权并查集。我们用 d[i] 来表示第 i i i 号战舰到它所在列的头部有多少个战舰。用 cnt[i] 表示 i i i 所在的集合中一共有多少个战舰,注意这里只有当 i i i 是根节点的时候 cnt[i] 才有此意义。


(2) 代码实现

cpp 复制代码
#include<iostream>
#include<cmath>

using namespace std;

const int N = 30010;
int pa[N], d[N], cnt[N];

void init()
{
    for(int i = 1; i <= 30000; i++)
    {
        pa[i] = i;
        cnt[i] = 1;
    }
}

int find(int x)
{
    if(pa[x] == x) return x;
    int t = find(pa[x]);
    d[x] += d[pa[x]];
    return pa[x] = t;
}

void uni(int x, int y)
{
    int fx = find(x);
    int fy = find(y);
    if(fx != fy)
    {
        pa[fx] = fy;
        d[fx] = cnt[fy];  // fx 节点到队头一共有 cnt[fy] 支战舰
        cnt[fy] += cnt[fx];  // fy 是根节点, 合并之后它所在集合多了 cnt[fx] 支战舰
    }
}

int query(int x, int y)
{
    if(find(x) != find(y)) return -1;
    return abs(d[x] - d[y]) - 1;
}

int main()
{
    int t; cin >> t;
    init();
    while(t--)
    {
        char ch; int i, j; 
        cin >> ch >> i >> j;
        if(ch == 'M') uni(i, j);
        else cout << query(i, j) << endl;
    }

    return 0;
}
相关推荐
云泽80812 小时前
C++ List 容器详解:迭代器失效、排序与高效操作
开发语言·c++·list
CodeAmaz12 小时前
通用 List 分批切割并循环查询数据库工具类
java·数据结构·工具类·分页
xlq2232212 小时前
15.list(上)
数据结构·c++·list
我不会插花弄玉12 小时前
排序【由浅入深-数据结构】
c语言·数据结构
Elias不吃糖13 小时前
总结我的小项目里现在用到的Redis
c++·redis·学习
ANYOLY13 小时前
Sentinel 限流算法详解
算法·sentinel
AA陈超13 小时前
使用UnrealEngine引擎,实现鼠标点击移动
c++·笔记·学习·ue5·虚幻引擎
XH华14 小时前
数据结构第三章:单链表的学习
数据结构
No0d1es14 小时前
电子学会青少年软件编程(C/C++)六级等级考试真题试卷(2025年9月)
c语言·c++·算法·青少年编程·图形化编程·六级
AndrewHZ14 小时前
【图像处理基石】图像去雾算法入门(2025年版)
图像处理·人工智能·python·算法·transformer·cv·图像去雾