克鲁斯卡尔算法
问题描述
修路问题:七个村庄(A,B,C,D,E,F,G)要修路,七个村庄之间路线很多,要怎样才可以在保证联通所有村庄的同时修最短的路
思路分析
我们可以把所有的边存入数组,然后按照从小到大的顺序取出2构建最小树,但是要保证取出的边不会构成回路。如图我们有十条边,先都存入数组,然后取出依次取出,先取出最短的F->E,不构成回路,继续取C->D,依然图中不构成回路,继续取D->E,现在可以看见F->C这条线是联通的了,然后继续取最短,这时可以发现C->F是最短的,但是很明显的看到如果加入了C->F这条路就构成了回路。那我们是不是可以理解为按照从小到大的顺序去取出,只要不构成回路问题就解决了。
回路问题思路(重点理解)
如果我们有个数组动态记录每次取出后这些点的终点,那我们是不是就可以解决问题。最开始的时候每个点的终点就是自己,当第一次取出(F->E)这条边后,F的终点还是自己,但是E的终点就是F了,然后继续取,取出C->D,C的终点就是D,D的终点还是D,继续取D->E,当这条边取出来的时候D的终点就是跟E一样了(D的终点引用E的,E是F) ,因为已经串起来成为一条线了,那么此时(C,D,E,F)的终点都是F,继续取C->F,此时这条边加入的时候C指向F,F指向F。这时就构成回路了。简而言之,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。
问题解决思路
这个时候我们要做的就是两件事。首先把所有的边存起来,然后排序,保证从小到大的顺序。第二步就是取出来每条边的同时判断是否构成回路。当边全部取完时,问题解决。
代码实现
public class Kruskal {
private int edgeNum;//边的个数
private char[] vertexs;//顶点数组
private int[][] matrix; // 邻接矩阵
private static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E'
, 'F', 'G'};
int[][] matrix = {
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ {0, 12, INF, INF, INF, 16, 14},
/*B*/ {12, 0, 10, INF, INF, 7, INF},
/*C*/ {INF, 10, 0, 3, 5, 6, INF},
/*D*/ {INF, INF, 3, 0, 4, INF, INF},
/*E*/ {INF, INF, 5, 4, 0, 2, 8},
/*F*/ {16, 7, 6, INF, 2, 0, 9},
/*G*/ {14, INF, INF, INF, 8, 9, 0}};
Kruskal kruskal = new Kruskal(vertexs, matrix);
kruskal.print();
kruskal.kruskal();
}
/**
* 定义构造方法初始化数据
* @param vertexs 顶点数组
* @param matrix 领接矩阵
*/
public Kruskal(char[] vertexs, int[][] matrix) {
int vlen = vertexs.length;
this.vertexs = new char[vlen];
this.matrix = new int[vlen][vlen];
// 初始化顶点
for (int i = 0; i < vlen; i++) {
this.vertexs[i] = vertexs[i];
}
// 初始化二维数组
for (int i = 0; i < vlen; i++) {
for (int j = 0; j < vlen; j++) {
this.matrix[i][j] = matrix[i][j];
}
}
// 计算有多少条边
for (int i = 0; i < vlen; i++) {
for (int j = i + 1; j < vlen; j++) {
if (this.matrix[i][j] != INF) {
edgeNum++;
}
}
}
}
public void kruskal(){
// 结果数组的索引
int index = 0;
// 用于保存已生成的最小数中的每个顶点在最小生成树中的终点
int[] ends = new int[edgeNum];
// 创建结果数组 保存最后的最小生成树
EData[] rets = new EData[edgeNum];
// 获取图中所有的边集合
EData[] edges = getEdges();
sortEdges(edges);
// 遍历数组将边添加到最小生成树是 判断是否形成回路
for (int i = 0; i < edgeNum; i++) {
int p1 = getPosition(edges[i].start);
int p2 = getPosition(edges[i].end);
// 获取p1顶点在这个最小生成树中的终点
int m = getEnd(ends,p1);
int n = getEnd(ends,p2);
// 判断是否构成回路
if (m != n){
ends[m] = n; // 设置m在已有最小生成树中的终点
rets[index++] = edges[i];
}
}
System.out.println("最小生成树");
for (int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}
public void print() {
System.out.println("领接矩阵为");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%13d", matrix[i][j]);
}
System.out.println();
}
}
// 我们需要对边的权值进行排序
private void sortEdges(EData[] edges) {
for (int i = 0; i < edges.length - 1; i++) {
for (int j = 0; j < edges.length - 1 - i; j++) {
if (edges[j].weight > edges[j + 1].weight) {
EData temp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = temp;
}
}
}
}
// 返回顶点对应的下标
private int getPosition(char ch) {
for (int i = 0; i < vertexs.length; i++) {
if (vertexs[i] == ch) {
return i;
}
}
return -1;
}
// 获取图中的边 放入数组中
private EData[] getEdges() {
int index = 0;
EData[] edgs = new EData[edgeNum];
for (int i = 0; i < vertexs.length; i++) {
for (int j = i + 1; j < vertexs.length; j++) {
if (matrix[i][j] != INF) {
edgs[index++] = new EData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edgs;
}
/**
* 获取下标为i的顶点的终点 用于后面判断两个顶点的
* 终点是否相同
* @param ends 数组就是记录各个顶点对应的终点是哪个
* ends数组是在遍历的过程中 逐步形成的
* @param i 传入顶点的下标
* @return 返回的是下标为i的这个顶点对应终点的下标
*/
private int getEnd(int[] ends,int i){
while (ends[i] != 0){
i = ends[i];
}
return i;
}
}
/**
* 创建一个类EData,它的对象实例就表示一条边
*/
class EData {
char start;
char end;
int weight;
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EData{<" +start +
", " + end +
">=" + weight +
'}';
}
}
运行结果
总结
相比起普利姆算法点击传送,克鲁斯卡尔相对适合求边稀疏的网的最小生成树。在这里,克鲁斯卡尔算法更多的是去考虑是否构成回路这个问题,如果这里想通了,应该就很容易想通了。
秃头萌新一枚 多多关照