贪婪算法 — 最小生成树Kruskal和Prim算法

文章目录

    • 前言
      • Kruskal算法
        • 1)问题描述
        • 2)算法思想
        • 3)算法的正确性
        • 4)数据结构选择及时间复杂度
        • 5)C++实现
      • Prim算法
        • 1)问题描述
        • 2)算法思想
        • 3)算法的正确性
        • 4)算法的数据结构选取和时间复杂性
        • 5)带有数据结构伪代码

前言

在讨论图的时候,我们利用DFS和BFS搜索构成过生成树。但是在加权无向图中,由DFS或BFS生成的生成树一般不具有最小成本(即生成树的边集之和)。

但是现实生活中又有最小生成树的应用,比如在一个城市中的送水站或快递公司应该建在哪里的问题。我们优秀的前辈们开始设计算法来解决这个实际问题。这次要介绍的Kruskal和Prim同样也是以作者名字命名的算法。

这两个算法的逻辑都很简单,关键点是要明白为什么这两个算法是可行的,也就是要验证算法的正确性。同时,还要注意数据结构的选取。

Kruskal算法

1)问题描述

生成树问题我们已经在图的那一部分简单介绍过,在这里不再赘述。

对于给定的一加权无向图,有n个顶点,设计算法选择(n-1)条边构成最小成本生成树。

2)算法思想

仍然使用贪婪算法思想,给出贪婪准则。

贪婪准则: 每步选择一条边,选择边的依据是从剩下的边中选择一条成本最小且不会产生环路的边加入已选择的边集。

/**
Kruskal
在n个顶点的网络中寻找一颗最小成本生成树
令T是选定的边集,初始时T为空
令E是网络的边集
while(!E.empty() && |T|!= n-1)
{
    令(u, v)是E中一条成本最小的边
    从E中删除边(u,v)
    if((u,v)在T中不会产生环路)
        把边(u,v)加入T
}
if(|T| == n-1)
    T是一颗最小成本生成树
else
    图不是连通图,没有生成树

*/   

3)算法的正确性

证明算法的正确性分为两个部分,一是证明通过算法可以生产一颗生成树,当算法失败时,图不连通,不存在生成树;二是产生的树具有最小的成本。

第一部分:
首先证明算法失败的情况,即找不到一条加入已选择的边集不构成环的边,算法结束。但在最后一步 T < n − 1 TT<n1, 说明有顶点没有被搜索到,它与其它任何的顶点都没有边进行联接,图不连通,生成树不存在。

如果图连通,证明返回的是一颗生成树。还是根据贪婪准则,最后的边集T只要再加任意一条边,就会形成环路。也就是说,在一个连通图中,只有构成环路的边没有被选中。而一个带有环路的连通图,去掉构成环路的边,仍然是连通图。不存在环路的连通边集,便是一颗生成树。

第二部分

这一部分我一开始看的《数据结构、算法与应用》读了两遍没读下去,读下去了好几遍又没看懂。打开PPT全英文读了几遍,没有老师的讲解英文继续劝退。最后还是打开了我中文版的《算法导论》。

总之,算法导论的解释最清晰、最基础,《数据结构、算法与应用》因为不涉及基本定理的证明,只注重了文字化逻辑推导,我真的觉得很难懂。

我尽量表达的简洁一点,有理解不到位的地方欢迎指正。

以下论证基于下图:
贪婪算法 — 最小生成树Kruskal和Prim算法_第1张图片

  • 最小生成树不唯一,在一个有权无向图中可能会存在多个最小生成树。
  • 设集合A是某个最小生成树的子集,在初始时A为空。
  • A ∪ ( u , v ) A\cup(u,v) A(u,v)同样是某一最小生成树子集时,将 ( u , v ) (u,v) (u,v)加入集合A,称 ( u , v ) (u,v) (u,v)为集合A的安全边
  • 无向图 G = ( V , E ) G = (V,E) G=(V,E)中的一切割 ( S , V − S ) (S,V-S) (S,VS)是集合V的一个划分,上图中的边 ( u , v ) (u,v) (u,v)两边的端点位于两个集合,称 ( u , v ) (u,v) (u,v)横跨切割。如果集合A中不存在横跨该切割的边,称该切割尊重集合A。在横跨的所有边中,权值最小的边,称为轻边。在上图中,若 ( u , v ) (u,v) (u,v)的权值在三条横跨的边中最小,那么 ( u , v ) (u,v) (u,v)便是轻边。
  • 定理:设A是某个最小生成树的子集,如果 ( S , V − S ) (S,V-S) (S,VS)是尊重集合A的一个切割,且边 ( u , v ) (u,v) (u,v)又是横跨该切割的轻边,那么 ( u , v ) (u,v) (u,v)是集合A的安全边。 A ∪ ( u , v ) A\cup(u,v) A(u,v)同样是某一最小生成树子集。
  • 上述定理证明:由于切割尊重集合A,那么在图中将A分为两个连通分量,两个连通分量如果要形成一颗最小生成树,必须加一条横跨切割的边。这条边一定会是轻边,否则成本一定比选择轻边的成本大。
  • 在算法的任意时刻,A是一颗森林,A中的每个连通分量是一颗树(可能只包含一个节点)。
  • 将A由森林合并为一颗树,需要添加 ( V − 1 ) (V-1) (V1)条安全边。Kruskal算法加入的每一条边都是安全边。

