一、问题引入
原题: HDU1232畅通工程
首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个互相独立的块。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。如果是1个连通分支,说明整幅图上的点都连起来了,不用再修路了;如果是2个连通分支,则只要再修1条路,从两个分支中各选一个点,把它们连起来,那么所有的点都是连起来的了;如果是3个连通分支,则只要再修两条路……
二、并查集原理
并查集就是由一个整形数组和2个函数构成,一个函数是find():查找一个点的标识,一个union():将2个点进行合并成为一个连通分支。
首先,我们先来看一副图:
上面表示了3个连通分支。假设不同连通的人就敌人,现在我们需要判断蜘蛛侠和蚁人是不是敌人,那我们该如何判断?我们可以首先找到蜘蛛侠所在分支的标识(钢铁侠),然后再找到蚁人所在分支的标识(美国队长),然后我们判断一下2个人是否是同一个人,很明显不是,所以蜘蛛侠和蚁人就是敌人,现在我们来看一下鹰眼和蚁人是不是敌人,他们的标识都是美国队长,所以他们不是敌人。
现在我们想让美国队长这个分支与钢铁侠这个分支成为一个分支(合并),应该如何做呢?
我们可以直接让标识:美国队长指向钢铁侠,也就是钢铁侠成为新分支的标识。
下面我们来看如何通过代码进行实现:
int[] parent;
parent[]这个数组,记录了每个人的上级是谁。如过这个人的上级是自己,那么他就是标识。也可以一个人就是一个集合,那么他的标识当然是自己了。
int find(int x) //查找x的这个分支的标识
{
while(patent[x] != x)
x = patent[x] ;
return x ;
}
find()方法查找x所在分支的标识,patent[x] != x 代表他的上级不是自己,那么他也不是标识,所以继续往上查找,x=patent[x];当x == patent[x] 的时候,代表x就是这个连通分支的标识了,这个时候就返回.
void union(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx != fy)
parent[fx]=fy;
}
比如我们想让上面的蚁人(x)和蜘蛛侠(y)成为一个阵营的,首先我们先找到x这个分支的表示fx(美国队长)和y这个分支的标识fy(钢铁侠),让fx(美国队长)的上级变成钢铁侠(parent[fx] = fy). 这个时候蚁人和蜘蛛侠就变成一个连通分支里面的点了。
上面就介绍了并查集的基本原理,但是还存在一些问题,接下来我们来看看如何解决这些问题,优化并查集。
三、并查集优化
我们将两个点进行union的时候完全是随机的,谁当谁的上级都是随机的,这样的话我们的分支结构可能会变成这样:
变成这样的话,当我们要查找最底层元素所在分支的标识的话会消耗一下时间。所以我们想让我们的分支结构变成所有下级都指向一个同一个上级,这样树的高度只有2,每个人直接与自己的上级就行相连,查找就会节约很多时间。如下图:
现在我们就来学学如何转换为这种结构也就是路径压缩算法:
1.非递归:
public static int find2(int x) {
int p = x;
//寻找这个集合的标识
while (p != parent[p]) {
p = parent[p];
}
while (x != parent[x]) {
int tem = parent[x];
//将每一个点都直接指向标识
parent[x] = p;
x = tem;
}
//最后返回查找到的标识
return p;
}
2.递归形式:
int find(int x) {
if (x == p[x]) return x;
p[x] = find(p[x]);
return p[x];
}
public static int find(int x) {
return x == parent[x] ? x : (parent[x] = find(parent[x]));
}
下面再来另外一个问题:
如果我们按照上面的union()方法进行合并的话,就会得到图右边的这种情况,这样树形结构又会使查询的速度变慢。所以我们要优化一下union()方法。下图是优化后的结果:
可以看到我们经过优化后深度明显变小了,这样会使查询速度提高很多。
这样的优化算法称为 加权union(),下面看代码:
int rank[N]; //树的高度
public static void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return;
if (rank[fx] > rank[fy]) {
p[fy] = fx;
} else {
if (rank[fx] == rank[fy]) rank[fy]++;
p[fx] = fy;
}
}
首先我们将每个点的高度都初始化为0。然后再进行合并的时候,我们先比较2个标识的高度。rank[fx] > rank[fy]:表示fx这棵树高度大于fy这颗树,这个时候我们可以直接将fy指向fx即可。若进入else:表名fx < 或者 = fy的高度,
所以进行判断一下,如果fx = fy,2棵树的高度相同,我们直接把fy的高度+1,然后让fx指向fy即可,若fx != fy,直接fx指向fy即可。
三、例题代码
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1005;
int p[N], rank[N];
int find(int x) {
return x == p[x] ? x : (p[x] = find(p[x]));
}
void merge(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return;
if (rank[fx] > rank[fy]) {
p[fy] = fx;
} else {
if (rank[fx] == rank[fy]) rank[fy]++;
p[fx] = fy;
}
}
int n, m, x, y;
void init() {
memset(rank, 0, sizeof(rank));
for (int i = 1; i <= n; i++) {
p[i] = i;
}
}
int main() {
//将p全部置为1
while (scanf("%d", &n), n) {
scanf("%d", &m);
init();
for (int i = 0; i < m; i++) {
scanf("%d%d", &x, &y);
merge(x, y);
}
int ans = 0;
//统计一下有多少个连通分支
for (int i = 1; i <= n; i++) {
if (p[i] == i) ans++;
}
printf("%d\n", ans - 1);
}
return 0;
}
Note :文章虽然到这里就结束了,但是并查集并不只是这些,这些只是基础入门的东西,若有兴趣可以自己去探索更高深的。