并查集的应用
一,概述
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
它虽然不是很复杂的数据类型,却也是设计的很精妙,可以运用于许多地方。
二,集合的个数
1,集合的个数:即为树的个数。
假设有 n 个点,m 条边,则初始时有 n 个集合,则每当两个点 join 时:
返回 1,代表两个点所在的集合不是同一个,两个集合可以合并。集合的个数由 2 变成 1,即集合的个数减少 1 个。
返回 0,代表两个点所在的集合是同一个,无需合并,集合的个数不变。
即:
集合的个数 = n - (join(每一条边) == 1 的个数)
2,之前遇到过这题型,现在找不到了,例题就先空着吧,遇到了再补。
三,环的个数
1,环的个数:根据所给的图(点和边),确定图中有几个环。
给定的图,必须限定图中的边无交叉(否则可能一次形成多个环),所算出来的环的个数也只是计算图中最小的环的个数(不包括多个环叠加起来的大的环那种),否则无法用并查集计算。
假设有 n 个点,m 条边,每当两个点 join 时:
返回 1,代表没有形成新的环。
返回 0,代表形成一个新的环。
即:
环的个数 = (join(每一条边) == 0 的个数)
注意到:
(join(每一条边) == 0 的个数) + (join(每一条边) == 1 的个数) = m
所以有:
集合的个数 - 环的个数 = n - m
2,如:HDU 2120 ice_cream's world I
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 1000000+10 int p[N], n, m; int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int join(int x, int y) { x = find(x), y = find(y); if (x == y) return 1; p[x] = y; return 0; } int main(void) { while (scanf("%d%d", &n, &m) != EOF) { for (int i = 0; i <= n; i++) p[i] = i; int ans = 0; while (m--) { int a, b; scanf("%d%d", &a, &b); ans += (join(a, b)); } printf("%d\n", ans); } system("pause"); return 0; }
四,集合中点的个数
1,确定与某个特定的点连通的点的个数。
2,如 POJ 1611 The Suspects,就是要求和点0连通的所有点的个数。该问题可以转化为求点0所在集合的个数。有因为每个集合都有一个代表性的点:根节点。
于是,设数组 num[],num[i] 表示以 i 为根节点的集合的点的个数。
算法开始前,将 num[i] = 1; 表示每个集合初始时只有一个点。
算法运行时,在集合合并结束后,将 num[根节点]++; 就可以了。
算法结束后,查询时,num[find(x)] 就表示点 x 所在的集合的点的个数。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> int p[30000 + 5], num[30000 + 5], a[30000 + 5]; int n, m; int find(int x) { if (x != p[x]) p[x] = find(p[x]); return p[x]; } void join(int x, int y) { x = find(x), y = find(y); if (x == y) return; p[x] = y; num[y] += num[x]; } int main(void) { while (scanf("%d%d", &n, &m), n + m) { for (int i = 0; i < n; i++) { p[i] = i; num[i] = 1; } while (m--) { int t; scanf("%d", &t); for (int i = 0; i < t; i++) { scanf("%d", &a[i]); if (i != 0) join(a[i], a[i - 1]); } } printf("%d\n", num[find(0)]); } system("pause"); return 0; }
五,集合的含义
1,点x 和 点y 存在不同的关系时,可以用不同的并查集表示不同的关系。
2,如 POJ 1182 食物链里 x 和 y 是有三种不同的对应关系的:y 是 x 的同类;y 是 x 的食物;y 是 x 的天敌。
那么,如何写出三个不同的并查集呢?这里有个小技巧,就是我们在题目中用到的数值都是有取值范围的,我们将其最大值设为 n。显然 x 和 y 的值都是小于等于 N。
我们设 x <= [1, n],y <= [1, n],于是有:
第一个并查集:其中的任意两个点,我们是区分不出哪个是 x,哪个是 y,但无所谓,第一个并查集可以用来表示同类。无论是 x 和 y 是同类,还是 y 和 x 是同类都是相同的意义。
第二个并查集:其中有些数的取值范围是 [1, n],表示 y;有些数的取值范围是 [1+n, n*2],表示 x。该集合可以用来表示 y 是 x 的食物。
第三个并查集:其中有些数的取值范围是 [1, n],表示 y;有些数的取值范围是 [1+n*2, n*3],表示 x。该集合可以用来表示 y 是 x 的天敌。
于是,我们就可以用 find函数,来判断 x,y 的关系了。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #define N 50000+5 int p[N * 3]; // g++ 不能这样?只能 int p[200000+5] 或 p[(50000+5)*3] // 同类,猎物,天敌 int find(int x) { int a = x; while (x != p[x]) x = p[x]; while (x != a) { int t = p[a]; p[a] = x; a = t; } return x; } void join(int x, int y) { x = find(x), y = find(y); if (x == y) return; p[x] = y; } int main(void) { int n, k; scanf("%d %d", &n, &k); { for (int i = 1; i <= n * 3 + 2; i++) p[i] = i; int ans = 0; while (k--) { int d, x, y; scanf("%d %d %d", &d, &x, &y); if (x > n || y > n) { ans++; continue; } if (d == 1) { // x 的猎物是 y 或 x 的天敌是 y if (find(x + n) == find(y) || find(x + 2 * n) == find(y)) { ans++; continue; } // x 的同类是 y join(x, y); join(x + n, y + n); join(x + 2 * n, y + 2 * n); } else // x 吃 y { if (x == y) { ans++; continue; } // x 的同类是 y 或者 y 的猎物是 x if (find(x) == find(y) || find(x) == find(y + n)) { ans++; continue; } join(x + n, y); // x 的猎物是 y join(x, y + 2 * n); // x 的天敌是 y join(x + 2 * n, y + n); // x的天敌 是 y的猎物 } } printf("%d\n", ans); } system("pause"); return 0; }
六,移动次数
1,如 HDU 3635 Dragon Balls
初始状态下:第 i 颗龙珠对应第 i 个城市。然后就是给你若干移动,即把第 i 颗龙珠所在的城市的所有龙珠移动到第 j 颗龙珠所在的城市。其实就是集合的合并。
这一题主要难点在于它会问,第 i 颗球的移动次数。这个要怎么理解呢?
我们设数组 mov[],将其初始化为 0,表示所有珠子都没有经过移动。然后,在 join 的时候,mov[移动的龙珠所在的集合的根节点]++; 此时,我们得到的 mov[] 数组,它只记录了移动时龙珠所在集合的根节点移动次数,其它同集合的移动次数都没有增加。
那么,如何让同集合的其它点的移动次数增加呢?
我们可以在压缩路径的递归算法,回溯的时候增加。怎么个增加法呢?
你那个回溯的时候,不正是从根节点往叶节点回溯。而每次回溯时,移动次数都是存在根节点中,而压缩路径又有延迟性,有可能你根节点进行了第二次移动变成了不是根节点,但之前的移动次数还保留着。所以,某个点的移动次数 = 该点到根节点的路径上所有节点的移动次数之和。所以你需要利用回溯,把所有节点的移动次数从根节点加下来。
最后,有一点需要注意,还是因为压缩路径。如果你要找第 i 颗球的移动次数,但有可能该颗球还没进行压缩路径,即没有计算别的球移动对它的移动次数的影响,所以回答 mov[i] 之前需要 find(i) 计算一下。
2,总结一下:就是将某个点对同集合的其它所有点的影响保存在当前根节点中(因为该根节点在之后集合的和合并后可能不是根节点了),然后利用路径压缩,将影响传递给所有同集合的点。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #define N 10005 int p[N], num[N], mov[N]; int n, m; int find(int x) { if (p[x] != x) { int t = p[x]; p[x] = find(p[x]); mov[x] += mov[t]; } return p[x]; } void join(int x, int y) { x = find(x), y = find(y); if (x == y) return; p[x] = y; num[y] += num[x]; mov[x]++; } int main(void) { int t; scanf("%d", &t); int ci = 0; while (t--) { scanf("%d%d", &n, &m); for (int i = 0; i <= n; i++) { p[i] = i; num[i] = 1; mov[i] = 0; } printf("Case %d:\n", ++ci); char str[10]; while (m--) { scanf("%s", str); int x, y; if (str[0] == 'T') { scanf("%d%d", &x, &y); join(x, y); } if (str[0] == 'Q') { scanf("%d", &x); int k = find(x); printf("%d %d %d\n", k, num[k], mov[x]); } } } system("pause"); return 0; }
=========== ========= ======== ======= ====== ====== ===== === == =
《沙扬娜拉》 徐志摩
——赠日本女郎
最是那一低头的温柔,
像一朵水莲花不胜凉风的娇羞,
道一声珍重,道一声珍重,
那一声珍重里有蜜甜的忧愁——