在一个有n个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按照一定的顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集——顾名思义,'并'即合并两个集合;'查'即查找某个元素属于哪个集合。合并和查找就是并查集常见的两种操作。一般地,在合并操作之前应该先使用查找操作判断两个元素是否属于同一集合。以上,就是利用并查集解决问题的关键。依惯例,写算法实现之前还是取一题HDU_1232畅通工程:
Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府"畅通工程"的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
思考一下,如何解决这个问题呢?熟悉图论的同学可能会想到构建一个无向图,然后求出图中连通分支的个数,该数减一就是所求。这的确是个好主意,但是图论我不熟:-,因此只能求诸于并查集,而且对于此类问题并查集写起来更加方便。我先给出思路:首先建立一个数组town,下标1~n,对应着初始化为1~n,表示每个元素构成一个单一的集合,接下来就是按照输入数据合并集合,我们定义集合的合并是以较小的元素代表该集合(换言之,较大的也可以),比如一条道路连通10和100这两个城镇,town[10]和town[100]就是10和100这两个城镇分属的集合,找到其中的较大值(如果相等的话,说明两个元素属于同一个集合,就不用执行合并操作了),然后遍历town数组,将所有较大值更新为较小值,就完成了两个集合的合并,重复这个过程直至所有的输入数据读取完毕,求出集合的个数(town[i] == i代表一个集合),然后减一即为所求。下面是在该思路指导下的实现方式:
#include<stdio.h> // 62MS
#define MAXN 1000
int find(int x);
void merge(int x, int y, int n);
int town[MAXN+10];
int main(void)
{
int n, m, town1, town2, count;
while(1)
{
scanf("%d", &n);
if(n == 0)
break;
scanf("%d", &m);
for(int i = 1; i <= n; i++)
town[i] = i;
if(m == 0)
{
printf("%d
", n-1);
continue;
}
count = 0;
while(m--)
{
scanf("%d%d", &town1, &town2);
merge(town1, town2, n);
}
for(int i = 1; i <= n; i++)
{
if(town[i] == i)
count++;
}
printf("%d
", count-1);
}
return 0;
}
int find(int x)
{
return town[x];
}
void merge(int x, int y, int n)
{
int either = find(x);
int another = find(y);
if(either == another)
return;
else if(either < another)
{
for(int i = 1; i <= n; i++)
if(town[i] == another)
town[i] = either;
}
else
{
for(int i = 1; i <= n; i++)
if(town[i] == either)
town[i] = another;
}
}
当然了,上面之所以写62MS AC的原因就是还可以优化:-,我们敬爱的计算机科学中,并查集是作为一种树形的数据结构出现的。因此我们可以考虑将树和并查集联系起来解决这个问题。思路是:每个集合用一棵"有根树"表示,定义数组set[1...n],如果set[i] = i 则i 表示本集合,并是集合对应树的根;如果set[i] = j, i≠j, 则j 是i 的父节点。实现方式如下:
#include<stdio.h> // 31MS
#define MAXN 1000
int find(int x); // 找根节点(查)
void merge(int x, int y); // 合并集合(并),因此得名并查集
int town[MAXN+10];
int main(void)
{
int n, m, town1, town2, count;
while(1)
{
scanf("%d", &n);
if(n == 0)
break;
scanf("%d", &m);
for(int i = 1; i <= n; i++) // 每个元素单独构成一个集合
town[i] = i;
if(m == 0)
{
printf("%d
", n-1);
continue;
}
count = 0;
while(m--)
{
scanf("%d%d", &town1, &town2);
merge(town1, town2); // 放到同一个集合里
}
for(int i = 1; i <= n; i++)
{
if(town[i] == i) // count统计集合的个数
count++;
}
printf("%d
", count-1); // 容易看出,集合个数减一即为所求最少道路数
}
return 0;
}
int find(int x)
{
while(town[x] != x)
{
x = town[x];
}
return x;
}
void merge(int x, int y)
{
int either = find(x);
int another = find(y);
if(either != another) // 该行代码有无皆可
town[either] = another; // 或者town[either] = town[another]亦可,因为another==town[another]
}
如上注释,我们把时间从62MS提高到了31MS,在OJ给出的测试数据的情况下,速度刚好提高了一倍。但是仍然存在性能瓶颈,最坏情况下的时间效率并没有提高,为了突破效率瓶颈,可以双向优化,对合并算法而言,采用将深度较小的树合并到深度较大的树上的方法,对于查找算法而言,可以进行路径压缩,方法就是找到根节点后,修改查找路径上的所有节点,使其全部指向根节点,降低了树的深度,从而使下次查找的时间缩短。