连通性问题:
假如已知一个整数对序列,每个整数代表某种类型的一个对象,并且将p-q对解释为p与q连通。假定连通可传递:如果p与q连通,q与r连通,那么p与r连通。编写一个程序,从集合中过滤额外的连接对,如果前面的对表明p与q连通,则程序应该忽略p-q,并继续输入下一个对。
例如 :
- input output
- 3-4 3-4
- 4-9 4-9
- 8-0 8-0
- 2-3 2-3
- 5-6 5-6
- 2-9 (put out nothing because 2-3-4-9)
- 5-9 5-9
方案一:quick-find 快速查找算法
我们可以先将问题图形化表示,先看一个树状图。(《algorithms in C》)
快速查找(慢速并集)的思路很简单:只要两个数组有相同的元素出现,那么就可以认为这两个数组连通,可以把它归并到一起,如上图所示。
如果用代码实现归并呢?很简单,只需要一个数组id,如果p与q连通,那么让id[p] = id[q],当且仅当id[p] == id[q]的时候,p与q连通。
#include<stdio.h>
#define N 1000
//quick-find
int main() {
int i,p,q,t,id[N];
for(i = 0;i < N;i ++) {
id[i] = i; //初始化id数组,此时每个元素均不相通
}
while(scanf("%d %d",&p,&q) == 2) { //输入pq数对
if(id[p] == id[q]) { //如果p-q的id相同,那么p-q连通,continue到下一次输入循环
continue;
}
t = id[p];
for(i = 0;i < N;i ++) { //在id数组中查找与id[p]相同的数字
if(id[i] == t) { //如果相等,赋值为id[q],这样id[p]与id[q]及所有与id[p]id[q]有联系的数组都有相同的id了,即归并两个数组
id[i] = id[q];
}
}
printf("%d %d\n",p,q);
}
}
性质:对于具有N个对象,每个对象包括M个并集运算的连通性问题,快速查找的算法至少要执行MN条指令。
方案二:quick-union 快速并集算法
方案一中的查找每一次都遍历了数组id的全部,效率要低很多,方案二提供了一个更好的思路。
图中表述的连通分量叫做树,每棵树只有一个指向它本身的对象,即树根,其余的对象均指向或者间接指向树根。比如一棵树的根是9,那么id[9] == 9,如果3->4,4->9,即id[3] == 4,id[4] == 9,3和4均不指向它本身,最终的指向都为9,3和4都在以9为根节点的书里面。
这样我们只需要找到树的一个节点,就可以根据这个节点找到树根,从而判断是否具有连通性了。
#include<stdio.h>
#define N 1000
//quick-union
int main() {
int i,j,p,q,id[N];
for(i = 0;i < N;i ++) {
id[i] = i;
}
/*
p q 0 1 2 3 4 5 6 7 8 9
3 4 0 1 2 4 4 5 6 7 8 9
4 9 0 1 2 4 9 5 6 7 8 9
8 0 0 1 2 4 9 5 6 7 0 9
2 3 0 1 9 4 9 5 6 7 0 9
5 6 0 1 9 4 9 6 6 7 0 9
2 9 0 1 9 4 9 6 6 7 0 9
5 9 0 1 9 4 9 6 6 7 0 9
*/
while(scanf("%d %d",&p,&q) == 2) {
for(i = p;i != id[i];i = id[i]); //从节点p开始找,一直找到对应的树根并赋给i
for(j = q;j != id[j];j = id[j]); //从节点q开始找,一直知道对应的树根并赋给j
if(i == j) { //p和q的树根相同,进行下一次输入循环
continue;
}
id[i] = j; //p和q的树根不相同,则把q的树根赋给p的树根,那么这两棵数就建立了联系变成了一棵树
printf("%d %d\n",p,q);
}
}
性质:若M>N,快速并集算法可能要运行多于M*N/2条指令来解决一个拥有N个对象、M个对的连通性问题。
方案三:weighted quick-union 加权快速并集算法
有了以上两个算法做铺垫,加权快速算法也就不难理解了,加权快速算法是快速并集算法的修改版:不是任意连通第二棵树和第一棵树,而是比较两棵树节点数多少,把小的树的根节点连到大的树的根节点上。这样每个节点与树根的距离短,查找的运算效率也就高了。
代码如下:
#include<stdio.h>
#define N 1000
//weighted quick-union
int main() {
int i,j,p,q,id[N],size[N]; //定义一个数组size来记录树的大小
for(i = 0;i < N;i ++) {
id[i] = i;
size[i] = 1; //初始所有的树的size为1
}
while(scanf("%d %d",&p,&q) == 2) {
for(i = p;id[i] != i;i = id[i]);
for(j = q;id[j] != j;j = id[j]);
if(i == j) {
continue;
}
if(size[i] < size[j]) { //如果i树的大小小于j树,那么把小的i树的根节点设为j,i树合并到j树,并且j树的size增大为size[i]+size[j]
id[i] = j;size[j] += size[i];
}
else {
id[j] = i;size[i] += size[j];
}
printf("%d %d\n",p,q);
}
}
性质:加权快速并集算法判断N个对象的其中两个是否连通,最多要跟踪2*lgN个指针。
加权快速并集算法最坏的情况是,每个并集运算连接相等大小树。如果对象的数量小于2^n,则从任意节点到树根的距离小于n。
现在我们发现,我们的算法已经优化了很多,我们来讨论这样一个问题:能否有一个确保线性性能的算法?
理想情况下,我们希望每一个节点都直接指向根节点,而不希望付出变换大量指针的代价。其实这个很容易实现,我们可以通过修改树的结构来达到这样的目的。这种方法叫做路径压缩(path compression)。
实现方法是添加另一遍次经过每条路径,把每个路径id设置为指向根。最终结果是我们会看到一个扁平的树,接近快速查找算法的理想情况。
方案四:weighted quick-union with path compression by halving 对分路径压缩加权快速并集算法
对分路径压缩是通过让每一个链接在通向树上部的路径跳到下一个节点来实现压缩。
代码如下:
#include<stdio.h>
#define N 1000
//weighted quick-union with path compression by halving
int main() {
int i,j,p,q,id[N],size[N];
for(i = 0;i < N;i ++) {
id[i] = i;
size[i] = 1;
}
while(scanf("%d %d",&p,&q) == 2) {
for(i = p;id[i] != i;i = id[i]) {
id[i] = id[id[i]]; //找到上一个节点的路径,对分路径长度
}
for(j = q;id[j] != j;j = id[j]) {
id[j] = id[id[j]];
}
if(i == j) {
continue;
}
if(size[i] < size[j]) {
id[i] = j;size[j] += size[i];
}
else {
id[j] = i;size[i] += size[j];
}
printf("%d %d\n",p,q);
}
}
参考资料:《Algorithms in C》