(超简单、超易懂、超详细)算法精讲(四十一): 最小生成树之Prim算法和Kruskal算法

        如果你也喜欢C#开发或者.NET开发,可以关注我,我会一直更新相关内容,并且会是超级详细的教程,只要你有耐心,基本上不会有什么问题,如果有不懂的,也可以私信我加我联系方式,我将毫无保留的将我的经验和技术分享给你,不为其他,只为有更多的人进度代码的世界,而进入代码的世界,最快捷和最容易的就是C#.NET,准备好了,就随我加入代码的世界吧!

一、算法简介

        最小生成树算法是解决连通图中找出最小权重生成树的问题的一类算法,其中最著名的算法是Prim算法和Kruskal算法。

        Prim算法是一种贪心算法,从一个起始节点开始,逐步选择与当前生成树最近且权值最小的边,将该边加入到生成树中,并将该节点标记为已访问,然后继续在已访问的节点集合中寻找下一个满足条件的边,直到所有的节点都被访问到。

        Kruskal算法是一种基于边的贪心算法,它首先将所有的边按照权重从小到大排序,然后从权重最小的边开始,逐步将边加入生成树中,但要保证生成树不形成环路,直到生成树包含了所有的节点。

        这两种算法都能够找到最小生成树,但是它们的实现方式和时间复杂度不同。Prim算法的时间复杂度为O(|V|^2),其中|V|为图中节点的个数;而Kruskal算法的时间复杂度为O(|E|log|V|),其中|E|为图中边的个数。选择哪种算法取决于具体问题和图的规模。

二、为什么要学习最小生成树算法:

        2.1 解决网络规划问题

        最小生成树算法可以用于解决网络规划问题,如在一个有很多节点和边的网络中,如何选择最优的边来连接所有节点,以使整个网络的总成本最小。

        2.2 解决电力传输问题

        最小生成树算法可以用于解决电力传输问题,如在一个有很多电力站和传输线的网络中,如何选择最优的传输线路,以使整个电力传输系统的能耗最小。

        2.3 解决通信网络问题

        最小生成树算法可以用于解决通信网络问题,如在一个有很多通信站和通信线路的网络中,如何选择最优的通信线路,以使整个通信网络的传输效率最高。

        2.4 算法应用广泛

        最小生成树算法是经典的图论算法,其思想和方法在其他领域也有广泛的应用,如计算机网络、运筹学、电力系统等领域。

        2.5 提高算法设计能力

        学习最小生成树算法可以提高算法设计能力和解决问题的能力,同时也可以锻炼逻辑思维和分析问题的能力。

三、最小生成树算法在项目中有哪些实际应用:

        3.1 网络设计和规划

        最小生成树算法可以帮助确定在一个网络中连接所有节点的最短路径。

        3.2 路线规划

        最小生成树算法可以帮助确定连接所有地点的最短路径,比如在物流行业中确定最优的送货路线。

        3.3 电力分配

        最小生成树算法可以帮助确定电力系统中的最优输电线路,以最小化电力传输的损失。

        3.4 电信网络

        最小生成树算法可以用于确定电信网络中的最短路径,以提供高效的通信服务。

        3.5 铁路规划

        最小生成树算法可以帮助确定铁路系统中的最优线路规划,以最大程度地减少建设成本和运营成本。

        3.6 传感器网络

        最小生成树算法可以帮助确定传感器网络中的最短路径,以最大限度地减少能量消耗。

