图 - 最小生成树

  加权图是一种为每条边关联一个权值或是成本的图模型。这种图可以天然地表示许多应用。在一幅航空图中,边表示航线,权值则能够表示距离或是费用。在这些情形中,最使人感兴趣的天然是将成本最小化。这里用加权无向图模型来解决最小生成树:给定一幅加权无向图,找到它的一棵最小生成树。html

 

  图的生成树是它的一棵含有其全部顶点的无环连通子图。一幅加权图的最小生成树(MST)是它的一棵权值(树中全部边的权值之和)最小的生成树。算法

  

 

   

  计算最小生成树的两种经典算法:Prim 算法和 Kruskal 算法。数组

 

  1.原理数据结构

  树的两个重要性质:ide

  1.用一条边链接树中的任意两个顶点都会产生一个新的环;函数

  2.从树中删除一条边将会获得两棵独立的树。性能

  这两条性质是证实最小生成树的另外一条基本性质的基础,而由这条基本性质就可以获得最小生成树的算法。this

  

 

 

  1.切分定理spa

  咱们称之为切分定理的这条性质将会把加权图中的全部全部顶点分为两个集合,检查横跨两个集合的全部边并识别哪条边应属于图的最小生成树。3d

  一般,咱们指定一个顶点集并隐式地认为它地补集为另外一个顶点集来指定一个切分。这样,一条横切边就是链接该集合地一个顶点和不在该集合地另外一个顶点地一条边。

 

  切分定理:在一幅加权图中,给定任意得切分,它的横切边中权重最小者必然属于图的最小生成树。

  注意,权重最小的横切边并不必定是全部横切边中惟一属于图的最小生成树的边。实际上,许多切分都会产生若干条属于最小生成树的横切边:

 

 

  2.贪心算法

  切分定理是解决最小生成树问题的全部算法的基础。更确切的说,这些算法都是一种贪心算法的特殊状况:使用切分定理找到最小生成树的一条边,不断重复直到找到最小生成树的全部便。这些算法之间的不一样之处在于保存切分和断定权重最小的横切边的方式,但它们都是一下性质特殊状况:

  最小生成树的贪心算法:下面这种算法会将含有 V 个顶点的任意加权连通图中属于最小生成树的边标记为黑色:初始状态下全部边均为灰色,找到一种切分,它产生的横切边均不为黑色。将它权重最小的横切边标记为黑色。反复,直到标记了 V - 1 条黑色边为止。

  

 

   

  2.加权无向图的数据类型

  

namespace MinimumSpanningTrees
{
    public class Edge:IComparable<Edge>
    {
        private int v;//顶点之一
        private int w;//另外一个顶点
        private double weight;//边的权重

        public Edge(int v, int w, double weight)
        {
            this.v = v;
            this.w = w;
            this.weight = weight;
        }

        public double Weight()
        {
            return weight;
        }

        public int Either()
        {
            return v;
        }

        public int Other(int vertex)
        {
            if (vertex == v)
                return w;
            else if (vertex == w)
                return v;
            else
                throw new ArgumentException();
        }



        public int CompareTo([AllowNull] Edge other)
        {
            if (this.Weight() < other.Weight())
                return -1;
            else if (this.Weight() > other.Weight())
                return 1;
            else
                return 0;
        }

        public override string ToString()
        {
            return string.Format("%d-%d %.2f",v,w,weight);
        }
    }
}

  

  加权无向图的实现使用了 Edge 对象。

namespace MinimumSpanningTrees
{
    public class EdgeWeightedGraph
    {
        private int v;//顶点总数
        private int e;//边的总数
        private List<Edge>[] adj;//邻接表

        public EdgeWeightedGraph(int V)
        {
            this.v = V;
            e = 0;
            adj = new List<Edge>[V];
            for (int _v = 0; _v < V; _v++)
            {
                adj[_v] = new List<Edge>();
            }
        }

        public int V()
        {
            return v;
        }

        public int E()
        {
            return e;
        }

        public void AddEdge(Edge edge)
        {
            int v = edge.Either();
            int w = edge.Other(v);

            adj[v].Add(edge);
            adj[w].Add(edge);

            e++;
        }

