news 2026/2/7 2:13:03

算法基础-并查集

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
算法基础-并查集

并查集

1.1双亲表⽰法

接下来要学习到的并查集,本质上就是⽤双亲表⽰法实现的森林。因此,我们先认识⼀下双亲表⽰
法。
在学习树这个数据结构的时,讲到树的存储⽅式有很多种:孩⼦表⽰法,双亲表⽰法、孩⼦双亲表⽰
法以及孩⼦兄弟表⽰法等。对⼀棵树⽽⾔,除了根节点外,其余每个结点⼀定有且仅有⼀个双亲,双
亲表⽰法就是根据这个特点存储树的,也就是把每个结点的双亲存下来。因此,我们可以采⽤数组来
存储每个结点的⽗亲结点的编号,这就实现了双亲表⽰法(so easy)。
但是,在实现并查集的时,我们⼀般让根节点⾃⼰指向⾃⼰。因此,上述存储就变成:


1.2并查集的概念

在有些问题中,我们需要维护若⼲个集合,并且基于这些集合要频繁执⾏下⾯的操作:
• 查询操作:查找元素x属于哪⼀个集合。⼀般会在每个集合中选取⼀个元素作为代表,查询的是
这个集合中的代表元素;
• 合并操作:将元素x所在的集合与元素y所在的集合合并成⼀个集合;(注意,合并的是元素所
在的集合,不是这两个元素!)
• 判断操作:判断元素xy是否在同⼀个集合。

并查集(Union Find):是⼀种⽤于维护元素所属集合的数据结构,实现为⼀个森林,其中每棵树表 ⽰⼀个集合,树中的节点表⽰对应集合中的元素根节点来代表整个集合


1.3并查集的实现

1.3.1初始化

初始状态下,所有的元素单独成为⼀个集合:
• 让元素⾃⼰指向⾃⼰即可。
代码实现:
#include <iostream> using namespace std; // 定义数组最大容量:100万+10(留余量避免越界) const int N = 1e6 + 10; // n表示需要管理的元素总数 int n; // fa数组:fa[i]代表元素i的父节点(组长),双亲表示法核心数组 int fa[N]; // 初始化并查集函数:让每个元素的父节点都是自己(自成一组) void init() { // 遍历1到n的所有元素 for(int i = 1; i <= n; i++) { // 每个元素初始时组长是自己 fa[i] = i; } } // 主函数示例:演示如何使用初始化函数 int main() { // 示例:假设要管理5个元素 n = 5; // 调用初始化函数 init(); // 打印初始化后的fa数组(验证结果) cout << "初始化后fa数组:" << endl; for(int i = 1; i <= n; i++) { cout << "fa[" << i << "] = " << fa[i] << endl; } return 0; }

1.3.2查询操作