四、最小生成树算法的实现与讲解:

        4.1 最小生成树算法之Prim算法的实现与讲解

                4.1.1 最小生成树算法之Prim算法的实现

 ///  /// 图的边
  ///  public class Edge
  {
      ///  /// 源节点
      ///  public int Source { get; set; }
      ///  /// 目标节点
      ///  public int Destination { get; set; }
      ///  /// 权重
      ///  public int Weight { get; set; }
  }
  ///  /// Prim算法
  ///  public static void Prim(List> graph)
  {
      int vertices = graph.Count; // 图的顶点数目
      int[] parent = new int[vertices]; // 保存最小生成树中每个顶点的父节点
      int[] key = new int[vertices]; // 保存每个顶点到最小生成树的最小权重 
      bool[] mstSet = new bool[vertices]; // 标记顶点是否已加入最小生成树 
      for (int i = 0; i < vertices; i++)
      {
          key[i] = int.MaxValue; // 初始化所有顶点的权重为无穷大
          mstSet[i] = false; // 初始化所有顶点都未加入最小生成树 
          key[0] = 0; // 从第一个顶点开始构建最小生成树
          parent[0] = -1; // 第一个顶点没有父节点 
          for (int count = 0; count < vertices - 1; count++)
          {
              int minKeyIndex = GetMinKeyIndex(key, mstSet, vertices); // 选取当前最小权重顶点 
              mstSet[minKeyIndex] = true; // 将选中的顶点标记为已加入最小生成树 
              foreach (var edge in graph[minKeyIndex])
              {
                  int destination = edge.Destination;
                  if (!mstSet[destination] && edge.Weight < key[destination])
                  {
                      parent[destination] = minKeyIndex; // 更新父节点
                      key[destination] = edge.Weight; // 更新权重
                  }
              }
          }
          PrintMST(parent, graph); // 打印最小生成树
      }
  }
  ///  /// 选取当前最小权重顶点 
  ///  private static int GetMinKeyIndex(int[] key, bool[] mstSet, int vertices)
  {
      int minValue = int.MaxValue;
      int minIndex = -1; 
      for (int i = 0; i < vertices; i++)
      {
          if (!mstSet[i] && key[i] < minValue)
          {
              minValue = key[i];
              minIndex = i;
          }
      } 
      return minIndex;
  }
  ///  /// 打印最小生成树
  ///  private static void PrintMST(int[] parent, List> graph)
  {
      Console.WriteLine("边\t权重");
      for (int i = 1; i < graph.Count; i++)
      {
          Console.WriteLine($"{parent[i]} - {i}\t{graph[i][parent[i]].Weight}");
      }
  }

                        方法调用

static void Main(string[] args)
{
    // 构建图
    List> graph = new List>();
    graph.Add(new List()
{
    new Edge() { Source = 0, Destination = 1, Weight = 2 },
    new Edge() { Source = 0, Destination = 3, Weight = 6 }
});
    graph.Add(new List()
{
    new Edge() { Source = 1, Destination = 0, Weight = 2 },
    new Edge() { Source = 1, Destination = 2, Weight = 3 },
    new Edge() { Source = 1, Destination = 3, Weight = 8 },
    new Edge() { Source = 1, Destination = 4, Weight = 5 }
});
    graph.Add(new List()
{
    new Edge() { Source = 2, Destination = 1, Weight = 3 },
    new Edge() { Source = 2, Destination = 4, Weight = 7 }
});
    graph.Add(new List()
{
    new Edge() { Source = 3, Destination = 0, Weight = 6 },
    new Edge() { Source = 3, Destination = 1, Weight = 8 },
    new Edge() { Source = 3, Destination = 4, Weight = 9 }
});
    graph.Add(new List()
{
    new Edge() { Source = 4, Destination = 1, Weight = 5 },
    new Edge() { Source = 4, Destination = 2, Weight = 7 },
    new Edge() { Source = 4, Destination = 3, Weight = 9 }
});
    Prim(graph); // 执行Prim算法,找到最小生成树
}

                 4.1.1 最小生成树算法之Prim算法的讲解

        在上述代码中,我们首先定义了一个Edge类,用来表示图的边。每条边都有源节点、目标节点和权重。

        然后,我们实现了Prim方法来执行Prim算法。算法的输入是一个邻接表表示的无向图。算法中使用了一些辅助数据结构,例如parent数组用于保存每个顶点的父节点,key数组用于保存每个顶点到最小生成树的最小权重,mstSet数组用于标记顶点是否已加入最小生成树。

        在算法开始之前,我们需要对辅助数据结构进行初始化。首先,将所有顶点的权重初始化为无穷大,表示它们都不在最小生成树中。然后,将第一个顶点的权重初始化为0,并设置其父节点为-1。

        接下来,我们通过循环选择最小权重顶点,并将其标记为已加入最小生成树。然后,遍历与该顶点相邻的顶点,并更新它们的权重和父节点,如果找到了更小的权重。

        最后,我们打印最小生成树的边和权重,即每个节点的父节点以及与之相连的边的权重。

        在主方法中,我们构建了一个图,并调用Prim方法执行Prim算法,找到最小生成树。

        4.2 最小生成树算法之Kruskal算法的实现与讲解

        Kruskal算法是一种常用的求解最小生成树的算法。最小生成树是指在一个连通图中,找到一个边的子集,使得这个子集形成一棵树,同时保证树的权重之和最小。

        Kruskal算法的基本思想是先对图中的所有边按照权重进行排序,然后逐步加入到最小生成树中。添加一条边的条件是:如果这条边的两个端点处于不同的连通分量中,那么就加入这条边,同时合并这两个连通分量。

