最小生成树问题的算法笔记

最小生成树问题


前提

在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即),而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集且为无循环图,使得联通所有结点的的 w(T) 最小,则此 T 为 G 的最小生成树。
最小生成树其实是最小权重生成树的简称。
先回忆回忆什么是树、什么是图、什么是最小生成树。

1 描述最小生成树问题算法的输入、输出

1.1 最小生成树问题算法的输入:由实际生活中的点和边构成的图,如基电站和电缆线路、景点和公路等抽象化出来的图;

1.2 最小生成树问题算法的输出:由输入的图经过Prim算法或者Kruskal 算法等处理最终得到一个各条边权值之和最小的树,而得到的这棵树叫做最小生成树,该生成树往往是修公路的费用最小化的修路方式、修电站的通信的电线的费用最小化的修造方式。

1.3 最小生成树要解决的两个问题:

(1)尽可能选取权值小的边,但不能构成回路;

(2)选取n-1条恰当的边以连接网的n个顶点。

1.4 构成网的一棵最小生成树,即:在e条带权的边中选取n-1条边(不构成回路),使“权值之和”为最小。


2 普里姆算法(Prim)

2.1 本质:加点法

2.2 基本思想:

取图中任意一个顶点V作为生成树的根,之后往生成树上添加新的顶点W。在添加的顶点W和已经在生成树上的顶点V之间必定存在一条边,该边的权值在所有连通项点V和W之间的边中取值为最小。之后继续往生成树上添加顶点,直至生成树上含有n个顶点为止。

2.3 具体做法:

在生成树的构造过程中,让连通图中n个顶点分属两个集合:已经加入到生成树上的顶点集合U和(原连通图上)还没加入到生成树上的顶点集合V-U;每次在V-U集合里选中一个和生成树上某一个顶点连线中权值最小的,将此顶点加入到生成树里。

2.4 实例:

(1)如下带权无向连通图1:

在这里插入图片描述

图1

(2)从顶点a开始加点:

说明:可能考虑的边的权值,这一列的数据,标志为红色是指这权值为X1的边的两个顶点为U集合;标志为蓝色是指这权值为X2的边再次被遇到,但这边不能在本次最小生成树;为空,则表示Prim算法结束。
在这里插入图片描述

(3)最终得到最小生成树,如下图中的用红色边连通的顶点所构成的树:
在这里插入图片描述
(4)主要代码(用java实现):

public static void onChangeVertex(Vertex vertex) {
    
    
  visitedVertexs.add(vertex); //添加初始节点,作为默认的开始节点
  leftedVertexs.remove(vertex);
}

public static Vertex findOneVertex(Graph g) {
    
    
  int minValue = Integer.MAX_VALUE;
  Vertex findVertex = new Vertex();
  Edge findEdge = new Edge();

  for(int i=0;i<visitedVertexs.size();i++) {
    
    
    for(int j=0;j<leftedVertexs.size();j++) {
    
    
      Vertex v1 = visitedVertexs.get(i);
      Vertex v2 = leftedVertexs.get(j); //获取两个顶点的名称

      for(int k=0;k<g.edge.length;k++) {
    
    
        String startName = g.edge[k].startVertex.vName;
        String endName = g.edge[k].endVertex.vName;

        if((v1.vName.equals(startName) && v2.vName.equals(endName)) ||(v1.vName.equals(endName) && v2.vName.equals(startName))){
    
    
          if(g.edge[k].weight < minValue) {
    
    
            findEdge = g.edge[k];
            minValue = g.edge[k].weight;
            if(leftedVertexs.contains(v1)){
    
     //会调用对象的equals方法比较对象,需重写equals方法
              findVertex = v1;
            }else if(leftedVertexs.contains(v2)){
    
    
              findVertex = v2;
            }
          }
        }
      }
    }
  }
  g.minWeight+= minValue;
  searchEdges.add(findEdge);

  return findVertex;
}

public static void prim(Graph g) {
    
    
  while(leftedVertexs.size()>0){
    
     //直到剩余节点集为空时结束循环
    Vertex findVertex = findOneVertex(g);
    onChangeVertex(findVertex);
  }
  System.out.print("\n最短路径包含的边: ");
  for(int i=0;i<searchEdges.size();i++) {
    
    
    System.out.print("("+searchEdges.get(i).startVertex.vName+","+searchEdges.get(i).endVertex.vName+")"+" ");
  }
  System.out.println("\n最短路径长度: "+g.minWeight);
}

(5)测试结果截图(截图过小,请您见谅):
在这里插入图片描述


3 克鲁斯卡尔算法(Kruskal)

3.1 本质:加边法。为使最小生成树上的边的权值之和达到最小,则应使生成树中的每条边的权值尽可能地小。