        public IEnumerable<Edge> Adj(int _v)
        {
            return adj[_v];
        }
    }
}

 

 

 

  为了整洁,用一对 int 值和一个 double 值表示每一个 Edge 对象。实际的数据结构是一个链表,其中每一个元素都是一个指向含有这些值的对象的指针。须要注意的是,虽然每一个Edge对象都有两个引用,但图中的每条边所对应的 Edge 对象只有一个。

 

  用权重来比较边

  Edge 类实现了 IComparable 接口的 CompareTo 方法。一幅加权无向图中的边的天然次序就是按权重排序。

 

  平行边

  和无环图的实现同样,这里也容许存在平行边。咱们也能够用更复杂的方法来消除平行边,好比保留平行边中权重最小者。

 

  自环  

  容许存在自环,这对最小生成树算法没有影响,由于最小生成树确定不会含有自环。

 

  有个 Edge 对象以后用例的代码就能够变得更加干净整洁。这也有小的代价:每一个邻接表的结点都是一个都是一个指向 Edge 对象的引用,它们含有一些冗余的信息, v 的邻接链表中的每一个结点都会用一个变量保存 v 。使用对象也会带来一些开销。虽然每条边的 Edge 对象都只有一个,但邻接表中仍是会含有两个指向同一 Edge 对象的引用。

  另外一种普遍使用的方案是与 Graph 同样,用两个结点对象表示一条边,每一个结点对象都会保存顶点的信息和边的权重。这种方法也是有代价的 -- 须要两个结点,每条边的权重都会被保存两次。

 

  3.最小生成树 API

  按照惯例,在 API 中会定义一个接受加权无向图为参数的构造函数而且支持可以为用例返回图的最小生成树和其权重的方法。那么应该如何表示最小生成树?因为图的最小生成树是图的一幅子图而且同时也是一棵树,所以能够有一下选择:

  1.一组边的列表;

  2.一幅加权无向图;

  3.一个以顶点为索引且含有父结点连接的数组。

  在为了各类应用选择这些表示方法时,咱们但愿尽量给予最小生成树的实现以最大的灵活性,采用以下 API :

 

 

  4.Prim 算法

  Prim 算法,它的每一步都会为一棵生长中的树添加一条边。一开始这棵树只有一个顶点,而后会向它添加 V-1 条边,每次老是将下一条链接树中的顶点与不在树中的顶点且权重最小的边加入树中。

 

   Prim 算法可以获得任意加权连通图的最小生成树。这棵不断生长的树定义了一个切分且不存在树中的横切边。该算法会选择权重最小的横切边并根据贪心算法不断将他们标记。

  

  数据结构

   实现 Prim 算法须要一些简单常见的数据结构。具体来讲,咱们会用一下方法表示树中的顶点,边和横切边:

  1.顶点:使用一个由顶点索引的布尔数组 marked[ ] ,若是顶点 v 在树中,那么 marked[v]  的值为 true。

  2.边:选择如下两种数据结构之一:一条队列 mst 来保存最小生成树中的边,或者一个由顶点索引的Edge对象的数组 edgeTo[ ] ,其中 edgeTo[ v ] 为将 v 链接到树中的 Edge 对象;

  3.横切边:使用一条优先队列 MinPQ<Edge> 来根据权重比较全部边。

  有了这些数据结构就能够回答 “哪条边的权重最小” 这个基本问题了。

 

  维护横切边的集合

  每当咱们向树中添加了一条边后,也向树中添加一个顶点。要维护一个包含全部横切边的集合,就要将链接这个顶点(新加入树)和其余全部不在树中的顶点的边加入优先队列。但还有一点:链接新加入树中的顶点和其余已经在树中的顶点的全部边都要失效(这样的边已经不是横切边了,由于它的两个顶点都在树中)。Prim 算法的即时实现能够将这样的边从优先队列中删掉,先来看一种延时实现,将这些边先留在优先队列中,等到要删除它们的时候再检查边的有效性。

  算法构造最小生成树的过程:

  

  1.将顶点 0 添加到最小生成树,将它的邻接链表中的全部边添加到优先队列中

  2.取权重最小的边 0-7,将顶点 7 和 边 0-7 添加到最小生成树中,将顶点 7 的邻接链表中的全部边添加到优先队列中。

  3.将顶点 1 和边 1-7 添加到最小生成树中,将顶点的邻接链表中的全部边添加到优先队列中。

  4.将顶点 2 和边 0-2 添加到最小生成树,将边 2-3 和 6-2 添加到优先队列。边 2-7 和 1-2 失效(由于边的两个顶点都在树中,最后删除)。

  5.将顶点 3 和边 2-3 添加到最小生成树,将边 3-6 添加到优先队列。边 1-3 失效。

  6.将顶点 5 和边 5-7 添加到最小生成树,将边 4-5 添加到优先队列。边 1-5 失效。

  7.从优先队列删除失效的边 1-3,1-5 和 2-7 。

  8.将顶点 4 和边 4-5 添加到最小生成树中,将边 6-4 添加到优先队列。边 4-7 和 0-4 失效。

  9.从优先队列删除失效的边 1-2,4-7 和 0-4 。

  10.将顶点 6 和边 6-2 添加到最小生成树中,和顶点 6 相关联的其余边均失效。

  在添加了 V 个顶点以及 V-1 条边以后,最小生成树就完成了。优先队列中剩下的边都是无效的,能够不用去检查。

 

  实现

  咱们使用一个私有方法 Visit()来为树添加一个顶点,将它标记为“已访问”并将与它关联的全部未失效的边加入优先队列,以保证队列含有全部链接树顶点和非树顶点的边(也可能含有一些已失效的边)。代码的内循环是算法的具体实现:从优先队列中取出一条边并将它添加到树中(若是没有失效),再把这条边的另外一个顶点也添加到树中,而后用新顶点做为参数调用 Visit 方法来更新横切边的集合。 Weight 方法能够遍历树的全部边并获得权重之和。

