图之最小生成树问题
这篇文章用来复习最小生成树问题.
此问题要解决的是在一个无向图中找出一颗最小生成树(minimum spanning tree),这个问题对有向图也有意义,不过找起来更困难.
引用wiki对Prim的描述:
大体而言,一个无向图的最小生成树就是图中那些连接所有顶点的边构成的树,且其总价值最低.
解决这个问题至少有两个算法,但我只实现了第一种:
- Prim
- Kruskal
Prim算法:
Prim算法和Dijkstra基本类似(参见Dijkstra博文),对每个顶点都有dv,pv,known,不同之处在于dv的定义不同,所以更新法则也不同,在每个顶点V被选取后,对每个与V邻接的未知顶点W,会触发的更新dw=min(dw,Cv-w),Cv-w指的是V-W的权值.即它并没有像Dijkstra那样判断累加值的大小,Prim只是在比较当前的大小,选取最小的那个权值.
由于仅是更新法则的不同,所以在实现的时候只需对其稍作改动即可:
// 使用邻接表存储图
//...
// 若此相邻点未被访问
if (getVertexState(node->verIndex) == NOVISIT)
{
// 获取更新的权值
int updateDis = node->weight;
// 判断更新后的权值是否小于现有的权值
if (updateDis < distance[node->verIndex])
{
// 更新现有权值
distance[node->verIndex] = updateDis;
// 记录当前顶点的上一个顶点编号
path[node->verIndex] = minDistanceVertex;
}
}
关于路径的记录:
其实这也谈不上路径的记录,path只是记录了一对一对的顶点.
例如 由< path的索引(顶点编号),索引值>构成
< V1,-1>,< V2,V1>,
< V3,V4>,< V4,V1>,
< V5,V7>,< V6,V7>,
< V7,V4>
这些顶点集合构成了一颗最小生成树.
关于Prim:
Prim算法是在无向图上运行的,不用堆时的运行时间为O(V2),对于一个稠密图来说还是不错的,使用二叉堆的运行时间是O(E · log(V)),但似乎最好是使用斐波那契堆与邻接表的结合,其运行时间为O(E + V · log(V)),但这仅仅是从时间复杂度上取得了优势.
实现中可能会用到的变量:
我使用的是邻接表存储图
// 记录距离的数组
int * distance;
// 记录路径的数组
int * path;
// 记录顶点访问状态
VISITEDSTATE * visited;
详细代码见我的github
Kruskal 算法:
-
连续的按照最小的权值选择边,并且当所选的边不产生圈时,就把它作为取定的边.
-
形式上,Kruskal算法像是在处理一个树的集合(森林),开始的时候,存在V颗单节点的树,而添加一条边就会使两棵树合并成一棵树,当算法结束的时候,就只有一棵树了,这棵树就是最小生成树:
-
对于一条边(u,v),若u和v在同一个集合中,那么连接他们的边就要放弃,因为他们已经连通了.因此若再添加此边就会增加一个圈.若这两个顶点不再同一个集合中,则将该边加入,并对含有(u,v)的这两个集合实施一次合并,因此可以通过并查集来实现.
-
代码描述如下:
// 最小生成树的边数 = 顶点树 - 1
while (EdgesAccepted < NumVertex - 1)
{
// 从由边建立的最小堆H中取出堆首元素 E = (u,v)
E = DeleteMin(H);
// 查找u顶点所在的集合
Uset = Find(u,S);
// 查找v顶点所在的集合
Vset = Fins (v,S);
if(Uset != Vset)
{
// 找到一条生成树的边
EdgesAccepted ++ ;
// 将Uset和Vset合并
setUnion(S,Uset,Vset);
}
}
关于Kruskal:
该算法的最坏情况情形运行时间为O(E · log(E)),获得了堆的特性,由于E=O(V2),因此运行时间实际上是O(E · log(V)),但是由于实际情况中会有孤立的点,故实践中该算法要比这个时间界要快一些.