3.2 基本思想:

先构造一个只含有n个顶点的子图SG,即只选原图中的所有的顶点n作为原图的子图,然后从权值最小的边开始(一条一条地分别加入,加入的过程只要保证子图不产生回路就可以,因为我们最终要找到的生成树是一棵树,所以它是不能有回路的,那么在这个加边的过程中只要保证这一点就可以,因为我们每次都要选权值最小的,只要不产生回路就满足了最小生成树的条件,直到最后这n-1条边全部加入完成,这个算法就可结束),若它的添加不能使SG中产生回路,则在SG上加上这条边,如此重复,直到加上 n-1 条边为止。

3.3 实例:

(1)如下带权无向连通图2:
在这里插入图片描述

图2

(2)加边过程(自个儿花时间做PPT,再截图):

提示:从上往下看,最后一张为红色边加顶点构成最小生成树。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)代码(C,仅供参考):

#include <stdio.h>
#include <stdlib.h>
#define Max 50

typedef struct road *Road;
typedef struct road
{
    
    
    int a , b;
    int w;
}road;
 
typedef struct graph *Graph;
typedef struct graph
{
    
    
    int e , n;
    Road data;
}graph;

Graph initGraph(int m , int n)
{
    
    
    Graph g = (Graph)malloc(sizeof(graph));
    g->n = m;
    g->e = n;
    g->data = (Road)malloc(sizeof(road) * (g->e));
    return g;
}

void create(Graph g)
{
    
    
    int i;
    for(i = 1 ; i <= g->e ; i++)
    {
    
    
       int x , y, w;
       scanf("%d %d %d",&x,&y,&w);
       if(x < y)
       {
    
    
           g->data[i].a = x;
           g->data[i].b = y;
       } else
       {
    
    
           g->data[i].a = y;
           g->data[i].b = x;
       }
       g->data[i].w = w;
    }
}

int getRoot(int v[], int x)
{
    
    
    while(v[x] != x)
    {
    
    
       x = v[x];
    }
    return x;
}

//这里没有用到效率更高的堆排序
void sort(Road data, int n)
{
    
    
    int i , j;
    for(i = 1 ; i <= n-1 ; i++)
    {
    
    
       for(j = 1 ; j <= n-i ; j++)
       {
    
    
           if(data[j].w > data[j+1].w)
           {
    
    
              road t = data[j];
              data[j] = data[j+1];
              data[j+1] = t;
           }
       }
    }
}

int Kruskal(Graph g)
{
    
    
    int sum = 0;
    //并查集
    int v[Max];
    int i;
    //初始化步骤
    for(i = 1 ; i <= g->n ; i++)
    {
    
    
       v[i] = i;
    }
    sort(g->data , g->e);
    //main
    for(i = 1 ; i <= g->e ; i++)
    {
    
    
       int a , b;
       a = getRoot(v,g->data[i].a);
       b = getRoot(v,g->data[i].b);
       if(a != b)
       {
    
    
           v[a] = b;
           sum += g->data[i].w;
       }
    }
    return sum;
}

3.4 最后温馨提示:

(一)图的生成树不唯一,从不同的顶点出发进行遍历,可以得到不同的生成树;

(二)即使从相同的顶点出发,在选择最小边时,可能有多条同样的边可选,此时任选其一;


4 最小生成树问题的代码解决

4.1 将通信网络抽象为无向图,将各个基站抽象为图的顶点,将预计可能要修的线路抽象为图的边,将修造各条线路的费用抽象为图的边的权值,线路便宜即是边的权值小,线路贵即边的权值大,最便宜的连通线路(含费用的意义)加基站构成最小生成树。

4.2 进行Java代码(Prim算法)实现:

(1)代码(java):

package JavaTestBag;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/*
 * 最小生成树(普里姆算法(Prim算法))简单版
 */

// 顶点类Vertex
class Vertex{
    
    
    String vName; //顶点的名称

    @Override
    public boolean equals(Object obj) {
    
    
        if(obj instanceof Vertex){
    
    
            Vertex vertex = (Vertex)obj;
            return this.vName.equals(vertex.vName);
        }
        return super.equals(obj);
    }
}

// 边类Edge
class Edge{
    
    
    Vertex startVertex;
    Vertex endVertex;
    int weight;
}

// 图的存储结构
class Graph{
    
    
    Vertex[] vertex; //顶点集
    Edge[] edge; //边集
    int minWeight; //最短路径
}

public class MinTreePrimer {
    
    
    private static List<Vertex> visitedVertexs,leftedVertexs; //分别为添加到集合U中的节点集和剩余的集合V中的节点集
    private static List<Edge> searchEdges;