namespace MinimumSpanningTrees
{
    public class LazyPrimMST
    {
        private bool[] marked;//最小生成树的顶点
        private Queue<Edge> mst;//最小生成树的边
        private MinPQ<Edge> pq;//横切边(包括失效的边)

        public LazyPrimMST(EdgeWeightedGraph G)
        {
            pq = new MinPQ<Edge>();
            marked = new bool[G.V()];
            mst = new Queue<Edge>();

            Visit(G,0);
            while (!pq.Count > 0)
            {
                Edge e = pq.Dequeue();

                int v = e.Either();
                int w = e.Other(v);
                if (marked[v] && marked[w])
                    continue;
                pq.Enqueue(e);

                if (!marked[v])
                    Visit(G,v);
                if (!marked[w])
                    Visit(G,w);
            }
        }

        private void Visit(EdgeWeightedGraph G, int v)
        {
            marked[v] = true;
            foreach (Edge e in G.Adj(v))
            {
                if (!marked[e.Other(v)])
                    pq.Enqueue(e);
            }
        }

        public IEnumerable<Edge> Edges()
        {
            return mst;
        }

        public double Weight()
        {
            return mst.Sum(o=>o.Weight());
        }
    }
}

  

  性能

  Prim 算法的延时实现计算一幅含有 V 个顶点和 E 条边的连通加权无向图的最小生成树所需的空间和 E 成正比,所需的时间与 ElogE成正比(最坏状况)。算法的瓶颈在于优先队列的删除和添加方法中比较边的权重的次数。优先队列最多可能含有 E 条边,这就是空间需求的上限。在最坏状况下,一次插入成本为 ~lgE ,删除最小元素的成本为 ~2lgE 。由于最多只能插入 E 条边,删除 E 次最小元素,时间上限显而易见。

 

  5. Prim 算法的即时实现

  要改进 LazyPrimMST ,能够尝试从优先队列中删除失效的边,这样优先队列就只有树顶点和非树顶点之间的横切边,但还能够删除更多的边。关键在于,咱们感兴趣的只是链接树顶点和非树顶点中权重最小的边。当咱们将顶点 v 添加到树中,对于每一个非树顶点 w 产生的变化只可能使得 w 到最小生成树的距离更近。简而言之,咱们不须要在优先队列中保存全部从 w 到树顶点的边 —— 而只需保存其中权重最小的那条。在将 v 添加到树中后检查是否须要更新这条权重最小的边(由于 v-w 的权重可能更小)。咱们只需遍历 v 的邻接链表就能够完成这个任务。换句话说,咱们只会在优先队列中保存每一个非树顶点 w 的一条边:将它与树中顶点连起来的权重最小的那条边。将 w 和树的顶点链接起来的其余权重较大的边早晚都会失效,因此不必在优先队列中保存他们。

  

 

   

  算法类 PrimMST 将 LazyPrimMST 类中 marked[ ] 和 mst[ ] 替换为两个顶点索引的数组 edgeTo[ ] 和 distTo[ ] :

  1.若是顶点 v 不在树中但至少含有一条边和树相连,那么 edgeTo[v] 是将 v 和树链接的最短边,distTo[v] 为这条边的权重。

  2.全部这类顶点 v 都保存在一条索引优先队列中,索引 v 关联的值是 edgeTo[v] 的边的权重。

  这些性质的关键在于优先队列中的最小键便是权重最小的横切边的权重,而和它相关联的顶点 v 就是下一个将被添加到树中的顶点。marked[ ] 数组已经没有必要了,由于判断条件 !marked[w] 等价于 distTo[w] 是无穷的(且 edgeTo[w] 为 null)。要维护这些数据结构,PrimMST 会从优先队列中取出一条边并检查它的邻接链表中的每条边 v-w 。若是 w 已经被标记过,那这条边就已经失效了;若是 w 不在优先队列中或者 v-w 的权重小于目前已知的最小值 edgeTo[w],代码会更新数组,将 v-w 做为将 w 和树链接的最佳选择。

namespace MinimumSpanningTrees
{
    public class PrimMST
    {
        private Edge[] edgeTo;//距离树最近的边
        private double[] distTo;//distTo[w]=edgeTo[w].weight()
        private bool[] marked;//若是 v 在树中则为 true
        public IndexMinPQ<double> pq;//有效的横切边