下面我们来实现Kruskal算法的具体步骤:

  1. 首先定义一个辅助数据结构 Edge 表示图中的一条边。Edge 包含两个属性:起始顶点 u 和终止顶点 v,以及权重 weight。
public class Edge : IComparable{
    public int U { get; set; }  // 起始顶点
    public int V { get; set; }  // 终止顶点
    public int Weight { get; set; }  // 权重
    public int CompareTo(Edge other)
    {
        return Weight - other.Weight;
    }
}
  1. 定义一个方法 Kruskal 来实现Kruskal算法。该方法接收一个图的邻接矩阵表示和顶点个数 n,返回一个列表表示最小生成树的边集合。
public List Kruskal(int[,] graph, int n)
{
    List result = new List();  // 存储最小生成树的边集合
    List edges = new List();  // 存储所有边的集合
    // 遍历邻接矩阵,将边添加到edges中
    for (int u = 0; u < n; u++)
    {
        for (int v = u + 1; v < n; v++)
        {
            if (graph[u, v] > 0)
            {
                edges.Add(new Edge { U = u, V = v, Weight = graph[u, v] });
            }
        }
    }
    edges.Sort();  // 对边按照权重进行排序
    int[] parent = new int[n];  // 记录每个顶点所在的连通分量
    // 初始化parent数组,每个顶点都是独立的连通分量
    for (int i = 0; i < n; i++)
    {
        parent[i] = i;
    }
    foreach (var edge in edges)
    {
        int u = edge.U;
        int v = edge.V;
        int parentU = FindParent(parent, u);  // 查找u的连通分量的根节点
        int parentV = FindParent(parent, v);  // 查找v的连通分量的根节点
        // 如果u和v不在同一个连通分量中,就将这条边添加到最小生成树中
        if (parentU != parentV)
        {
            result.Add(edge);
            parent[parentU] = parentV;  // 合并u和v所在的连通分量
        }
    }
    return result;
}
  1. 实现一个辅助方法 FindParent 来查找顶点 u 所在的连通分量的根节点。
private int FindParent(int[] parent, int u)
{
    while (parent[u] != u)
    {
        parent[u] = parent[parent[u]];
        u = parent[u];
    }
    return u;
}

现在可以使用以上实现的Kruskal算法来找到最小生成树。例如,给定如下的邻接矩阵表示的图:

int[,] graph = new int[,] {
    { 0, 1, 3, 0 },
    { 1, 0, 3, 6 },
    { 3, 3, 0, 4 },
    { 0, 6, 4, 0 }
};
Kruskal kruskal = new Kruskal();
List result = kruskal.Kruskal(graph, 4);

最后,result 列表就是最小生成树的边集合。你可以根据需要对其进行进一步操作,比如输出边的信息或计算最小生成树的总权重。

五、最小生成树算法需要注意的是:

        5.1 边的权重

        最小生成树算法通常是基于边的权重来构建最小生成树的,因此需要确保边的权重是正确的。如果边的权重没有正确计算或者赋值,最终得到的最小生成树可能是错误的。

        5.2 图的连通性

        最小生成树算法通常要求图是连通的,即任意两个顶点之间都有路径。如果图不是连通的,那么最小生成树算法可能无法得到正确的结果。

        5.3 图的表示方式

        最小生成树算法的实现通常需要用到图的表示方式,如邻接矩阵或邻接表。选择合适的图表示方式可以影响算法的效率和实现的复杂度。

        5.4 算法的选择

        有多种最小生成树算法可供选择,如Prim算法和Kruskal算法。不同的算法适用于不同的问题和情况,因此需要根据具体的需求选择合适的算法。

        5.5 算法的复杂度

        最小生成树算法的复杂度是一个重要的考虑因素,特别是对于大规模图问题。了解算法的时间复杂度和空间复杂度可以帮助选择合适的算法。

        5.6 算法的正确性

        最小生成树算法需要保证生成的树是最小生成树,即权重最小。因此,需要对算法进行正确性验证,并对边的权重进行验证和比较,确保生成的树是正确的。