一个连通图的生成树是一个极小的连通子图,它包含图中全部的
n
个顶点,但只有构成一棵树的n-1
条边。
一个连通图可以有多棵生成树;
一个连通图的所有生成树都包含相同的顶点个数和边数;
生成树当中不存在环;
移除生成树中的任意一条边都会导致图的不连通, 生成树的边最少特性;
在生成树中添加一条边会构成环。
对于包含 n
个顶点的连通图,生成树包含 n
个顶点和 n-1
条边;
对于包含 n
个顶点的无向完全图最多包含 n ⁿ﹣²
棵生成树。
示意图:
所谓一个带权图的最小生成树,就是原图中边的权值最小的生成树,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树(Minimum Spanning Tree)。由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。
假设有 7 个村庄(A, B, C, D, E, F, G),现在需要修路把 7 个村庄连通。各个村庄的距离用边线表示(权),比如 A<–>B 距离 5 公里,问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?(通过普利姆算法求最小生成树)
public class PrimAlgorithmDemo {
public static void main(String[] args) {
// 各顶点的数据。
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
// 顶点个数。
int vertexes = data.length;
// 邻接矩阵的关系使用二维数组表示,这里用 Integer.MAX_VALUE 这个大数,表示两个点不联通。
int x = Integer.MAX_VALUE;
int[][] matrix = {
// A B C D E F G
/*A*/ {x, 5, 7, x, x, x, 2},
/*B*/ {5, x, x, 9, x, x, 3},
/*C*/ {7, x, x, x, 8, x, x},
/*D*/ {x, 9, x, x, x, 4, x},
/*E*/ {x, x, 8, x, x, 5, 4},
/*F*/ {x, x, x, 4, 5, x, 6},
/*G*/ {2, 3, x, x, 4, 6, x},
};
Graph graph = new Graph(vertexes);
MinTree minTree = new MinTree();
minTree.createGraph(graph, vertexes, data, matrix);
// 图创建是否成功。
minTree.showGraph(graph);
// [2147483647, 5, 7, 2147483647, 2147483647, 2147483647, 2]
// [5, 2147483647, 2147483647, 9, 2147483647, 2147483647, 3]
// [7, 2147483647, 2147483647, 2147483647, 8, 2147483647, 2147483647]
// [2147483647, 9, 2147483647, 2147483647, 2147483647, 4, 2147483647]
// [2147483647, 2147483647, 8, 2147483647, 2147483647, 5, 4]
// [2147483647, 2147483647, 2147483647, 4, 5, 2147483647, 6]
// [2, 3, 2147483647, 2147483647, 4, 6, 2147483647]
// 测试普利姆算法。
System.out.println();
// 从顶点 A 开始。
minTree.prim(graph, 0);
// 边A<->G 权值:2
// 边G<->B 权值:3
// 边G<->E 权值:4
// 边E<->F 权值:5
// 边F<->D 权值:4
// 边A<->C 权值:7
}
}
/**
* 最小生成树。
*/
class MinTree {
/**
* 创建图
*
* @param graph 图
* @param vertexes 顶点个数
* @param data 各顶点的值
* @param matrix 邻接矩阵
*/
public void createGraph(Graph graph, int vertexes, char[] data, int[][] matrix) {
int i;
int j;
// 顶点。
for (i = 0; i < vertexes; i++) {
graph.setData(i, data[i]);
for (j = 0; j < vertexes; j++) {
graph.setEdges(i, j, matrix[i][j]);
}
}
}
/**
* 显示图信息。
*
* @param graph 图
*/
public void showGraph(Graph graph) {
for (int[] link : graph.getEdges()) {
System.out.println(Arrays.toString(link));
}
}
/**
* 普利姆算法。
* 目的:通过该算法,得到最小生成树。
*
* @param graph 图对象
* @param v 表示从图的第几个顶点开始生成('A'->0 'B'->1...)。
*/
public void prim(Graph graph, int v) {
int vertexes = graph.getVertexes();
int[] isVisited = new int[vertexes];
isVisited[v] = 1;
// x 和 y 记录两个顶点的下标。
int x = -1;
int y = -1;
int minWeight = Integer.MAX_VALUE;
for (int k = 1; k < vertexes; k++) {
// 这个是确定每一次生成的子图 ,和哪个节点的距离最近。
// i节点表示被访问过的节点。
for (int i = 0; i < vertexes; i++) {
// j节点表示还没有访问过的节点。
for (int j = 0; j < vertexes; j++) {
if (1 == isVisited[i]
&& 0 == isVisited[j]
&& graph.getEdgeByCoords(i, j) < minWeight
) {
// 替换最小权值(寻找已经访问过的节点和未访问过的节点间的权值最小的边)。
minWeight = graph.getEdgeByCoords(i, j);
x = i;
y = j;
}
}
}
// 找到一条边是最小。
System.out.println(
"边" + graph.getData(x) + "<->" + graph.getData(y)
+ " 权值:" + minWeight
);
// 将当前这个节点标记为已经访问。
isVisited[y] = 1;
// minWeight 重新设置为最大值 Integer.MAX_VALUE。
minWeight = Integer.MAX_VALUE;
}
}
}
/**
* 图。
*/
class Graph {
/**
* 图顶点的个数。
*/
private final int vertexes;
/**
* 图顶点的数据。
*/
private final char[] data;
/**
* 图的边(邻接矩阵)。
*/
private final int[][] edges;
/**
* 初始化构造。
*
* @param vertexes 顶点个数
*/
public Graph(int vertexes) {
this.vertexes = vertexes;
this.data = new char[vertexes];
this.edges = new int[vertexes][vertexes];
}
public int getVertexes() {
return vertexes;
}
public char getData(int n) {
return this.data[n];
}
public int[][] getEdges() {
return edges;
}
public int getEdgeByCoords(int i, int j) {
return this.edges[i][j];
}
public void setData(int i, char data) {
this.data[i] = data;
}
public void setEdges(int i, int j, int weight) {
this.edges[i][j] = weight;
}
}
克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。与普里姆算法不同,它的时间复杂度为
O(eloge)
(e为网中的边数),所以,适合于求边稀疏的网的最小生成树。
假设有 7 个村庄(A, B, C, D, E, F, G),现在需要修路把 7 个村庄连通。各个村庄的距离用边线表示(权),比如 A<–>B 距离 12 公里,问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?(通过克鲁斯卡尔算法求最小生成树)
public class KruskalAlgorithmDemo {
public static void main(String[] args) {
// 各顶点的数据。
char[] vertexes = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
// 邻接矩阵的关系使用二维数组表示,这里用 Integer.MAX_VALUE 这个大数,表示两个点不联通。
int x = Integer.MAX_VALUE;
int[][] matrix = {
// A B C D E F G
/*A*/ {0, 12, x, x, x, 16, 14},
/*B*/ {12, 0, 10, x, x, 7, x},
/*C*/ {x, 10, 0, 3, 5, 6, x},
/*D*/ {x, x, 3, 0, 4, x, x},
/*E*/ {x, x, 5, 4, 0, 2, 8},
/*F*/ {16, 7, 6, x, 2, 0, 9},
/*G*/ {14, x, x, x, 8, 9, 0}
};
Graph graph = new Graph(vertexes, matrix);
graph.showMatrix();
// 邻接矩阵为:
//
// 0 12 2147483647 2147483647 2147483647 16 14
// 12 0 10 2147483647 2147483647 7 2147483647
// 2147483647 10 0 3 5 6 2147483647
// 2147483647 2147483647 3 0 4 2147483647 2147483647
// 2147483647 2147483647 5 4 0 2 8
// 16 7 6 2147483647 2 0 9
// 14 2147483647 2147483647 2147483647 8 9 0
System.out.println();
EdgeData[] results = new MinTree().kruskal(graph);
System.out.println("图的边共计:" + results.length + "条。");
// 图的边共计:12条。
System.out.println("最小生成树如下:");
for (EdgeData result : results) {
if (null != result) {
System.out.println(result);
}
}
// 最小生成树如下:
// EdgeData: {= 2}
// EdgeData: {= 3}
// EdgeData: {= 4}
// EdgeData: {= 7}
// EdgeData: {= 8}
// EdgeData: {= 12}
}
}
/**
* 最小生成树。
*/
class MinTree {
/**
* 克鲁斯卡尔算法。
* 目的:通过该算法,得到最小生成树。
*
* @param graph 图
* @return {@link EdgeData[]} 边数据信息
*/
public EdgeData[] kruskal(Graph graph) {
// 表示最后结果数组的索引。
int index = 0;
int edgeNum = graph.getEdgeNum();
// 用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点。
int[] ends = new int[edgeNum];
// 创建结果数组, 保存最后的最小生成树。
EdgeData[] results = new EdgeData[edgeNum];
// 图中12条边构成的集合。
EdgeData[] edges = graph.getEdges();
// 按照边的权值大小进行排序(从小到大)。
graph.sortEdges(edges);
// 将没有形成回路的边添加到最小生成树中时,否则不添加。
for (int i = 0; i < edgeNum; i++) {
// 获取到第i条边的第一个顶点(边的起点:p1=4)。
int p1 = graph.getPosition(edges[i].start);
// 获取到第i条边的第二个顶点(边的终点:p2=5)
int p2 = graph.getPosition(edges[i].end);
// 获取p1这个顶点在已有最小生成树中的终点(m=4)。
int m = graph.getEnd(p1, ends);
// 获取p2这个顶点在已有最小生成树中的终点(n=5)。
int n = graph.getEnd(p2, ends);
// 如果没有构成回路。
if (m != n) {
// 设置 m 在"已有最小生成树"中的终点 [0,0,0,0,5,0,0,0,0,0,0,0]
ends[m] = n;
// 有一条边加入到rets数组。
results[index++] = edges[i];
}
}
return results;
}
}
/**
* 图。
*/
class Graph {
/**
* 边的条数。
*/
private int edgeNum;
/**
* 顶点数据。
*/
private final char[] vertexesData;
/**
* 邻接矩阵。
*/
private final int[][] matrix;
/**
* 表示边不联通。
*/
private static final int DISCONNECTED = Integer.MAX_VALUE;
public int getEdgeNum() {
return edgeNum;
}
/**
* 构造图。
*
* @param vertexesData 顶点数据
* @param matrix 矩阵
*/
public Graph(char[] vertexesData, int[][] matrix) {
// 初始化顶点数和边的个数。
int vLen = vertexesData.length;
// 通过复制拷贝的方式,初始化顶点。
this.vertexesData = new char[vLen];
System.arraycopy(vertexesData, 0, this.vertexesData, 0, vLen);
// 通过复制拷贝的方式,初始化边。
this.matrix = new int[vLen][vLen];
for (int i = 0; i < vLen; i++) {
System.arraycopy(matrix[i], 0, this.matrix[i], 0, vLen);
}
// 统计边的条数。
for (int i = 0; i < vLen; i++) {
for (int j = i + 1; j < vLen; j++) {
if (DISCONNECTED != this.matrix[i][j]) {
// 联通的边进行计数。
edgeNum++;
}
}
}
}
/**
* 显示矩阵信息。
*/
public void showMatrix() {
System.out.println("邻接矩阵为: \n");
int length = this.vertexesData.length;
for (int i = 0; i < length; i++) {
for (int j = 0; j < length; j++) {
System.out.printf("%12d", matrix[i][j]);
}
System.out.println();
}
}
/**
* 通过冒泡方式对边排序。
*
* @param edges 边的集合
*/
public void sortEdges(EdgeData[] edges) {
int len = edges.length - 1;
EdgeData tmp = null;
for (int i = 0; i < len; i++) {
for (int j = 0; j < (len - i); j++) {
// 前一个的权值大于后一个,则互换位置。
if (edges[j].weight > edges[j + 1].weight) {
tmp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = tmp;
}
}
}
}
/**
* 找到对应顶点的下标。
*
* @param vertex 顶点的值
* @return int 返回顶点下标,找不到时返回-1
*/
public int getPosition(char vertex) {
for (int i = 0; i < this.vertexesData.length; i++) {
if (vertex == this.vertexesData[i]) {
return i;
}
}
return -1;
}
/**
* 通过邻接矩阵获取图的边信息,并将其保存至数组。
*
* @return {@link EdgeData[]}
*/
public EdgeData[] getEdges() {
int index = 0;
EdgeData[] edges = new EdgeData[this.edgeNum];
int length = this.vertexesData.length;
for (int i = 0; i < length; i++) {
for (int j = (i + 1); j < length; j++) {
if (DISCONNECTED != this.matrix[i][j]) {
// 示例:[['A','B', 12], ['B','F',7], .....]
edges[index++] = new EdgeData(vertexesData[i], vertexesData[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 获取当前下标顶点的终点。
* 目的:用于后面判断两个顶点的终点是否相同。
*
* @param i 对应顶点的下标
* @param ends 各个顶点所对应的终点
* @return int 当前顶点对应的终点下标
*/
public int getEnd(int i, int[] ends) {
while (0 != ends[i]) {
i = ends[i];
}
return i;
}
}
/**
* 边数据。
*/
class EdgeData {
/**
* 边的起始点。
*/
char start;
/**
* 边的终点。
*/
char end;
/**
* 边的权值。
*/
int weight;
/**
* 构造器
*
* @param start 边的起始点
* @param end 边的终点
* @param weight 权值
*/
public EdgeData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EdgeData: {<" + start + ", " + end + ">= " + weight + "}";
}
}
O(eloge)
,其中 e
为边数,克鲁斯卡尔算法主要针对边展开,边数少时效率会很高,所以对于稀疏图有优势。O(nⁿ)
,其中 n
为顶点数,适用于稠密图。“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。