查询操作是并查集的核⼼操作,其余所有的操作都是基于查询操作实现的!
找到元素x所属的集合:
• ⼀直向上找爸爸~
代码实现:
int find(int x) { if(fa[x] == x) return x; return find(fa[x]); // 一行实现 // return fa[x] == x ? x : find(fa[x]); }

1.3.3合并操作

将元素x所在的集合与元素y所在的集合合并成⼀个集合:
• 让元素x所在树的根节点指向元素y所在树的根节点。
(反过来也是可以的)
代码实现:
// 合并操作 void un(int x, int y) // 注意,函数名字不能⽤ union,因为它是 C++ 的关键字 { int fx = find(x); int fy = find(y); fa[fx] = fy; }

1.3.4判断操作

判断元素x和元素y是否在同⼀集合:
• 看看两者所在树的根节点是否相同。
代码实现:
// 判断是否在同⼀集合 bool issame(int x, int y) { return find(x) == find(y); }

1.4并查集的优化

极端情况:在合并的过程中,整棵树变成⼀个链表。
路径压缩:在查询时,把被查询的节点到根节点的路径上的所有节点的⽗节点设置为根节点,从⽽减 ⼩树的深度。也就是说,在向上查询的同时,把在路径上的每个节点都直接连接到根上,以后查询时 就能直接查询到根节点。
代码实现:
// 找根节点 - 路径压缩 int find(int x) { if(fa[x] == x) return x; return fa[x] = find(fa[x]); // ⼀⾏实现 return fa[x] == x ? x : fa[x] = find(fa[x]); }
还有⼀种优化⽅式是按秩合并,但是基本上不⽤按秩合并,并查集的时间复杂度就很优秀了。感兴趣 的同学可以搜⼀下按秩合并,按照⼤家现在的⽔平,应该很容易就能看懂~
在《算法导论》中有严格的证明,并查集查询根节点的最坏时间复杂度为O(α(n)) ,是⼀个很⼩的常 数。因此,并查集查询以及合并的效率近似可以看成O(1)。

1.5普通并查集

1.5.1【模板】并查集

题⽬来源: 洛⾕
题⽬链接:P3367 【模板】并查集
难度系数: ★★

题目背景

本题数据范围已经更新到 1≤N≤2×105,1≤M≤106。

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式

第一行包含两个整数 N,M ,表示共有 N 个元素和 M 个操作。

接下来 M 行,每行包含三个整数 Zi​,Xi​,Yi​ 。

当 Zi​=1 时,将 Xi​ 与 Yi​ 所在的集合合并。

当 Zi​=2 时,输出 Xi​ 与 Yi​ 是否在同一集合内,是的输出Y;否则输出N

输出格式

对于每一个 Zi​=2 的操作,都有一行输出,每行包含一个大写字母,为Y或者N

输入输出样例

输入 #1复制

4 7 2 1 2 1 1 2 2 1 2 1 3 4 2 1 4 1 2 3 2 1 4

输出 #1复制

N Y N Y

说明/提示

对于 15% 的数据,N≤10,M≤20。

对于 35% 的数据,N≤100,M≤103。

对于 50% 的数据,1≤N≤104,1≤M≤2×105。

对于 100% 的数据,1≤N≤2×105,1≤M≤106,1≤Xi​,Yi​≤N,Zi​∈{1,2}。


【解法】

模板题~

【参考代码】

#include <iostream> using namespace std; const int N = 2e5 + 10; // 数组最大容量:20万+10(留余量) int n; // 元素总数(比如示例里n=4) int fa[N]; // fa[i]:元素i的父节点(组长) // find函数:找元素x的“最终组长”(根节点),并做路径压缩(提速) int find(int x) { // 如果x的父节点是自己,说明x就是组长,直接返回 if(fa[x] == x) return x; // 否则:递归找x父节点的组长,同时把x的父节点直接指向组长(路径压缩) return fa[x] = find(fa[x]); } int main() { int T; // T是操作总数(示例里T=7) cin >> n >> T; // 初始化并查集:每个元素的组长都是自己 for(int i = 1; i <= n; i++) fa[i] = i; // 遍历所有操作(T--:执行T次,每次减1) while(T--) { int z, x, y; // z是操作类型,x、y是操作的元素 cin >> z >> x >> y; if(z == 1) // 操作1:合并x和y所在的集合 { int fx = find(x); // 找x的最终组长 int fy = find(y); // 找y的最终组长 fa[fx] = fy; // 把x的组长的父节点设为y的组长(合并两组) } else // 操作2:查询x和y是否同组 { // 若x和y的最终组长相同,说明同组→输出Y,否则输出N if(find(x) == find(y)) cout << "Y" << endl; else cout << "N" << endl; } } return 0; }

1.5.2亲戚

题⽬来源: 洛⾕
题⽬链接:P1551 亲戚
难度系数: ★★

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

规定:x 和 y 是亲戚,y 和 z 是亲戚,那么 x 和 z 也是亲戚。如果 x,y 是亲戚,那么 x 的亲戚都是 y 的亲戚,y 的亲戚也都是 x 的亲戚。

输入格式

第一行:三个整数 n,m,p,(n,m,p≤5000),分别表示有 n 个人,m 个亲戚关系,询问 p 对亲戚关系。

以下 m 行:每行两个数 Mi​,Mj​,1≤Mi​, Mj​≤n,表示 Mi​ 和 Mj​ 具有亲戚关系。

接下来 p 行:每行两个数 Pi​,Pj​,询问 Pi​ 和 Pj​ 是否具有亲戚关系。

输出格式

p 行,每行一个YesNo。表示第 i 个询问的答案为“具有”或“不具有”亲戚关系。

输入输出样例

输入 #1复制

6 5 3 1 2 1 5 3 4 5 2 1 3 1 4 2 3 5 6

输出 #1复制

Yes Yes No

【解法】

具有亲戚关系的两个集合就合并在⼀个集合中。因此,可以⽤并查集解决。

【参考代码】

#include <iostream> using namespace std; const int N = 5010; // 数组最大容量:5010(因为n≤5000,留10个余量) int n, m, p; // n=人数,m=亲戚关系数,p=询问数 int fa[N]; // fa[i]:第i个人的“亲戚组长”(根节点) // find函数:找第x个人的最终组长(路径压缩,提速) // 三元运算符:fa[x]==x就返回x,否则递归找并压缩路径 int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } // un函数:合并x和y的亲戚组(un是union的缩写,因为union是关键字不能用) void un(int x, int y) { int fx = find(x); // 找x的最终组长 int fy = find(y); // 找y的最终组长 fa[fy] = fx; // 把y的组长的父节点设为x的组长(合并两组) } // issame函数:判断x和y是不是亲戚(最终组长相同就是亲戚) bool issame(int x, int y) { return find(x) == find(y); } int main() { cin >> n >> m >> p; // 初始化并查集:每个人的组长都是自己(初始无亲戚) for(int i = 1; i <= n; i++) fa[i] = i; // 处理m组亲戚关系:合并对应的人 while(m--) { int x, y; cin >> x >> y; un(x, y); // 合并x和y的亲戚组 } // 处理p个询问:判断是不是亲戚,输出Yes/No while(p--) { int x, y; cin >> x >> y; if(issame(x, y)) cout << "Yes\n"; // 是亲戚输出Yes else cout << "No\n"; // 不是输出No } return 0; }

1.5.3Lake Counting

题⽬来源: 洛⾕
题⽬链接:P1596 [USACO10OCT] Lake Counting S
难度系数: ★★

由于最近的降雨,水在农夫约翰的田地里积聚了。田地可以表示为一个 N×M 的矩形(1≤N≤100;1≤M≤100)。每个方格中要么是水(W),要么是干地(.)。农夫约翰想要弄清楚他的田地里形成了多少个水塘。一个水塘是由连通的水方格组成的,其中一个方格被认为与它的八个邻居相邻。给定农夫约翰田地的示意图,确定他有多少个水塘。

输入格式

第 1 行:两个用空格分隔的整数:N 和 M。

第 2 行到第 N+1 行:每行 M 个字符,表示农夫约翰田地的一行。

每个字符要么是W,要么是.

字符之间没有空格。

输出格式

第 1 行:农夫约翰田地中的水塘数量。

显示翻译

题意翻译

输入输出样例

输入 #1复制

10 12 W........WW. .WWW.....WWW ....WW...WW. .........WW. .........W.. ..W......W.. .W.W.....WW. W.W.W.....W. .W.W......W. ..W.......W.

输出 #1复制

3

说明/提示

输出详情:共有三个水塘:一个在左上角,一个在左下角,还有一个沿着右侧。

(由 ChatGPT 4o 翻译)


【解法】

遍历整个矩阵,每次遇到⼀个⽔坑时,就把这个⽔坑右、下,左下以及右下的⽔坑合并在⼀起。
最终判断⼀下⼀共有多少个集合。

【参考代码】

#include <iostream> using namespace std; const int N = 110; // 网格最大100×100,留10个余量 int n, m; // n=行数,m=列数(示例里n=10,m=12) char a[N][N]; // 存储网格:a[i][j]是第i行第j列的字符(W/.) int fa[N * N]; // 并查集数组:fa[编号] = 该位置的组长(编号= i*m + j) // 方向数组:只查4个关键方向(右、下、右下、左下),避免重复合并 // dx[k]是行偏移,dy[k]是列偏移 int dx[] = {0, 1, 1, 1}; // 行:0(同列)、1(下一行)、1(下一行)、1(下一行) int dy[] = {1, 1, 0, -1}; // 列:1(右)、1(右下)、0(下)、-1(左下) // find函数:找位置编号x的最终组长(路径压缩) // 三元运算符:如果x的组长是自己,返回x;否则递归找并压缩路径 int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } // un函数:合并两个位置的集合(x和y是一维编号) void un(int x, int y) { // 把x的组长的父节点设为y的组长 → 合并两个集合 fa[find(x)] = find(y); } int main() { cin >> n >> m; // 读入行数n和列数m // 读入网格:逐行逐列存字符(W/.) for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) cin >> a[i][j]; // 初始化并查集:每个位置的组长都是自己(不管是W还是.) for(int i = 0; i < n * m; i++) fa[i] = i; // 遍历每个网格位置 for(int i = 0; i < n; i++) // 遍历第i行 { for(int j = 0; j < m; j++) // 遍历第j列 { if(a[i][j] == '.') continue; // 是旱地,跳过 // 检查4个关键方向的网格 for(int k = 0; k < 4; k++) { // 计算目标位置的行x、列y int x = i + dx[k]; int y = j + dy[k]; // 边界判断:y≥0(列不越左界)+ x<n(行不越下界) + 目标位置是W if(x < n && y >= 0 && a[x][y] == 'W') { // 把当前位置(i,j)和目标位置(x,y)合并 // 一维编号:i*m+j(当前)、x*m+y(目标) un(i * m + j, x * m + y); } } } } // 统计水坑数:遍历所有位置,数“是W且组长是自己”的数量 int ret = 0; // ret存水坑数,初始为0 for(int i = 0; i < n * m; i++) { // 一维编号转二维坐标:x=行,y=列 int x = i / m; // 行号 = 编号 ÷ 列数(比如i=12,m=12 → x=1) int y = i % m; // 列号 = 编号 % 列数(比如i=12,m=12 → y=0) // 如果该位置是W,且组长是自己 → 是一个独立水坑 if(a[x][y] == 'W' && fa[i] == i) ret++; } cout << ret << endl; // 输出水坑数 return 0; }

1.5.4程序⾃动分析

题⽬来源: 洛⾕
题⽬链接:P1955 [NOI2015] 程序⾃动分析
难度系数: ★★★

题目描述

在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。

考虑一个约束满足问题的简化版本:假设 x1​,x2​,x3​,⋯ 代表程序中出现的变量,给定 n 个形如 xi​=xj​ 或 xi​=xj​ 的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。例如,一个问题中的约束条件为:x1​=x2​,x2​=x3​,x3​=x4​,x4​=x1​,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。

现在给出一些约束满足问题,请分别对它们进行判定。

输入格式

输入的第一行包含一个正整数 t,表示需要判定的问题个数。注意这些问题之间是相互独立的。

对于每个问题,包含若干行:

第一行包含一个正整数 n,表示该问题中需要被满足的约束条件个数。接下来 n 行,每行包括三个整数 i,j,e,描述一个相等/不等的约束条件,相邻整数之间用单个空格隔开。若 e=1,则该约束条件为 xi​=xj​。若e=0,则该约束条件为 xi​=xj​。

输出格式

输出包括 t 行。

输出文件的第 k 行输出一个字符串YES或者NO(字母全部大写),YES表示输入中的第 k 个问题判定为可以被满足,NO表示不可被满足。

输入输出样例

输入 #1复制

2 2 1 2 1 1 2 0 2 1 2 1 2 1 1

输出 #1复制

NO YES

输入 #2复制

2 3 1 2 1 2 3 1 3 1 1 4 1 2 1 2 3 1 3 4 1 1 4 0

输出 #2复制

YES NO

说明/提示

【样例解释1】

在第一个问题中,约束条件为:x1​=x2​,x1​=x2​。这两个约束条件互相矛盾,因此不可被同时满足。

在第二个问题中,约束条件为:x1​=x2​,x1​=x2​。这两个约束条件是等价的,可以被同时满足。

【样例说明2】

在第一个问题中,约束条件有三个:x1​=x2​,x2​=x3​,x3​=x1​。只需赋值使得 x1​=x2​=x3​,即可同时满足所有的约束条件。

在第二个问题中,约束条件有四个:x1​=x2​,x2​=x3​,x3​=x4​,x4​=x1​。由前三个约束条件可以推出 x1​=x2​=x3​=x4​,然而最后一个约束条件却要求 x1​=x4​,因此不可被满足。

【数据范围】

所有测试数据的范围和特点如下表所示:

测试点编号n 的规模i,j 的规模约定
11≤n≤101≤i,j≤1041≤t≤10e∈{0,1}
2
31≤n≤100
4
51≤n≤105
6
7
81≤n≤1051≤i,j≤109
9
10

【解法】

先利⽤并查集维护所有相等的信息,然后遍历所有的不相等信息,判断⼀下是否合法。
因为数据范围的问题,需要先对所有的数离散化处理。

【参考代码】

#include <iostream> #include <unordered_map> // 哈希表,用于离散化映射 #include <algorithm> // 排序,离散化需要 using namespace std; const int N = 1e5 + 10; // 约束条件最多10万条 int n; // 每个问题的约束数 // 存储每个约束条件:x=变量i,y=变量j,e=1(相等)/0(不等) struct node { int x, y, e; }a[N]; // 离散化相关变量 int pos; // 记录所有出现过的变量编号数量 int disc[N * 2]; // 存储所有出现过的变量编号(去重前) unordered_map<int, int> mp; // 映射表:原编号→离散化后的小编号 // 并查集数组 int fa[N * 2]; // find函数:找离散化后编号x的最终组长(路径压缩) int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } // un函数:合并两个离散化后的编号x和y void un(int x, int y) { fa[find(x)] = find(fa[y]); } // issame函数:判断两个离散化后的编号是否在同一集合 bool issame(int x, int y) { return find(x) == find(y); } // 处理单个问题的函数:返回true(满足)/false(不满足) bool solve() { cin >> n; // 读入当前问题的约束数 // 清空离散化相关数据(多组测试用例,避免干扰) pos = 0; mp.clear(); // 第一步:读入所有约束,收集所有出现过的变量编号 for(int i = 1; i <= n; i++) { cin >> a[i].x >> a[i].y >> a[i].e; // 把变量x、y存入disc数组(后续离散化用) disc[++pos] = a[i].x; disc[++pos] = a[i].y; } // 第二步:离散化(大编号→小编号) // 1. 对disc数组排序(方便去重) sort(disc + 1, disc + 1 + pos); int cnt = 0; // 离散化后的编号(从1开始) for(int i = 1; i <= pos; i++) { int x = disc[i]; if(mp.count(x)) continue; // 已经映射过,跳过(去重) cnt++; // 新的小编号 mp[x] = cnt; // 记录映射关系(比如x1→1) } // 第三步:初始化并查集(离散化后的编号从1到cnt) for(int i = 1; i <= cnt; i++) fa[i] = i; // 第四步:处理所有相等约束(e=1),合并变量 for(int i = 1; i <= n; i++) { int x = a[i].x, y = a[i].y, e = a[i].e; if(e == 1) // 相等约束:合并x和y的离散化编号 un(mp[x], mp[y]); } // 第五步:检查所有不等约束(e=0),判断是否矛盾 for(int i = 1; i <= n; i++) { int x = a[i].x, y = a[i].y, e = a[i].e; if(e == 0) // 不等约束 { // 如果x和y在同一集合(本该不等,却相等)→ 矛盾,返回false if(issame(mp[x], mp[y])) return false; } } // 所有约束都满足,返回true return true; } int main() { int T; cin >> T; // 读入问题个数 while(T--) // 处理每个问题 { if(solve()) // 满足所有约束 cout << "YES" << endl; else // 不满足 cout << "NO" << endl; } return 0; }

1.6扩展域并查集

普通的并查集只能解决各元素之间仅存在⼀种相互关系,⽐如《亲戚》题⽬中:
ab是亲戚关系,bc是亲戚关系,这时就可以查找出ac也存在亲戚关系。
但如果存在各元素之间存在多种相互关系,普通并查集就⽆法解决。⽐如下⾯的案例:
• a和b 是敌⼈关系,b 和 c是敌⼈关系,但是a 和c 其实不是敌⼈关系,⽽是另⼀种朋友关
系。
此时,就不仅仅是简单的敌⼈关系,还是出现⼀种朋友关系。
解决这类问题就需要对并查集进⾏扩展:将每个元素拆分成多个域,每个域代表⼀种状态或者关系。
通过维护这些域之间的关系,来处理复杂的约束条件。
敌⼈朋友问题中,我们会将x分成两个域,朋友域x以及敌⼈域y
xy是朋友,正常处理,把xy合并成⼀个集合;
• x和 y是敌⼈:那么x 和y 的敌⼈y+n 就是朋友,合并x 与y+n ; y和 x的敌⼈
x+n就是朋友,合并y 与 x+n。
这样就可以利⽤两个域,将所有的关系维护起来。

1.6.1团伙

题⽬来源: 洛⾕
题⽬链接:P1892 [BOI2003] 团伙
难度系数: ★★★

题目描述

现在有 n 个人,他们之间有两种关系:朋友和敌人。我们知道:

  • 一个人的朋友的朋友是朋友
  • 一个人的敌人的敌人是朋友

现在要对这些人进行组团。两个人在一个团体内当且仅当这两个人是朋友。请求出这些人中最多可能有的团体数。

输入格式

第一行输入一个整数 n 代表人数。

第二行输入一个整数 m 表示接下来要列出 m 个关系。

接下来 m 行,每行一个字符 opt 和两个整数 p,q,分别代表关系(朋友或敌人),有关系的两个人之中的第一个人和第二个人。其中 opt 有两种可能:

  • 如果 opt 为F,则表明 p 和 q 是朋友。
  • 如果 opt 为E,则表明 p 和 q 是敌人。

输出格式

一行一个整数代表最多的团体数。

输入输出样例

输入 #1复制

6 4 E 1 4 F 3 5 F 4 6 E 1 2

输出 #1复制

3

说明/提示

对于 100% 的数据,2≤n≤1000,1≤m≤5000,1≤p,q≤n。


【解法】

扩展域并查集模板题:
ab如果是朋友,那就直接合并在⼀起;
ab如果是敌⼈,那就把ab+n以及a+nb合并在⼀起。

【参考代码】

#include <iostream> using namespace std; const int N = 1010; // 人数最多1000,扩展域后是2000,留10余量 int n, m; // n=人数,m=关系数 int fa[N * 2]; // 扩展域并查集:1~n是原域(朋友),n+1~2n是敌域(敌人) // find函数:找x的最终组长(路径压缩) int find(int x) { // 三元运算符:x的组长是自己则返回x,否则递归找并压缩路径 return fa[x] == x ? x : fa[x] = find(fa[x]); } // un函数:合并x和y(让y的组长指向x的组长,保证朋友域为父节点) void un(int x, int y) { fa[find(y)] = find(x); } int main() { cin >> n >> m; // 读入人数n和关系数m // 初始化扩展域并查集:1~2n的每个位置组长都是自己 for(int i = 1; i <= n * 2; i++) fa[i] = i; // 处理m个关系 while(m--) { char op; // 关系类型:F(朋友)/E(敌人) int x, y; // 有关系的两个人 cin >> op >> x >> y; if(op == 'F') // 朋友关系:合并x和y的原域 { un(x, y); } else // 敌人关系:合并x和y的敌域、y和x的敌域 { un(x, y + n); // x 加入 y的敌域 un(y, x + n); // y 加入 x的敌域 } } // 统计团体数:原域(1~n)中组长是自己的数量 int ret = 0; for(int i = 1; i <= n; i++) { if(fa[i] == i) ret++; } cout << ret << endl; // 输出团体数 return 0; }

1.6.2⻝物链

题⽬来源: 洛⾕
题⽬链接:P2024 [NOI2001] ⻝物链
难度系数: ★★★

题目描述

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

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

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

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

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

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

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

输入格式

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

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

输出格式

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

输入输出样例

输入 #1复制

100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5

输出 #1复制

3

说明/提示

对于全部数据,1≤N≤5×104,1≤K≤105。


【解法】

针对x,扩展三个域:同类域 x,捕⻝域 x + n,被捕⻝域 x + n + n。
如果 x 和 y 是同类:
x 和 y 是同类
x + n 与 y + n 是同类
x + n + n 与 y + n + n 是同类
如果 x 捕⻝ y:
• x + n 与 y 同类
• x 与 y + n + n 同类
• x + n + n 与 y + n 同类

【参考代码】

#include <iostream> using namespace std; const int N = 5e4 + 10; // 动物最多5万,三倍扩展后15万+10 int n, k; // n=动物数,k=话的数量 int fa[N * 3]; // 三倍扩展域并查集:1~n(同类)、n+1~2n(捕食)、2n+1~3n(被捕食) // find函数:找x的最终组长(路径压缩,提速) int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); } // un函数:合并x和y的集合(让x的组长指向y的组长) void un(int x, int y) { fa[find(x)] = find(y); } int main() { cin >> n >> k; // 初始化扩展域并查集:1~3n的每个位置组长都是自己 for(int i = 1; i <= n * 3; i++) fa[i] = i; int ret = 0; // 统计假话数量,初始为0 // 处理k句话 while(k--) { int op, x, y; cin >> op >> x >> y; // 假话条件2:X或Y超过n → 直接记假话 if(x > n || y > n) { ret++; continue; // 跳过后续判断 } if(op == 1) // 第1种说法:X和Y是同类 { // 冲突判定:X在Y的捕食域(find(x)==find(y+n))或被捕食域(find(x)==find(y+2n)) if(find(x) == find(y + n) || find(x) == find(y + 2 * n)) { ret++; // 假话,计数+1 } else { // 合并同类域、捕食域、被捕食域 un(x, y); // 同类域合并 un(x + n, y + n); // 捕食域合并 un(x + 2 * n, y + 2 * n); // 被捕食域合并 } } else // 第2种说法:X吃Y { // 冲突判定:X和Y是同类(find(x)==find(y)),或X在Y的捕食域(Y吃X,冲突) if(find(x) == find(y) || find(x) == find(y + n)) { ret++; // 假话,计数+1 } else { // 合并规则:X吃Y → 维护三种关系 un(x, y + 2 * n); // X的同类域 ↔ Y的被捕食域 un(y, x + n); // Y的同类域 ↔ X的捕食域 un(x + 2 * n, y + n); // X的被捕食域 ↔ Y的捕食域 } } } cout << ret << endl; // 输出假话总数 return 0; }

1.7带权并查集

1.带权并查集的概念

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

2.带权并查集的实现

我们以最简单的距离问题为例,实现⼀个能够查询任意两点之间距离的并查集。 实现带权并查集的核⼼是在进⾏ FindUnion操作时,不仅要维护集合的结构,还要维护结点的权值
注意:带权并查集的实现是多种多样的,基本上换⼀道题,实现的代码就要更改。因此⼀定要重点关 注实现过程的思考⽅式,这才是通⽤的。

初始化init

const int N = 1e5 + 10, INF = 0x3f3f3f3f; int n; int fa[N], d[N]; void init() { for(int i = 1; i <= n; i++) { fa[i] = i; d[i] = 0; // 根据题⽬要求来初始化 } }

查询根节点操作find

int find(int x) { if(fa[x] == x) return x; int t = find(fa[x]); // 这句代码⼀定要先执⾏,先让⽗结点挂在根节点的后⾯ d[x] += d[fa[x]]; // 注意,可能会根据权值的意义有所改变 return fa[x] = t; }

合并操作union

// x 所在集合与 y 所在集合合并,x 与 y 之间的权值是 w void un(int x, int y, int w) { int fx = find(x), fy = find(y); if(fx != fy) // 不在同⼀个集合中 { fa[fx] = fy; d[fx] = d[y] + w - d[x]; // 注意,可能会根据权值的意义有所改变 } }

查询距离操作query

// 查询 x 到 y 的距离 int query(int x, int y) { int fx = find(x), fy = find(y); if(fx != fy) return INF; // 如果不在同⼀个集合中,说明距离未知 return d[y] - d[x]; }

1.7.1⻝物链

题⽬来源: 洛⾕
题⽬链接:P2024 [NOI2001] ⻝物链
难度系数: ★★★

题目描述

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

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

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

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

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

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

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

输入格式

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

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

输出格式

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

输入输出样例

输入 #1复制

100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5

输出 #1复制

3

说明/提示

对于全部数据,1≤N≤5×104,1≤K≤105。


【解法】

把真话⾥⾯的相互关系,⽤"带权并查集"维护起来权值表⽰当前节点相对于根节点的距离。那么对 于集合中的任意两点xy
• 如果 (d[y] −d[x]) % 3 == 0 ,表⽰两者是同类关系;
• 如果 (d[y] −d[x]) % 3 == 1 ,表⽰两者是捕⻝关系;
• 如果 (d[y] −d[x]) % 3 == 2 ,表⽰两者是天敌关系。
find 操作:
• 更新d数组:按照最基础的距离更新的⽅式, d[x] = d[x] + d[fa[x]] ;
union 操作:
如果xy是同类,那么边权就是0
如果xy,那么边权就是1

【参考代码】

#include <iostream> using namespace std; const int N = 5e4 + 10; // 动物最多5万,足够存储 int n, k; // n=动物数,k=话的数量 int fa[N], d[N]; // fa[]:父节点;d[]:当前节点到根节点的权值(距离) // find函数:找x的根节点,同时更新d[x](路径压缩+权值更新) int find(int x) { if(fa[x] == x) return x; // x是根节点,直接返回 int t = find(fa[x]); // 先找父节点的根(递归) d[x] += d[fa[x]]; // 更新x到根的权值:x→父节点 + 父节点→根 return fa[x] = t; // 路径压缩:x直接指向根 } // un函数:合并x和y的集合,w是x和y的期望权值(0=同类,2=x吃y) void un(int x, int y, int w) { int fx = find(x), fy = find(y); // 找x和y的根 if(fx != fy) // 不在同一集合,需要合并 { fa[fx] = fy; // 把x的根挂到y的根上 // 核心:更新fx到fy的权值,保证(d[y]-d[x])%3 = w d[fx] = d[y] + w - d[x]; } } int main() { cin >> n >> k; // 初始化:每个动物的父节点是自己,权值为0(到自己的距离为0) for(int i = 1; i <= n; i++) fa[i] = i; int ret = 0; // 统计假话数量 while(k--) { int op, x, y; cin >> op >> x >> y; // 先处理边界:X/Y超出范围 → 假话 if(x > n || y > n) { ret++; continue; } // 提前找根(后续判断冲突需要) int fx = find(x), fy = find(y); if(op == 1) // 说法1:X和Y是同类(期望(d[y]-d[x])%3=0) { // 冲突条件:同集合,但权值差模3≠0 if(fx == fy && ((d[y] - d[x]) % 3 + 3) % 3 != 0) { ret++; } else { un(x, y, 0); // 合并,期望权值0(同类) } } else // 说法2:X吃Y(期望(d[y]-d[x])%3=2 → 对应x吃y) { // 特殊情况:X=Y(X吃X)→ 假话 if(x == y) { ret++; continue; } // 冲突条件:同集合,但权值差模3≠1(注意:代码里写的是≠1,等价于≠2,看推导) if(fx == fy && ((d[y] - d[x]) % 3 + 3) % 3 != 1) { ret++; } else { un(x, y, 2); // 合并,期望权值2(X吃Y) } } } cout << ret << endl; return 0; }

1.7.2银河英雄传说

题⽬来源: 洛⾕
题⽬链接:P1196 [NOI2002] 银河英雄传说
难度系数: ★★★

题目背景

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

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

题目描述

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

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

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

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

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

输入格式

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

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

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

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

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

输出格式

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

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

输入输出样例

输入 #1复制

4 M 2 3 C 1 2 M 2 4 C 4 2

输出 #1复制

-1 1

说明/提示

样例解释

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


【解法】

这道题中有明显的边权关系,因此可以⽤"带权并查集"解决。

【参考代码】

#include <iostream> #include <cstdlib> // 用于abs函数(绝对值) using namespace std; const int N = 3e4 + 10; // 战舰最多3万,留10余量 int n = 3e4; // 固定战舰总数30000 int fa[N], d[N], cnt[N]; // fa=父节点,d=到父节点的距离,cnt=集合大小(列战舰数) // find函数:找x的根节点,同时更新d[x](路径压缩+权值更新) int find(int x) { if(fa[x] == x) return x; // x是根节点,直接返回 int t = find(fa[x]); // 递归找父节点的根 d[x] += d[fa[x]]; // 更新x到根的距离:x→父 + 父→根 return fa[x] = t; // 路径压缩:x直接指向根 } // un函数:合并x所在列到y所在列的尾部 void un(int x, int y) { int fx = find(x), fy = find(y); // 找x和y的根 if(fx != fy) // 不在同一列,执行合并 { fa[fx] = fy; // 把x的根挂到y的根上 d[fx] = cnt[fy]; // fx到fy的距离 = y列的总战舰数 cnt[fy] += cnt[fx]; // 合并后y列总数 = 原y列数 + 原x列数 } } // query函数:查询x和y之间的战舰数 int query(int x, int y) { int fx = find(x), fy = find(y); // 找x和y的根 if(fx != fy) return -1; // 根不同→不在同一列 // 根相同→距离差的绝对值减1(中间的战舰数) return abs(d[x] - d[y]) - 1; } int main() { // 初始化:每个战舰的父节点是自己,距离为0,列数为1 for(int i = 1; i <= n; i++) { fa[i] = i; // 初始父节点是自己 d[i] = 0; // 初始到自己的距离为0 cnt[i] = 1; // 初始每列只有1艘战舰 } int T; cin >> T; // 读入指令总数 while(T--) { char op; int x, y; cin >> op >> x >> y; // 读入指令类型和战舰编号 if(op == 'M') // 合并指令:x列接在y列尾部 { un(x, y); } else // 查询指令:C x y { cout << query(x, y) << endl; } } return 0; }

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/4 13:12:14

Flutter动态UI开发终极指南:用JSON构建可配置界面

Flutter动态UI开发终极指南&#xff1a;用JSON构建可配置界面 【免费下载链接】dynamic_widget A Backend-Driven UI toolkit, build your dynamic UI with json, and the json format is very similar with flutter widget code. 项目地址: https://gitcode.com/gh_mirrors/…

作者头像 李华
网站建设 2026/2/4 20:59:58

PurestAdmin:终极前后端分离权限管理框架快速入门指南

PurestAdmin&#xff1a;终极前后端分离权限管理框架快速入门指南 【免费下载链接】purest-admin 基于 .NET 8 vue3 实现的极简rabc权限管理系统后端 后端基于精简后的abp框架&#xff0c;前端基于vue-pure-admin&#xff0c;前端极强的表格框架vxe-table&#xff0c;旨在打造…

作者头像 李华
网站建设 2026/2/4 21:12:53

终极AI开发指南:5步构建自主可控的智能系统

终极AI开发指南&#xff1a;5步构建自主可控的智能系统 【免费下载链接】wgai 开箱即用的JAVAAI在线训练识别平台&OCR平台AI合集包含旦不仅限于(车牌识别、安全帽识别、抽烟识别、常用类物识别等) 图片和视频识别&#xff0c;可自主训练任意场景融合了AI图像识别opencv、yo…

作者头像 李华
网站建设 2026/2/6 3:02:21

Pascal VOC 2012数据集终极下载指南:告别缓慢下载的完整解决方案

Pascal VOC 2012数据集终极下载指南&#xff1a;告别缓慢下载的完整解决方案 【免费下载链接】PascalVOC2012数据集下载链接 Pascal VOC 2012 数据集是计算机视觉领域中广泛使用的基准数据集之一&#xff0c;包含了大量的图像和标注信息&#xff0c;适用于目标检测、图像分割等…

作者头像 李华
网站建设 2026/2/6 2:43:10

终极指南:WhisperLiveKit 实时语音转录与说话人识别完整教程

终极指南&#xff1a;WhisperLiveKit 实时语音转录与说话人识别完整教程 【免费下载链接】WhisperLiveKit Real-time, Fully Local Speech-to-Text and Speaker Diarization. FastAPI Server & Web Interface 项目地址: https://gitcode.com/GitHub_Trending/wh/WhisperL…

作者头像 李华
网站建设 2026/2/6 9:26:47

周云杰四年“苦战”,海尔智家2500亿市值魔咒何时能破?

2021年底&#xff0c;在张瑞敏这位传奇商业教父转身隐退之后&#xff0c;海尔正式迈入了“周云杰时代”。然而&#xff0c;四年时间如白驹过隙&#xff0c;海尔智家的市值却如同被施了魔法一般&#xff0c;始终在2500亿的门槛前徘徊不前&#xff0c;仿佛陷入了一个无法挣脱的估…

作者头像 李华