4)数据结构选择及时间复杂度

  • 由于需要非递减顺序选择边,我们使用最小堆实现。若图中有e条边,那么堆的初始化需要用时 O ( e ) O(e) O(e),一条边的提取时间需要用时 O ( l o g ( e ) ) O(log(e)) O(log(e))
  • 为了确定边 ( u , v ) (u,v) (u,v)是否会产生环路,需要检查u,v是否属于同一顶点集。我们需要用到并查集(union-find)。假设我们使用按秩合并加路径压缩算法实现,若图中有e条边,n个节点,那么最多需要2e次find()操作,(n-1)次unite()操作,时间复杂度为 O ( e ) O(e) O(e)
  • 最后将n-1条边存储,需要时间复杂度为 O ( n ) O(n) O(n)
  • 总时间复杂度为 O ( n + e l o g ( e ) ) O(n+elog(e)) O(n+elog(e))

5)C++实现

/**
Kruskal
weightedEdge类,定义了一个向数据类型T的类型转换,返回
的是边上的权。
*/

bool kruskal(weightedEdge<T>* spanningTreeEdges)
{
    int n = numberOfVertices();
    int e = numberOfEdges();

    weightedEdge<T>* edge = new weightedEdge[e+1];
    int k = 0;
    for(int i = 1; i<=n; i++)// 提取出所有的边
    {
        vertexIterator<T> *ii = new iterator(i);
        int j;
        T w;
        while((j = ii->next(w)) != 0) // 在这里给w赋值
        {
            if(i<j) // 防止重复
            {
                edge[++k] = weightedEdge<T>(i,j,w);
            }
        }
    }
    // 初始化最小堆
    minHeap<weightedEdge<T>> heap(1);
    heap.initialize(edge,e);

    // 声明并查集结构
    UnionFind uf(n);

    k=0;
    while(e > 0 && k < n - 1)
    {
        // 从最小堆中提取边
        weightedEdge<T> x = heap.top();
        heap.pop();
        e--;
        int a = uf.find(x.vertex1());
        int b = uf.find(x.vertex2());
        if(a != b) // 边不会形成环
        {
            spanningTreeEdges[k++] = x;
            uf.unite(a,b);
        }
    }
    return (k == n - 1);
}

Prim算法

1)问题描述

与Kruskal相同。

2)算法思想

使用贪婪算法思想,给出贪婪准则。

贪婪准则:从剩余的边中,选择一条成本最小的边,并且把它加入到已选的边集中。

/**
//假设网络最少有一个顶点
令T是入选的边集,初始化T为空
令TV为是已在树中的顶点集,我们随机选择一个顶点,TV={1}
E为网络的边集

while(!E.empty() && T != n-1)
{
	令(u,v)是一条成本最小的边,且u∈TV,v∉TV
	if(没有这样的边)
		break;
	从E中删除边(u,v)
	把边(u,v)加入T
	把顶点v加入TV
}
if(T == n-1)
	T是一颗最小成本生成树
else
	图不是连通图,没有生成树
*/

3)算法的正确性

同样证明①能生成树、②生成的树为最小生成树。

第一部分:类似Kruskal
第二部分:同Kruskal,该贪婪准则选取的边同样都为安全边。

4)算法的数据结构选取和时间复杂性

  • 因为每一步要选取一条可以形成树的最小成本的边,仍然使用最小堆来维护。
  • 保存节点顺序的数据结构可用数组实现
  • 同时还需要保存前序节点和每个节点v与树中的节点的所有边中最小边的权重,需要两个数组。
  • 对每个顶点,需要遍历其所有的邻接节点,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 算法总时间负复杂度为 O ( n 2 ) O(n^2) O(n2)

5)带有数据结构伪代码

贪婪算法 — 最小生成树Kruskal和Prim算法_第2张图片

你可能感兴趣的:(数据结构)