这里的图指的是带权无向图,也就是无向网。
关于最小生成树,https://blog.csdn.net/namewdy/article/details/105645409
图的最小生成树要解决的问题:用最小的代价连通图中的所有顶点。
下面两种算法都是运用贪心思想,利用MST(Minimum Spanning Tree)性质构建最小生成树。
MST性质: 假设N=(V, E)是一个连通网,U是顶点集V的一个非空子集。若(u, v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u, v)的最小生成树。
证明: 假设网N的任何一棵最小生成树都不包含(u, v)。设T是连通网上的一棵最小生成树,当边(u, v)加入到T中时,由生成树的定义,T中必存在一条包含(u, v)的回路。在这个回路中任意去除一条除(u, v)的边便可消除回路,同时得到另一棵生成树。因为新包含(u, v)的生成树代价小于T,由此和假设矛盾。
普里姆算法主要针对的是邻接矩阵形式存储的稠密网。
普里姆算法视频讲解
算法描述
假设N=(V, E)是连通网,TE是N上最小生成树中边的集合,U是N上最小生成树的顶点集合。算法从U={ u o u_o uo} ( u o u_o uo∈V),TE={ }开始,重复执行下述操作:在所有u∈U,v∈V-U的边(u, v)∈E中找一条代价最小的边( u 0 , v 0 u_0,v_0 u0,v0)并入集合TE,同时 v 0 v_0 v0并入U,直至U=V为止。此时TE中必有n-1条边,则T=(U, TE)为N的最小生成树。
普里姆算法的具体实现
在实际的算法实现中,需要创建两个辅助数组 closest 和 lowcost。
lowcost 数组记录各顶点到在建生成树的最小权值,例如lowcost[ i ]=0代表顶点 v i v_i vi已经被加入到在建生成树的顶点集U中,lowcost[ i ]= + ∞ +\infty +∞表示顶点 v i v_i vi不在U中且与在建生成树中的任意顶点均不直接连通,lowcost[ i ]=num表示顶点 v i v_i vi不在U中且与在建生成树中的某些顶点直接连通并且所有连通的边中权值最小的边权值为num。
closest 数组记录顶点 v i v_i vi到生成树的最小权值边在生成树一端的顶点。
lowcost 数组首先需要根据邻接矩阵初始化为起始顶点 v i v_i vi到其它各顶点的权值,lowcost[ j ] = matrix[ locate( v i v_i vi) ][ j ],因为无向网中matrix[ locate( v i v_i vi) ][ locate( v i v_i vi) ]为 + ∞ +\infty +∞,所以根据lowcost数组的定义还需要将lowcost[ locate( v i v_i vi) ] 初始化为0。closest数组初始化为起始顶点 v i v_i vi。
以上面左图无向网的邻接矩阵为例,普里姆算法的java实现:
public class Prim {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F' }; // 顶点数组
int[][] matrix = new int[6][6]; // 邻接矩阵
int INF = 1 << 31 - 1; // INF表示正无穷
// 创建邻接矩阵
private void creatMartix() {
matrix[locate('A')][locate('B')] = matrix[locate('B')][locate('A')] = 6;
matrix[locate('B')][locate('E')] = matrix[locate('E')][locate('B')] = 3;
matrix[locate('E')][locate('F')] = matrix[locate('F')][locate('E')] = 6;
matrix[locate('F')][locate('D')] = matrix[locate('D')][locate('F')] = 2;
matrix[locate('D')][locate('A')] = matrix[locate('A')][locate('D')] = 5;
matrix[locate('C')][locate('A')] = matrix[locate('A')][locate('C')] = 1;
matrix[locate('C')][locate('B')] = matrix[locate('B')][locate('C')] = 5;
matrix[locate('C')][locate('E')] = matrix[locate('E')][locate('C')] = 6;
matrix[locate('C')][locate('F')] = matrix[locate('F')][locate('C')] = 4;
matrix[locate('C')][locate('D')] = matrix[locate('D')][locate('C')] = 5;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
if (matrix[i][j] == 0) {
matrix[i][j] = INF;
}
}
}
}
private int locate(char v) {
int i = 0;
for (; i < vertex.length; i++) {
if (v == vertex[i])
break;
}
return i;
}
// 普里姆算法
private void prim(char v) {
int[] lowcost = new int[matrix.length];
char[] closest = new char[matrix.length];
// lowcost、closest数组的初始化
for (int j = 0; j < matrix.length; j++) {
lowcost[j] = matrix[locate(v)][j];
closest[j] = v;
}
lowcost[locate(v)] = 0;
int mincost;
int k = 0; // 保存离在建生成树最近顶点的数组下标
for (int i = 1; i < matrix.length; i++) { // n个顶点的图需要寻找n-1次
mincost = INF;
// 根据各顶点到在建生成树的最小权值找出离在建生成树最近的顶点
for (int j = 0; j < matrix.length; j++) {
// lowcost[j]!=0限制只在所有生成树外的顶点与生成树之间的边中寻找
if (lowcost[j] != 0 && lowcost[j] < mincost) {
mincost = lowcost[j];
k = j;
}
}
// 根据找到的最近顶点输出最短边的信息
System.out.println("边 (" + closest[k] + "," + vertex[k] + ") 权:" + mincost);
// 将这个顶点加入到生成树中
lowcost[k] = 0;
// 因为生成树中新加入了顶点,所以需要重新更新在建生成树以外的顶点到在建生成树的最小距离
for (int j = 0; j < matrix.length; j++) {
if (lowcost[j] != 0 && matrix[k][j] < lowcost[j]) {
lowcost[j] = matrix[k][j];
closest[j] = vertex[k];
}
}
}
}
public static void main(String[] args) {
Prim p = new Prim();
p.creatMartix();
p.prim('A');
}
}
算法分析
假设连通网N=(V, E),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V, { }),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。
算法的关键点在于如何判断代价最小的边加入T后是否落在T的不同连通分量上,也就是判断T中是否出现了环,这里使用了一种叫做并查集的数据结构,通过这种数据结构可以方便的查找一个元素所属的集合和对两个集合进行合并。并查集(disjoint set)视频讲解。
这种数据结构的实现是:
用克鲁斯卡尔算法生成上面左图无向网的最小生成树:
public class Kruskal {
class Edge {
char begin;
char end;
int weight;
public Edge(char begin, char end, int weight) {
this.begin = begin;
this.end = end;
this.weight = weight;
}
}
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F' };
Edge[] edges;
/*
* 这里为了方便直接按权值大小顺序手动添加网的所有边。
* 也可以根据邻接矩阵的下三角或者上三角生成边数组,
* 生成之后还要根据边的权值大小对生成的边数组排序。
*/
private void creatEdges() {
edges = new Edge[10];
edges[0] = new Edge('A', 'C', 1);
edges[1] = new Edge('D', 'F', 2);
edges[2] = new Edge('B', 'E', 3);
edges[3] = new Edge('C', 'F', 4);
edges[4] = new Edge('B', 'C', 5);
edges[5] = new Edge('C', 'D', 5);
edges[6] = new Edge('A', 'D', 5);
edges[7] = new Edge('A', 'B', 6);
edges[8] = new Edge('C', 'E', 6);
edges[9] = new Edge('E', 'F', 6);
}
class TreeNode {
char verName; // 结点表示的顶点
int parent; // 结点父结点的位置下标
int rank; // 保存以该结点为根结点的树的高度
public TreeNode(char verName) {
this.verName = verName;
}
}
TreeNode[] tree = { new TreeNode('A'),
new TreeNode('B'),
new TreeNode('C'),
new TreeNode('D'),
new TreeNode('E'),
new TreeNode('F') };
private void initTree() {
for (int i = 0; i < tree.length; i++) {
tree[i].parent = i; // 初始每个结点的父结点都指向自己
tree[i].rank = 1; // 所有单独的顶点都是一棵高度为1的树
}
}
private void kruskal() {
creatEdges();
initTree();
int i = 1;
int j = 0;
char begin;
char end;
// n个顶点的最小生成树n-1条边,所以需要循环添加n-1次
while (i <= vertex.length - 1) {
begin = edges[j].begin;
end = edges[j].end;
// 如果边的两个顶点属于两棵不同的子树则加入该边不会构成环
if (findRoot(begin) != findRoot(end)) {
// 输出边的信息并根据边两端的顶点将顶点所在子树合并
System.out.println("边 (" + begin + "," + end + ") 权:" + edges[j].weight);
union(begin, end);
i++;
}
// 判断下一条边
j++;
}
}
// 将两棵不相交的子树合并
private void union(char begin, char end) {
int i = findRoot(begin);
int j = findRoot(end);
// begin顶点所在子树的高度大于end顶点所在子树的高度
if (tree[i].rank > tree[j].rank) {
// end顶点所在子树的根结点连接在begin顶点所在子树的根结点
tree[j].parent = i;
} else {
// 否则begin顶点所在子树的根结点连接在end顶点所在子树的根结点
tree[i].parent = j;
// 如果两棵子树高度相等,那么连接之后需要将end顶点所在子树的高度+1
if (tree[i].rank == tree[j].rank) {
tree[j].rank++;
}
}
}
// 根据所给顶点返回顶点所在的子树的根结点位置
private int findRoot(char c) {
// 找到所给顶点的位置
int index = locate(c);
// 根据所给顶点循环找到所在子树的根结点,根结点的特点是父结点也指向自己
while (index != tree[index].parent) {
index = tree[index].parent;
}
return index;
}
// 返回顶点在所有子树的存储数组中的位置
private int locate(char v) {
int i = 0;
for (; i < tree.length; i++) {
if (v == tree[i].verName)
break;
}
return i;
}
public static void main(String[] args) {
Kruskal k = new Kruskal();
k.kruskal();
}
}
算法分析