基本概念
生成树
给定一个带权的无向连通图,能够连通该图的全部顶点且不产生回路的子图即为该图的生成树;
极小连通子图
一个连通图的生成树是一个极小连通子图,它含有图中全部N个顶点且只有足以构成一棵树的N-1条边;
最小生成树 (简称MST)
- 给定一个带权的无向连通图,如何选取一棵生成树,使得树上所有边的权总和最小,这棵生成树就叫做最小生成树;
- 给定N个顶点的无向连通图,其最小生成树一定有N-1条边;
- 最小生成树中含有N个顶点;
- 最小生成树中的N-1条边都在给定的无向连通图中;
问题引出
首先看这样一个场景:
- 现在有7个村庄(A,B,C,D,E,F,G),需要修路将这7个村庄连通起来;
- 问如何修路保证使得各个村庄连通起来,并且修路的总长度最短;
思路:需要保证修路的条数尽可能少且每条路的长度尽可能短,即
- N个顶点(村庄)最少需要N-1条边(路)进行连通;
- 每次修新路时都选择 {能连接 [已经连通起来的几个村庄] 的公路} 中最短的那条将新的村庄连通进来,比如说当已经把 <A–(2)–G> 连通起来时,我们选择的下一条边应该是 <A–(5)–B> <A–(7)–C> <G–(3)–B> <G–(4)–E> <G–(6)–F>中最短的那条,即 <G–(3)–B>,将B与A–G连通;
prim算法介绍
- 普利姆(Prim)算法求最小生成树,就是在给定含有N个顶点的带权无向连通图中,找出包含N个顶点且只有N-1条边的连通子图,也即常说的极小连通子图,并保证该子图的权值和最小
- 普利姆算法思路:
1)设G=(V,E)是给定的无向带权图,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
2)若从G中一个顶点v开始构造最小生成树的,则先从V集合中取出v放入集合U中;
3)寻找集合U中顶点ui与集合V-U中顶点vj之间权值最小且不形成回路的边,将顶点vi加入到U集合中,并将边(ui,vj)加入到集合D中;
4)重复步骤3),直到所有N个顶点都加入到U中,此时D中恰有N-1条边;
代码实现
import java.util.Arrays;
public class PrimAlgorithm {
public static void main(String[] args) {
char[] data = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int verNum = data.length;
int[][] weight = new int[][] { // 1024表示无边
{ 1024, 5, 7, 1024, 1024, 1024, 2 },
{ 5, 1024, 1024, 9, 1024, 1024, 3 },
{ 7, 1024, 1024, 1024, 8, 1024, 1024 },
{ 1024, 9, 1024, 1024, 1024, 4, 1024 },
{ 1024, 1024, 8, 1024, 1024, 5, 4 },
{ 1024, 1024, 1024, 4, 5, 1024, 6 },
{ 2, 3, 1024, 1024, 4, 6, 1024 } };
// 创建MGraph对象
MGraph graph = new MGraph(verNum);
// 创建MinTree对象
MinTree minTree = new MinTree();
minTree.creatGraph(graph, verNum, data, weight);
minTree.showGraph(graph);
// 测试prim算法
System.out.println("-----从" + data[0] + "开始的最小生成树-----");
minTree.prim(graph, 0);
}
}
//创建最小生成树->村庄的图
class MinTree {
// 创建图的邻接矩阵
/**
* @param graph 图对象
* @param verNum 图的节点个数
* @param data 图的各个节点的值
* @param weight 图的邻接矩阵
*/
public void creatGraph(MGraph graph, int verNum, char[] data, int[][] weight) {
for (int i = 0; i < verNum; i++) {// 顶点
graph.data[i] = data[i];
for (int j = 0; j < verNum; j++) {// 边
graph.weight[i][j] = weight[i][j];
}
}
}
// 显式图的邻接矩阵
public void showGraph(MGraph graph) {
for (int i = 0; i < graph.verNum; i++) {
System.out.print(" " + graph.data[i]);
}
System.out.println();
int j = 0;
for (int[] link : graph.weight) {
System.out.println(graph.data[j++] + " " + Arrays.toString(link));
}
}
// prim算法
/**
* @param graph 图对象
* @param v 表示最小生成树的起始点
*/
public void prim(MGraph graph, int v) {
// visted用于标记节点是否被访问过 默认初始值都为0表示所有节点都没有被访问过
int[] visited = new int[graph.verNum];
// 将起始点v标记为已经访问
visited[v] = 1;
// 用 indexBegin 和 indexEnd 记录两个顶点的下标
int indexBegin = -1;
int indexEnd = -1;
int minWeight = 1024;
for (int k = 0; k < graph.verNum-1; k++) {// 普利姆算法结束后,一共有graph.verNum-1条边,故k∈[0,graph.verNum-2]
// 下面双重循环的作用寻找已经访问过的节点和为访问过的节点之间权值最小的未访问节点
for (int i = 0; i < graph.verNum; i++) {// i节点表示访问过的节点
for (int j = 0; j < graph.verNum; j++) {// j节点表示未被访问过的节点
if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight) {
minWeight = graph.weight[i][j];
indexBegin = i;
indexEnd = j;
}
}
} // 此时已经找到
System.out.println("边<" + graph.data[indexBegin] + "," + graph.data[indexEnd] + "> 权值:" + minWeight);
//将信息存入minTreeEdage
// 将新找到的节点标记为已访问
visited[indexEnd] = 1;
minWeight = 1024;// 重置minWeight用于找下一个待加入子图的节点
}
}
}
class MGraph {
int verNum; // 表示图的节点个数
char[] data;// 存放节点数据
int[][] weight; // 邻接矩阵
public MGraph(int verNum) {
this.verNum = verNum;
this.data = new char[verNum];
weight = new int[verNum][verNum];
}
}