【算法】并查集(普通/扩展/带权)模板与例题

一、 普通并查集

1.1 数据结构与基础概念

  • 概念解释
    并查集 (Union-Find Set):一种用于管理元素所属集合的数据结构,能够高效地处理"合并两个集合"以及"查询两个元素是否同属一个集合"的操作。
  • 笔记
  • 标签:预处理、基础算法。
  • 维护"集合"的三件套:查询、合并、判同。
  • 数据结构 :使用数组 fa[i] 作为结点的父指针,根结点指向自己。

1.2 核心操作实现

  • 初始化
cpp 复制代码
void init(int n) { for(int i=1;i<=n;i++) fa[i]=i; }

功能与参数说明:初始化阶段,让每个元素自成一个独立的集合。参数 n 为元素总数,遍历使得 fa[i] = i

  • 查找
cpp 复制代码
int find(int x) {                    // 路径压缩
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}

功能与参数说明:查找元素 x 所在集合的根节点。

  • 概念解释(路径压缩) :在递归查找根节点的回溯过程中,将查找路径上的所有节点直接连接到根节点上。这能使后续查询的时间复杂度逼近 O(1)O(1)O(1)。
  • 合并
cpp 复制代码
void un(int x,int y) {               // 按秩/大小合并可写一行
    int fx=find(x), fy=find(y);
    if(fx!=fy) fa[fx]=fy;            // 把 fx 挂到 fy 下
}

功能与参数说明:将 xy 所在的集合合并。分别找到两者根节点,若不同,则将其中一棵树的根挂载到另一棵树的根下。

  • 概念解释(按秩合并):一种优化策略,把较矮/较小的树挂到较高/较大的树下,防止树退化成链表。结合路径压缩通常可简化实现。
  • 判同
cpp 复制代码
bool same(int x,int y) { return find(x)==find(y); }

功能与参数说明:判断 xy 是否属于同一个集合。通过比对两者的根节点是否一致来返回布尔值。

1.3 统计集合个数

  • 笔记:通过判定有多少个元素的父节点依然指向自己(即有多少个根节点),来统计连通块/集合的总数。
cpp 复制代码
int cnt=0;
for(int i=1;i<=n;i++) if(fa[i]==i) cnt++;

二、 扩展域查并集(关系 > 2种)

2.1 多关系处理机制

  • 概念解释
    扩展域并查集:当元素间的关系不仅限于"同属一集"(如存在"敌对"关系)时,通过将原数组的容量扩大数倍,让每个元素的多个"域"来代表其不同的状态或身份。
  • 笔记
  • 核心思想:把"每个元素"拆成多个域,每个域代表一种状态/关系,用偏移量存储。
  • 经典场景:朋友 & 敌人(2倍域)
  • 域 1∼n1 \sim n1∼n (或 0∼n−10 \sim n-10∼n−1):朋友身份。
  • 域 n+1∼2nn+1 \sim 2nn+1∼2n (或 n∼2n−1n \sim 2n-1n∼2n−1):敌人身份。

2.2 核心操作实现

  • 初始化
cpp 复制代码
const int N = 1e6 + 10;
int fa[N * 2];             // 0~n-1 为原域,n~2n-1 为"敌人域"

void init(int n) {
    for (int i = 1; i <= 2 * n; i++) fa[i] = i;
}

功能与参数说明:为 2n2n2n 个元素(包含本体域和敌人域)进行独立初始化,自成一派。

  • 查找
cpp 复制代码
int find(int x) {          // 与普通版完全相同
    if (fa[x] == x) return x;
    return fa[x] = find(fa[x]);
}

功能与参数说明:扩展域中的查找逻辑与普通并查集完全一致,仍使用路径压缩。

  • 合并
cpp 复制代码
void un(int x, int y) {    // 合并根,普通写法
    int fx = find(x), fy = find(y);
    if (fx != fy) fa[fx] = fy;
}

void mergeFriend(int x, int y, int n) { // 朋友:双向绑定
    un(x, y);
    un(x + n, y + n);
}

void mergeEnemy(int x, int y, int n) {  // 敌人:交叉绑定
    un(x, y + n);
    un(y, x + n);
}

功能与参数说明:

  • 朋友关系 :合并 xxx 与 yyy 的原域,同时合并 xxx 与 yyy 的敌人域。
  • 敌人关系 :说"xxx 和 y+ny+ny+n 合并"意思是:xxx 和 yyy 的敌人是朋友。执行交叉绑定。
  • 判同 (状态查询)
cpp 复制代码
bool isFriend(int x, int y) { return find(x) == find(y); }
bool isEnemy (int x, int y)  { return find(x) == find(y + n); }
bool conflict(int x, int y, int n) { // 判断是否"既友又敌"
    return isFriend(x, y) && isEnemy(x, y, n);
}

功能与参数说明:分别检测本体域是否连通、交叉域是否连通,以及是否同时触发导致逻辑冲突。


三、 带权并查集

3.1 核心思想与数据结构

  • 概念解释
    带权并查集:在边上记录权值的并查集,通常用来维护节点间的一维相对关系(如相对距离、高度差、分数差)。
  • 笔记
  • 核心思想 :给每条父边记录"权值 d[i]",表示 iii 到父节点的某种累积量。路径压缩后 d[i] 直接是 i→根i \rightarrow \text{根}i→根 的累积量。
  • 数据结构:引入额外数组维护权值。
cpp 复制代码
int fa[N], d[N];   // d[i]:i 相对于父节点的权值

3.2 核心操作实现

  • 初始化
cpp 复制代码
void init(int n) {
    for(int i=1;i<=n;i++) { fa[i]=i; d[i]=0; }
}

功能与参数说明:初始状态下,父节点是自己,距离自身权值偏差为 000。

  • 查找
