图之最小生成树问题

时间:2021-01-29 12:35:50

图之最小生成树问题
这篇文章用来复习最小生成树问题.
此问题要解决的是在一个无向图中找出一颗最小生成树(minimum spanning tree),这个问题对有向图也有意义,不过找起来更困难.
引用wiki对Prim的描述:

In computer science, Prim's algorithm is a greedy algorithm that finds a minimum spanning tree for a weighted undirected graph.

大体而言,一个无向图的最小生成树就是图中那些连接所有顶点的边构成的树,且其总价值最低.
解决这个问题至少有两个算法,但我只实现了第一种:

  • 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 算法:

引用wiki对其的描述:
Kruskal's algorithm is a minimum-spanning-tree algorithm which finds an edge of the least possible weight that connects any two trees in the forest. It is a greedy algorithm in graph theory as it finds a minimum spanning tree for a connected weighted graph adding increasing cost arcs at each step

  • 连续的按照最小的权值选择边,并且当所选的边不产生圈时,就把它作为取定的边.
  • 形式上,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)),但是由于实际情况中会有孤立的点,故实践中该算法要比这个时间界要快一些.