最小生成树(C#)(算法第四版)

本文介绍了最小生成树的概念,针对加权无向图,讲解了Prim算法的延时和即时两种实现方式,以及Kruskal算法的详细过程。讨论了算法的空间和时间复杂度,并提供了C#代码实现。

最小生成树 - 加权无向图(算法第四版)

约定

这里只简单的介绍一般情况下的最小生成树问题,因此最加权图数据做了一些规定:

  1. 只考虑连通图(如果不满足会产生最小森林)
  2. 所有边的权重不同(不满足会出现多个最小生成树)

加权图数据结构

using System;
using System.Collections.Generic;
using System.Text;

namespace{
    public class EdgeWeightedGraph
    {
        private List<Edge>[] _adj; //邻接表
        private int _v; //结点数
        private int _e; //边数

        /// <summary>
        /// 按文件数据生成一个加权无向图,
        /// </summary>
        /// <param name="path">图文件路径</param>
        public EdgeWeightedGraph(string path)
        {
            string line;
            System.IO.StreamReader sr = new System.IO.StreamReader(path);
            _v = Convert.ToInt32(sr.ReadLine());
            _e = 0;
            _adj = new List<Edge>[_v];
            for (int i = 0; i < _v; i++) { _adj[i] = new List<Edge>(); }
            while ((line = sr.ReadLine()) != null)
            {
                string[] str = line.Split(' ');
                int a = Convert.ToInt32(str[0]);
                int b = Convert.ToInt32(str[1]);
                float w = (float)Convert.ToDouble(str[2]);
                AddEdge(new Edge(a,b,w));
            }
        }

        public int V => _v;
        public int E => _e;

        public EdgeWeightedGraph(int v)
        {
            this._v = v;
            _adj = new List<Edge>[v];
        }

        public void AddEdge(Edge e)
        {
            int v = e.Either, w = e.Other(v);
            _adj[v].Add(e);
            _adj[w].Add(e);
            _e++;
        }

        public Edge[] Adj(int v)
        {
            return _adj[v].ToArray();
        }

        public Edge[] Edges()
        {
            List<Edge> edges = new List<Edge>();
            for (int i = 0; i < _adj.Length; i++)
            {
                for (int j = 0; j < _adj[i].Count; j++)
                {
                    edges.Add(_adj[i][j]);
                }
            }
            return edges.ToArray();
        }
    }
    //边的类
    public class Edge : IComparable
    {
        private float _weight;
        private int v; //一个结点
        private int w; //另一个结点
        public Edge(int v, int w, float weight)
        {
            this.v = v;
            this.w = w;
            this._weight = weight;
        }

        public float Weight => _weight;
        public int Either => v;

        /// <summary>
        /// 取得不同于参数的另一个结点
        /// </summary>
        public int Other(int vertex)
        {
            if (vertex == v) return w;
            else if (vertex == w) return v;
            else throw new Exception("不包含该顶点");
        }

        public int CompareTo(object? obj)
        {
            Edge other = (Edge)obj;
            if (this._weight > other.Weight) return 1;
            else if (this._weight < other.Weight) return -1;
            else return 0;
        }
    }
}

实例的加权图数据

8
4 5 .35
4 7 .37
5 7 .28
0 7 .16
1 5 .32
0 4 .38
2 3 .17
1 7 .19
0 2 .26
1 2 .36
1 3 .29
2 7 .34
6 2 .40
3 6 .52
6 0 .58
6 4 .93

切分定理

将加权图的结点分为两个部分:已经确定为最小生成树的结点尚未加入最小生成树的结点,这连接这两部分的边中最小的一条边可以加入最小生成树。获得最小生成树的算法大都基于切分定理

常用的算法:

Prim算法
延时Prim算法

描述:从一个结点开始将与之关联的边加入一个最小堆,然后从最小堆中得到权重最小的边,将边加入用于表示最小生成树的队列,同时遍历与该边连接的另一个结点的邻接表,将边都加入最小堆,重复这个过程直到最小堆为空。
所需空间:E成正比,
所需时间:ElogE成正比(最坏情况)
API

public LazyPrimMST(EdgeWeightedGraph g) //构造函数
private void Visit(EdgeWeightedGraph g, int v)  //添加v的未访问的邻接结点
public Edge[] Edges()  //返回最小树边的数组

详细代码

using System;
using System.Collections.Generic;
using System.Text;
using SortFunction;

namespace{
    public class LazyPrimMST
    {
        private bool[] marked;  //是否检查过该节点
        private Queue<Edge> mst; 
        private MinPQ<Edge> pq;

        public LazyPrimMST(EdgeWeightedGraph g)
        {
            marked = new bool[g.V];
            mst = new Queue<Edge>();
            pq = new MinPQ<Edge>();
            Visit(g, 0);
            while (!pq.IsEmpty())
            {
                Edge min = pq.DeleteMin();
                int v = min.Either, w = min.Other(v);
                if (marked[v] && marked[w]) continue; //跳过失效的边
                mst.Enqueue(min);
                if (!marked[v]) Visit(g, v);
                if (!marked[w]) Visit(g, w);
            }
            
        }


        /// <summary>
        /// 添加v的未访问的邻接结点
        /// </summary>
        private void Visit(EdgeWeightedGraph g, int v)
        {
            marked[v] = true;
            foreach (Edge i in g.Adj(v))
            {
                if (!marked[i.Other(v)]) pq.Insert(i);
            }
        }

        public Edge[] Edges()
        {
            return mst.ToArray();
        }
    }
}

即时Prim算法

描述:从延迟的prim算法中我们可以看到无效的边(边的两端都已经加入了最小生成树)也被加入到了最小堆里,但每次寻找边时我们需要的只是树外的点到树结点的权值最小的边,因此可以只将树外点w连接到树的边中权值最小的那个加入到最小堆(实际上是使用的索引最小堆)当中,这样可以避免在最小堆出堆时再检查边的有效性。
API: 同延时实现
所需空间:V成正比,
所需时间:ElogV成正比(最坏情况)
详细代码

using SortFunction;
using System;

namespace{
    public class Prim
    {
        private Edge[] edgeTo;
        private bool[] marked;
        private double[] distTo;
        private IndexMinPQ<double> pq; //索引最小堆,方便在找到更小的边时进行替换

        public Prim(EdgeWeightedGraph g)
        {
            edgeTo = new Edge[g.V];  //记录点连接到生成树的边
            marked = new bool[g.V]; 
            distTo = new double[g.V]; //记录连接边的权值
            pq = new IndexMinPQ<double>();
            //未赋值时设置为最大
            for (int i = 0; i < g.V; i++)
            {
                distTo[i] = Double.MaxValue;
            }
            distTo[0] = 0.0f;
            pq.Insert(0, 0.0f);
            while (!pq.IsEmpty())
            {
                Visit(g, pq.DeleteMin());
            }
        }

        private void Visit(EdgeWeightedGraph g, int 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, e.Weight);
                    else pq.Insert(w, e.Weight);
                }
            }
        }
    }
}
Kruskal算法

描述: 每次将权值最小的边加入最小堆中,然后依次出堆,直到树中有V-1条边为止(完成最小生成树),每次出堆要对结点的连通性进行检查,防止形成环
所需空间:E成正比
所需时间:ElogE成正比(最坏情况)

using SortFunction;
using System.Collections.Generic;

namespace{
    public class KruskalMST
    {
        private Queue<Edge> mst;

        public KruskalMST(EdgeWeightedGraph g)
        {
            mst = new Queue<Edge>();
            MinPQ<Edge> pq = new MinPQ<Edge>();
            foreach (Edge e in g.Edges()) pq.Insert(e);
            UF uf = new UF(g.V); //uf可以进行连通性检查,本体是union-find算法

            while (!pq.IsEmpty() && mst.Count < g.V - 1)
            {
                Edge e = pq.DeleteMin();
                int v = e.Either, w = e.Other(v);
                if (uf.Connected(v, w)) continue; //如果这两个结点进通过边相连,则跳过
                uf.Union(v, w);
                mst.Enqueue(e);
            }
        }

        public Edge[] Edges => mst.ToArray();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值