    //初始化图的信息
    public static void initGraph(Graph g) {
    
    
        visitedVertexs = new ArrayList<Vertex>();
        leftedVertexs = new ArrayList<Vertex>();
        searchEdges = new ArrayList<Edge>();

        Scanner sc = new Scanner(System.in);
        System.out.print("输入基站数: ");
        int vertexNumber = sc.nextInt();
        System.out.print("请输入预计可能要修的线路数: ");
        int edgeNumber = sc.nextInt();
        String[] allVertex = new String[vertexNumber];
        String[] allEdge = new String[edgeNumber];

        System.out.println("=================================");
        System.out.println("请输入各个基站的代号(如:A):");
        Scanner scanner = new Scanner(System.in);
        for(int i=0;i<vertexNumber;i++){
    
    
            System.out.print("基站"+(i+1)+":");
            allVertex[i] = scanner.nextLine();
        }
        System.out.println("=================================");
        for(int i=0;i<edgeNumber;i++){
    
    
            System.out.print("输入线路(Vi,Vj)中的基站名称和费用W(如:A B 7): ");
            allEdge[i] = scanner.nextLine();
        }

        g.vertex = new Vertex[allVertex.length];
        g.edge = new Edge[allEdge.length];
        g.minWeight = 0;

        for(int i=0;i<allVertex.length;i++) {
    
    
            g.vertex[i] = new Vertex();
            g.vertex[i].vName = allVertex[i];
            leftedVertexs.add(g.vertex[i]); //初始化剩余点集合
        }

        for(int i=0;i<allEdge.length;i++) {
    
    
            g.edge[i] = new Edge();
            g.edge[i].startVertex = new Vertex();
            g.edge[i].endVertex = new Vertex();

            String edgeInfo[] = allEdge[i].split(" ");
            g.edge[i].startVertex.vName = edgeInfo[0];
            g.edge[i].endVertex.vName = edgeInfo[1];
            g.edge[i].weight = Integer.parseInt(edgeInfo[2]);
        }
    }

    public static void onChangeVertex(Vertex vertex) {
    
    
        visitedVertexs.add(vertex); //添加初始节点,作为默认的开始节点
        leftedVertexs.remove(vertex);
    }

    public static Vertex findOneVertex(Graph g) {
    
    
        int minValue = Integer.MAX_VALUE;
        Vertex findVertex = new Vertex();
        Edge findEdge = new Edge();

        for(int i=0;i<visitedVertexs.size();i++) {
    
    
            for(int j=0;j<leftedVertexs.size();j++) {
    
    
                Vertex v1 = visitedVertexs.get(i);
                Vertex v2 = leftedVertexs.get(j); //获取两个顶点的名称

                for(int k=0;k<g.edge.length;k++) {
    
    
                    String startName = g.edge[k].startVertex.vName;
                    String endName = g.edge[k].endVertex.vName;

                    if((v1.vName.equals(startName) && v2.vName.equals(endName)) ||(v1.vName.equals(endName) && v2.vName.equals(startName))){
    
    
                        if(g.edge[k].weight < minValue) {
    
    
                            findEdge = g.edge[k];
                            minValue = g.edge[k].weight;
                            if(leftedVertexs.contains(v1)){
    
     //会调用对象的equals方法比较对象,需重写equals方法
                                findVertex = v1;
                            }else if(leftedVertexs.contains(v2)){
    
    
                                findVertex = v2;
                            }
                        }
                    }
                }
            }
        }
        g.minWeight+= minValue;
        searchEdges.add(findEdge);

        return findVertex;
    }

    public static void prim(Graph g) {
    
    
        while(leftedVertexs.size()>0){
    
     //直到剩余节点集为空时结束循环
            Vertex findVertex = findOneVertex(g);
            onChangeVertex(findVertex);
        }
        System.out.print("\n在实现连通各个基站、所花费的费用最低的情况下,要修的线路: ");
        for(int i=0;i<searchEdges.size();i++) {
    
    
            System.out.print("("+searchEdges.get(i).startVertex.vName+","+searchEdges.get(i).endVertex.vName+")"+" ");
        }
        System.out.println("\n在实现连通各个基站、所花费的费用最低的情况下,要修的线路的费用: "+g.minWeight);
    }

    public static void main(String[] args) {
    
    
        Graph g = new Graph();
        initGraph(g);
        onChangeVertex(g.vertex[0]);
        prim(g);
    }
}

(2)测试结果图如下:

在这里插入图片描述


参考文献

[1] Jon Kleinberg,Eva Tardos 著,张立昂,屈婉玲 译. 算法设计. .北京:清华大学出版社,2007.3
[2] Eric,Lehaman. 计算机科学中的数学-信息与智能时代的必修课. 北京:电子工业出版社

猜你喜欢

转载自blog.csdn.net/xu_yushu/article/details/124538740