        public PrimMST(EdgeWeightedGraph G)
        {
            edgeTo = new Edge[G.V()];
            distTo = new double[G.V()];
            marked = new bool[G.V()];

            for (int v = 0; v < G.V(); v++)
            {
                distTo[v] = Double.MaxValue;
            }

            pq = new IndexMinPQ<double>(G.V());

            distTo[0] = 0.0;
            pq.Insert(0,0.0); //用顶点 0 和权重 0 初始化 pq
            while (pq.Count > 0)
            {
                Visit(G,pq.DelMin());//将最近的顶点添加到树中
            }
        }

        private void Visit(EdgeWeightedGraph G, int v)
        {
            //将顶点 v 添加到树中,更新数据
            marked[v] = true;
            foreach (Edge e in G.Adj(v))
            {
                int w = e.Other(v);

                if (marked[w])
                    continue;

                if (e.Weight() < distTo[w])
                {
                    edgeTo[w] = e;
                    distTo[w] = e.Weight();

                    if (pq.Contains(w))
                        pq.Change(w, distTo[w]);
                    else
                        pq.Insert(w,distTo[w]);
                }
            }
        }
    }
}

  

 

 

  Prim 算法的即时实现计算一幅含有 V 个顶点和 E 条边的连通加权无向图的最小生成树所需的空间和 V  成正比,所需的时间与 ElogV成正比(最坏状况)。

 

  6.Kruskal 算法

  第二种生成最小生成树的主要思想是按照边的权重顺序(从小到大)处理它们,将边加入最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有 V-1 条边为止。这些边逐渐由一片森林合共成一棵树。

 

  为何 Kruskal 算法可以计算任意加权连通图的最小生成树?

  若是下一条将被加入最小生成树中的边不会和已有的边构成环,那么它就跨越了由全部和树顶点相邻的顶点组合的合集以及它们的补集所构成的一个切分。由于加入的边不会造成环,它是目前已知的惟一一条横切边且是按照权重顺序选择的边,因此它必然是权重最小的横切边。所以,该算法可以连续选择权重最小的横切边,和贪心算法一致。

 

  Prim 算法是一条边一条边地来构造最小生成树,每一步都为一棵树添加一条边。 Kruskal 算法构造最小生成树地时候也是一条边一条边地构造,但不一样的是它寻找的边会链接一片森林中的两棵树。咱们从一片由 V 棵单顶点的树构成的森林开始并不断将两棵树合并(用能够找到的最短边)直到只剩下一棵树,它就是最小生成树。

 

 

  实现

  咱们将会使用一条优先队列来将边按照权重排序,用一个 union-find 数据结构来识别会造成环的边,以及一条队列来保存最小生成树的全部边。

    public class KruskalMST
    {
        private Queue<Edge> mst;
        public KruskalMST(EdgeWeightedGraph G)
        {
            mst = new Queue<Edge>();//最小生成树的边
            MinPQ<Edge> pq = new MinPQ<Edge>(G.Edges);//全部边
            UF uf = new UF(G.V());

            while (!pq.IsEmpty() && mst.Count < G.V() - 1)
            {
                Edge e = pq.DelMin();//从 pq 获得权重最小的边和它的顶点
                int v = e.Either(), w = e.Other(v);

                if (uf.Connected(v, w))//忽略失效的边
                    continue;
                uf.Union(v,w);//合并份量
                mst.Enqueue(e);//将边添加到最小生成树中
            }
        }
    }

 

  Kruskal 算法的计算一幅含有 V 个顶点和 E 条边的连通加权无向图的最小生成树所需的空间和 E 成正比,所需的时间和 ElogE 成正比(最坏状况)。

  算法的实如今构造函数中使用全部边初始化优先队列,成本最多为 E 次比较(请见 union-find)。优先队列构造完成后,其他的部分和 Prim 算法彻底相同。优先队列中最多可能含有 E 条边,即所需空间的上限。每次操做的成本最多为 2lgE 次比较,这就是时间上限的由来。 Kruskal 算法最多还会进行 E 次 Connected() 和 V 次 Union()操做,但这些成本相比 ElogE 的总时间的增加数量级能够忽略不计。

 

  与 Prim 算法同样,这个估计是比较保守的,由于算法在找到 V-1 条边以后就会终止。实际成本应该与 E+E0 logE 成正比,其中 E0 是权重小于最小生成树中权重最大的边的全部边的总数。尽管拥有这个优点,Kruskal 算法通常仍是比 Prim 算法要慢,由于在处理每条边时,两种方法都要完成优先队列操做以外,它还须要进行一次 Connect() 操做。

 

 

  

 

 

相关文章
相关标签/搜索