最小生成树问题
前提
在一给定的无向图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:
(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)加边过程(自个儿花时间做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. 计算机科学中的数学-信息与智能时代的必修课. 北京:电子工业出版社