cpp 复制代码
int find(int x) {
    if(fa[x]==x) return x;
    int root=find(fa[x]);   // 先让祖先挂到根
    d[x] += d[fa[x]];       // 累加"祖父→根"部分
    return fa[x]=root;      // 挂根,返回根
}

功能与参数说明:路径压缩并累计权值。在递归回溯阶段,将当前节点原本到父节点的权值,加上父节点到根节点的权值,完成权值的路径压缩。

  • 合并
cpp 复制代码
void un(int x,int y,int w) {   // w: x→y 的给定边权
    int fx=find(x), fy=find(y);
    if(fx==fy) return;         // 已同集合,视情况判断矛盾
    fa[fx]=fy;
    d[fx] = d[y] - d[x] - w;   // 保证公式成立
}

功能与参数说明:以 "xxx 到 yyy 的边权为 www" 为例。目标让 fxfxfx 挂到 fyfyfy,必须满足向量等式 dx+dfx+w=dydx + dfx + w = dydx+dfx+w=dy,故反解出 dfxdfxdfx 的赋值公式。

  • 判同 (查询两点间量值)
cpp 复制代码
int query(int x,int y) {
    int fx=find(x), fy=find(y);
    if(fx!=fy) return INF;     // 不同集合,量值未知
    return d[y] - d[x];        // 根据合并公式推导
}

功能与参数说明:判定两点是否同源,若同源,通过两者的绝对偏移量相减得出相对量值。


四、 选用策略

场景 用法
纯连通、集合计数 普通 UFS + 路径压缩 + 按秩合并
朋友-敌人 / 食物链 扩展域(2 倍或 3 倍)
差分、距离、相对高度 带权 UFS(维护 d[]

五、 实战:P1525 关押罪犯

5.1 问题建模与策略

  • 概念解释
    贪心算法 (Greedy Algorithm):在对问题求解时,总是做出在当前状态下最优的选择。
  • 笔记
  • 题目核心 :NNN 名罪犯分 222 座监狱,同监狱有仇则发生摩擦产生"怨气值 ccc"。求最大冲突事件的怨气值最小化。
  • 数据规模 :N≤20000,M≤100000N \le 20000, M \le 100000N≤20000,M≤100000 →\rightarrow→ 时间复杂度需控制在 O(Mlog⁡M)O(M \log M)O(MlogM)。
  • 问题建模 :将罪犯分成两个监狱,使同一监狱内的最大仇恨值最小 ⇔\Leftrightarrow⇔ 优先把仇恨值大的罪犯分到不同监狱
  • 贪心策略
  1. 从仇恨值最大的矛盾开始处理。
  2. 尽量把仇恨值大的罪犯分到不同监狱。
  3. 当发现两个罪犯无法分到不同监狱时,当前的仇恨值就是答案
  • 逻辑梳理 (扩展域并查集)

  • 1∼n1 \sim n1∼n 域:罪犯本身集合;n+1∼2nn+1 \sim 2nn+1∼2n 域:罪犯的敌人集合。

  • un(a, b + n):aaa 与 bbb 的敌人是朋友。

  • un(b, a + n):bbb 与 aaa 的敌人是朋友。

  • 冲突检测if(find(a) == find(b)),说明之前的强力分配已经迫使他们在同一个监狱(已经是朋友),矛盾爆发。

5.2 代码实现

  • 笔记:结构体、排序支持与完整主逻辑。
cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 4e4 + 10, M = 1e5 + 10;

int n, m;
struct node
{
    int a, b, c;  // a,b:两个罪犯编号, c:仇恨值
}e[M];

int fa[N];  // 并查集数组

// 查找
int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

// 合并
void un(int x, int y)
{
    fa[find(x)] = find(y);
}

// 贪心排序支持
bool cmp(node& x, node& y)
{
    return x.c > y.c;
}

int main()
{
    cin >> n >> m;  // n:罪犯数量, m:矛盾数量

    for(int i = 1; i <= m; i++) 
        cin >> e[i].a >> e[i].b >> e[i].c;

    // 初始化:扩展为2倍
    for(int i = 1; i <= n + n; i++) 
        fa[i] = i;

    // 贪心:按仇恨值从大到小排序
    sort(e + 1, e + 1 + m, cmp);

    for(int i = 1; i <= m; i++)
    {
        int a = e[i].a, b = e[i].b, c = e[i].c;

        // 建立敌人关系:交叉绑定
        un(a, b + n);  
        un(b, a + n);  

        // 判同检测:如果a和b已经是朋友,说明无法分到不同监狱
        if(find(a) == find(b))
        {
            cout << c << endl; // 输出当前最大的不可避免的冲突
            return 0;
        }
    }

    cout << 0 << endl;
    return 0;
}

相关推荐
qq7422349841 小时前
从“感知”到“决断”:测评百度伐谋产业决策智能体的端到端推理与行动机制
人工智能·算法·百度·大模型·运筹优化
中屹指纹浏览器1 小时前
指纹浏览器环境克隆、批量派生的风控隐患剖析与标准化新建环境实操指南
经验分享·笔记
繁星蓝雨1 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余1 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习
a诠释淡然2 小时前
C++ vs Rust:哪个更适合你的下一个项目?
开发语言·c++·rust
小小de风呀2 小时前
de风——【从零开始学C++】(十二):stack和queue的基本使用和模拟实现
开发语言·c++
元直数字电路验证2 小时前
云计算实验笔记(四):容器编排(Container Orchestration)
运维·笔记·docker·云计算
huohaiyu2 小时前
深入解析Java垃圾回收机制
java·开发语言·算法·gc
汉克老师2 小时前
GESP6级C++考试语法知识(五十三、动态规划----背包问题(六、分组背包)
c++·动态规划·背包问题·gesp6级·gesp六级·分组背