连通性问题是《Algorithms in C++》讲述的第一个问题,目的是为了通过这个问题,来说明学习、分析、设计算法的一般过程,而我写这篇文章主要是为了整理思路,留下资料,加深理解。
一、问题描述
概念:和图论中的连通性概念一样,两点直接相连则连通,连通具有传递性,a-b,b-c=>a-c。则两点间接相连也连通。
假设:图G有N个顶点,顶点编号从0到N-1。
输入:M组数据,每组数据由两个整数对组成,写为(p,q),1<=p,q<=N,表示顶点p和顶点q之间有一条边。
输出:如果(p,q)之前没有连通,则在图G中增加一条连接这两个点的边,然后输出。
如果(p,q)之前就连通,则不做任何事。
如图所示:N为10,第一列为每次读入的整数对(p,q),如果p,q之前没有连通,则在第二列中打印出此整数对,并在图G中加入一条连通p,q的边。如果p,q之前已经连通,则第三列打印出从p到q的一条路。
二、基本算法之快速查找
2.1 处理思路
用到了图中连通分量的概念,如下图所示,N为10,定义一个数组cc[N]记录顶点属于哪个连通分量,连通分量用分量内部的一个顶点号来表示,初始化为cc[i]=i;0<=i<=9,也就是每个顶点都属于一个只包含自己的连通分量。
对每个整数对(p,q),分别查找p和q所属的连通分量。
如果两者属于同一个连通分量,则表示p,q之前就连通,不做任何事。
如果两者属于不同的连通分量,则表示p,q之前不连通,所以在p,q中加入一条边,使得两个连通分量合并为一个。
2.2 保存的数据及说明
数据保存是说我们记录了图G的什么信息?显然我们并没有记录图G的完整信息,因为N个顶点的无向图,至少需要N(N-1)/2个存储单位来保存所有边的信息,而我们只用到了cc[N],只保存了每一个顶点所属的连通分量的信息,而且根据2.1中描述,很容易根据cc[N]的信息来解决连通性问题,如果采用的是传统的图的表示方式,记录边的信息,反而在解决连通性问题的时候,不是那么直接。
所以在为一个问题设计算法的时候,(先?)要考虑需要保存问题的什么信息,往往这些信息和问题的本质联系紧密,我们要保存下来,用于实现算法。虽然连通性这个问题是图论中的概念,采用图的传统的表示方式貌似合情合理。但是上面的分析告诉我们,只维护一个记录连通性的数组cc[N],使得算法在时间、空间上都有更好的表现,思路也更加的清晰。(也就是大的类别概念-图论中的连通性,可以帮助我们更深刻的理解问题,但是在理解了问题之后,不一定非得用图的数据结构来解决问题)
2.3 抽象操作
查找:数据结构用的是数组,所以查找十分简单,O(1)时间完成。
合并:顶点p,q分别属于连通分量m,n,合并操作要将连通分量m和n合并成一个连通分量m(或者n),所以要遍历所有顶点,O(N)时间完成。
2.4 效率分析
显然时间复杂度为O(MN)。
2.5 一个重要的问题
图论中有一个定理,每个连通图G都有:边数>=顶点数减一。
在上面的问题中,边数为M,顶点数为N。每次输出一个整数对(q,p)都表示在图G中加入一条‘新’的边,可以证明,当向图G中加入了N-1条边后,图G就成为连通图,所以之后的整数对都是连通的,不用再判断。
这就是一个典型的例子,如果待解决的问题涉及到某个领域,往往这个领域里面的一两个结论可以为算法的设计提供很大的方便。所以码农要学的实在不嫌多······
三、基本算法之快速合并
3.1 基本思路
一样的数据结构,不一样的‘解释’。保存的信息一样,但是保存信息的方式的不一样。处理思路不变,但是具体的处理过程不一样。
cc[N]现在的‘意思’改变了。在快速查找算法中,cc[N]从始至终都保存了顶点i所属的连通分量,所以使得查找操作变的很简单,但是合并两个连通分量的时候,要遍历所有的顶点,使得合并操作稍嫌复杂。
现在,给出cc[N]的另一种解释,cc[N]的值只有在和顶点的编号相等的时候cc[i]==i,才表示所属的连通分量。比如初始化的时候就设置所有的顶点为cc[i]=I,表示每个顶点都属于一个只包含自己的连通分量。当cc[i]的值为j(j!=i)的时候,j是一个顶点的编号,表示i所属的连通分量和j所属的连通分量相同,要继续查看cc[j]的值,直到顶点编号和cc[顶点编号]的值相等的时候,才能确定i所属的连通分量。
3.2 抽象操作
查找操作:对每一个整数对(p,q),分别查询cc[p],cc[q],直到顶点编号i满足cc[i]==i,每次查找迭代过程从1此到N-1次不等,具体的次数如何确定参看后文。
合并操作:十分简单,设cc[p]的查找的终点为顶点m,cc[q]查找的终点为n,cc[m]=m.cc[n]=n,且m!=n,合并只需要将cc[m]=n即可。(能不能是cc[n]=m?两者有什么区别?这会影响程序的时间复杂度,具体分析参看后文。)
3.3 保存的数据及解释
下图是对上文例子的操作,从图中可以很直接的看出,虽然我们采用的是数组这个数据结构,但是在逻辑层面,是由树构成的。
输入第一个整数对(3,4),cc[3]=3,cc[4]=4,顶点3、4不属于同一个连通分量,则执行cc[3]=4.则第3号顶点,就成了叶子结点,以后对3号顶点的查找操作都要一层一层向上,直到跟结点。
3.4 效率分析
显然这个快速合并算法的时间复杂度要比快速查找算法快,因为每次查找的时候并不需要遍历所有的cc[N],但是快多少呢?等学到后面再来解答。
四、快速合并算法的改进版本-带权重的快速合并
4.1 基本思想
如果你对树这个数据结构有一定的了解,你肯定会知道这个数据结构很犀利,比如红黑树,可以在ln(n)的时间存取数据,为什么?因为平衡的力量,这也就是树->二叉树->平衡二叉树出现的原因。快速合并算法合并操作非常快,但是查找操作偏慢,原因就是在查找一个顶点的连通分量时,要从树的叶子结点一级一级的向上找,直到找到根结点(cc[i]==i),所以,如果我们可以在合并的时候,把树的高度考虑进去,尽量的降低树的高度,那么自然就使得查找的步数减少,效率提高。如果看到这里还不明白,那么就去草稿纸上去手动跟踪快速合并算法以及快速合并算法的改进版本,用这样的输入做测试:N=6,整数对分别为(1,2)(2,3)(3,4)(4,5)(1,5)(1,5).....画出两者形成的连通分量数组的逻辑结构‘树’,观察树的高度,一定会恍然大悟。
4.2 效率分析