续前章节:并查集及应用
目录
- 1 带权问题
- 1.1 点带权
- 1.2 边带权
- 2 例题
- 2.1 家族合并
- 2.2 信息传递
- 2.3 [NOI2002] 银河英雄传说
1 带权问题
1.1 点带权
用num[i]
记录节点 i i i 所在的集合的数量。
- 初始化:所有的
num[i]
都是 1 1 1,因为每个点 i i i 都是自成一个集合。
我们只能在add()
和find()
中更新和维护num[]
。
-
add()
:void add(int x, int y) {int xx = find(x);int yy = find(y);if (xx != yy) fa[xx] = yy, num[yy] += num[xx];//x所在集合合并到y所在的集合,所以y所在集合的节点个数应该加上x所在集合的节点个数 }
-
find()
:不做改动。
注意:仅仅每个集合的根节点 i i i 的num[i]
具有时效性和真实性。一个集合里除根节点以外的其他节点的num[i]
时滞后的,没有意义。也就是说,在一个点所在集合的点的数量保存在其根节点的num[]
中。
1.2 边带权
用dis[i]
记录节点 i i i 到所在集合的根节点的距离(边权是 1 1 1)。
-
初始化:全部是 0 0 0,因为每个点自为一个集合,自己到自己的距离是 0 0 0。
-
add()
:void add(int x, int y) {int xx = find(x);int yy = find(y);if (xx != yy) fa[xx] = yy, dis[x] = dis[y] + 1;//理论上来讲,是x成为了y的子节点,所以x和y之间多了一条边,而且现在的dis[x]应当是到y所在集合根节点的距离//原先x所在集合的其他节点的dis[]会在find()中更新 }
-
find()
:每次find()
过程中把集合更新维护一遍。int find(int x) {if (fa[x] == x)return x;int k = find(fa[x]);//在x更新之前,先把它的所有祖先节点更新了dis[x] += dis[fa[x]];//换了新的根节点后,合并进来的集合中的每个节点都应当再加上一个距离(原根节点到新根节点的距离),从fa[x]依次向下更新return fa[x] = k; }
注意:因为真正把一整个集合重新更新dis[]
以来find()
函数,所以在每次获取dis[i]
的值之前,应当先把 i i i 所在集合find()
一遍。
2 例题
2.1 家族合并
题目描述
有 n 个人,刚开始每个人都代表着一个家族,现在要对其进行 Q Q Q 次操作,一共有如下三种操作:
C a b
,a 和 b 所在的家族合并到一起Q1 a b
,查询 a 和 b 是否在同一个家族Q2 a
,查询 a 所在的家族有多少个人对于每个询问指令
Q1 a b
,如果 a 和 b 在同一个家族中,则输出Yes
,否则输出No
。对于每个询问指令
Q2 a
,输出一个整数表示点 a 所在家族的人数。
题解
点带权问题。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, a, b, fa[N], num[N];
string s;
int find(int x) {if (fa[x] == x)return x;return fa[x] = find(fa[x]);
}
void add(int x, int y) {x = find(x);y = find(y);if (x != y) fa[x] = y, num[y] += num[x];
}
int main() {cin >> n >> m;for (int i = 1; i <= n; i++) fa[i] = i, num[i] = 1;while (m--) {cin >> s;if (s == "C") {cin >> a >> b;add(b, a);} else if (s == "Q1") {cin >> a >> b;if (find(a) == find(b))cout << "Yes" << endl;else cout << "No" << endl;} else {cin >> a;cout << num[find(a)] << endl;}}return 0;
}
2.2 信息传递
题目描述
n n n 名同学,每名同学都有一个固定的传递对象(非本人,一个人可能同为多个人的传递对象)。每个人都有一条自己特有的信息。
每进行一轮游戏时,每个人都会把自己已知的所有信息告诉传递对象,同时接收别人传来的信息。当有一个人从别人那里接收到自己的信息时,游戏停止。
问游戏可以进行几轮。
题解
每名同学都有一个固定的传递对象(非本人……
这句话告诉我们: n n n 名同学必然会形成一个环。
也就是说:当环里的一名同学的信息在环中传递了一遍,最后又传到自己时,游戏就停止了。这个环一定是最小的环。
现在问题转化为:求最小的一个环有多少个节点(或者多少条边)组成。因为不是所有的num[]
都是最新的,考虑求边数。
什么时候会产生环?
在并查集中,如果两个节点已经在同一个集合中,这两个点还要建立关系,就会形成环。
可以发现,一个环里如果信息能够顺畅传递,必然含有根节点。
在一个集合里,环通常是这样的:
也就是说,知道知道一个环里有 x , y x,y x,y 这两个节点,这个环的边数必然等于 dis[x] + dis[y] + 1
(以上图为例, y y y 到根节点的距离 2 2 2 加上 x x x 到根节点的距离 2 2 2,加上 x , y x,y x,y 之间的距离 1 1 1,环的总边数为 5 5 5)。
每次发现环时,堆环的边数求最小值即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, fa[N], dis[N], ans = INT_MAX, a;
string s;
int find(int x) {if (fa[x] == x)return x;dis[x] += dis[fa[x]];return fa[x] = find(fa[x]);
}
void add(int x, int y) {int x2 = find(x);int y2 = find(y);if (x2 == y2) ans = min(ans, dis[x] + dis[y] + 1); //发现环,记录答案fa[x2] = y2;dis[x] = dis[y] + 1;
}
int main() {CLOSE;cin >> n;for (int i = 1; i <= n; i++) fa[i] = i;for (int i = 1; i <= n; i++) cin >> a, add(i, a); //注意i和a的顺序cout << ans;return 0;
}
2.3 [NOI2002] 银河英雄传说
P1196 [NOI2002] 银河英雄传说 - 洛谷
题目描述
敌军将战场划分为 30000 30000 30000 列,开战之前,将序号为 1 , 2 , 3 , … , 30000 1,2,3,\dots,30000 1,2,3,…,30000 的战舰依次放置到对应的列上。
在战斗中,敌军会对战舰发出合并指令:
M i j
:表示 i i i 号战舰所在一整列的战舰作为一个整体(头在前尾在后)接在 j j j 号战舰所在列的尾部。我军的系统监听到了敌军的这些指令。我军指挥官会随时向系统发出询问指令:
C i j
:询问 i i i 号战舰和 j j j 号战舰之间隔着多少战舰。如果 i i i 和 j j j 号军舰,不在同一列,输出-1
。现在请处理我军和敌军所有的 T T T 条指令。
题解
因为所有节点成链状,不妨考虑点、边权结合。
find()
函数和边带权一样。
add()
函数:
void add(int x, int y) {int xx = find(x);int yy = find(y);if (xx == yy) return;fa[xx] = yy;dis[xx] = num[yy];//根节点xx接在yy所在集合的尾部后面,到根节点yy的距离显然就是原先yy集合的节点数num[yy] += num[xx]; //yy集合的节点数加上加入的xx集合的数量
}
再看主函数部分:
int main() {CLOSE;cin >> m;for (int i = 1; i <= 3e4 + 10; i++)fa[i] = i, num[i] = 1;while (m--) {int x, y;cin >> s >> x >> y;if (s == "M") {add(x, y); //注意x和y的顺序} else {int k1 = find(x), k2 = find(y); //先find()一下,更新dis[]if (k1 != k2) cout << -1 << endl;else cout << (int)(abs(dis[x] - dis[y])) - 1 << endl;//不确定x和y的先后,取绝对值}}return 0;
}