克鲁斯卡尔(Kruskal)算法基本介绍

应用场景

公交站问题

image-20221129095049596

  1. 某城市新增7个站点(A,B,C,D,E,F,G),现在需要修路把7个站点连通。
  2. 各个站点的距离用边线表示(权),例如A-B距离12公里。
  3. 问:如何修路保证各个站点都能连通,并且修建公路总里程最短?

基本介绍

  1. 克鲁斯卡尔(Sruskal)算法,是用来求加权连通图的最小生成树的算法。
  2. 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
  3. 具体做法:首先构造一个只含n个顶点的森林,然后按照权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直到森林变成一棵树为止。

在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树。

image-20221129115558878

例如,对于如上图所示的连通网可以有多棵权值总和不相同的生成树。

image-20221129115836995

步骤图解

20221129130753

操作步骤:

  1. 将边<E,F>加入R中。边<E,F>的权值最小,因此将它加入到最小生成树结果R中。
  2. 将边<C,D>加入R中。上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中。
  3. 将边<D,E>加入R中。上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中。
  4. 将边<B,F>加入R中。上一 步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果R中。
  5. 将边<E,G>加入R中。上一 步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中。
  6. 将边<A,B>加入R中。上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果R中。
  7. 此时,最小生成树构造完成!它包括的边依次是: <E,F> <C,D> <D,E> <B,F> . <E,G> <A,B>。

算法分析

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决以下两个问题:

  1. 对图的所有边按照权值大小进行排序。
  2. 将边添加到最小生成树中时,怎么判断是否形成了回路。

解决办法:

  1. 问题1采用排序算法即可;
  2. 问题2处理方式为:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话会构成回路。

如何判断是否构成回路

image-20221129131230751

在将<E,F> <C,D> <D,E>加入到最小生成树R中之后,这几条边的顶点就都有了终点:

  • C的终点是F。
  • D的终点是F。
  • E的终点是F。
  • F的终点是F。

关于终点的说明:

  1. 就是将所有顶点按照从小到大的顺序排列好之后,某个顶点的终点就是"与它连通的最大顶点"。
  2. 虽然<C,E>是权值最小的边。但是C和E的终点都是F,即它们的终点相同;因此,将<C,E>加入最小生成树的话,会形成回路;这就是判断回路的方式。
  3. 也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路

Kruskal算法解决公交问题-构建图

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.algorithm.kruskal;

/**
* @author Joker大雄
* @data 2022/11/29 - 13:21
**/
public class KruskalCase {

private int edgeNum; // 边的个数
private char[] vertexs; // 顶点数组
private int[][] matrix; // 邻接矩阵
// 使用Integer最大值表示两个顶点不能连通
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}};
// 创建克鲁斯卡尔对象实例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
// 输出
kruskalCase.print();
}

// 构造器
public KruskalCase(char[] vertexs,int[][] matrix){
// 初始化顶点数和边的个数
int vlen = vertexs.length;

// 初始化顶点,拷贝方式
this.vertexs = new char[vlen];
for(int i=0;i<vertexs.length;i++){
this.vertexs[i] = vertexs[i];
}
// 初始化边
this.matrix = new int[vlen][vlen];
// 初始化,拷贝方式
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 = 0; j < vlen; j++) {
if(this.matrix[i][j] != INF){// 边有效
edgeNum++;
}
}
}
}
// 打印邻接矩阵
public void print(){
System.out.println("邻接矩阵为:\n");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%10d\t",matrix[i][j]);
}
System.out.println();
}
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
邻接矩阵为:

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

Process finished with exit code 0

Kruskal算法解决公交问题-统计边

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package com.algorithm.kruskal;

import java.util.Arrays;

/**
* @author Joker大雄
* @data 2022/11/29 - 13:21
**/
public class KruskalCase {

private int edgeNum; // 边的个数
private char[] vertexs; // 顶点数组
private int[][] matrix; // 邻接矩阵
// 使用Integer最大值表示两个顶点不能连通
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}};
// 创建克鲁斯卡尔对象实例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
// 输出
// kruskalCase.print();
// 获取到边的集合
EData[] edge = kruskalCase.getEdges();
// 返回边的数组
System.out.println("未排序:"+Arrays.toString(edge));
// 测试排序
kruskalCase.sortEdges(edge);
System.out.println("排序后:"+Arrays.toString(edge));
}

// 构造器
public KruskalCase(char[] vertexs,int[][] matrix){
// 初始化顶点数和边的个数
int vlen = vertexs.length;

// 初始化顶点,拷贝方式
this.vertexs = new char[vlen];
for(int i=0;i<vertexs.length;i++){
this.vertexs[i] = vertexs[i];
}
// 初始化边
this.matrix = new int[vlen][vlen];
// 初始化,拷贝方式
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 print(){
System.out.println("邻接矩阵为:\n");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%10d\t",matrix[i][j]);
}
System.out.println();
}
}
// 按照边的权值进行排序,这里用冒泡
/**
* @param edges 边的集合
*/
public 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;
}
}
}
}
// 根据顶点返回对应的下标
/**
*
* @param ch 传入一个顶点,例如'C','D'
* @return 返回顶点对应的下标,找不到返回-1
*/
private int getPosition(char ch){
for (int i = 0; i < vertexs.length; i++) {
if(vertexs[i] == ch){
return i;
}
}
return -1;
}

/**
* 功能:获取图中的边,放到EData[]数组中,然后遍历该数组
* 通过matrix邻接矩阵来获取
* EData[] 形式[['A','B',12],......]
* @return
*/
private EData[] getEdges(){
int index = 0;
EData[] edge = 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){
edge[index++] = new EData(vertexs[i],vertexs[j],matrix[i][j]);
}
}
}
return edge;
}

}
// 创建一个类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;
}
// 重写toString方法,便于输出边
@Override
public String toString() {
return "EData{" +
"<" + start +
"," + end +
">=" + weight +
'}';
}
}

运行结果

1
2
3
4
未排序:[EData{<A,B>=12}, EData{<A,F>=16}, EData{<A,G>=14}, EData{<B,C>=10}, EData{<B,F>=7}, EData{<C,D>=3}, EData{<C,E>=5}, EData{<C,F>=6}, EData{<D,E>=4}, EData{<E,F>=2}, EData{<E,G>=8}, EData{<F,G>=9}]
排序后:[EData{<E,F>=2}, EData{<C,D>=3}, EData{<D,E>=4}, EData{<C,E>=5}, EData{<C,F>=6}, EData{<B,F>=7}, EData{<E,G>=8}, EData{<F,G>=9}, EData{<B,C>=10}, EData{<A,B>=12}, EData{<A,G>=14}, EData{<A,F>=16}]

Process finished with exit code 0

Kruskal算法解决公交问题-算法实现

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package com.algorithm.kruskal;

import java.util.Arrays;

/**
* @author Joker大雄
* @data 2022/11/29 - 13:21
**/
public class KruskalCase {

private int edgeNum; // 边的个数
private char[] vertexs; // 顶点数组
private int[][] matrix; // 邻接矩阵
// 使用Integer最大值表示两个顶点不能连通
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}};
// 创建克鲁斯卡尔对象实例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
// 克鲁斯卡尔算法-> 最小生成树
kruskalCase.kruskal();
}

// 构造器
public KruskalCase(char[] vertexs,int[][] matrix){
// 初始化顶点数和边的个数
int vlen = vertexs.length;

// 初始化顶点,拷贝方式
this.vertexs = new char[vlen];
for(int i=0;i<vertexs.length;i++){
this.vertexs[i] = vertexs[i];
}
// 初始化边
this.matrix = new int[vlen][vlen];
// 初始化,拷贝方式
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 print(){
System.out.println("邻接矩阵为:\n");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%10d\t",matrix[i][j]);
}
System.out.println();
}
}
// 使用克鲁斯卡尔完成 最小生成树
public void kruskal(){
int index = 0; // 表示结果数组的索引
// 用于保存已有 "最小生成树"中的每个顶点在最小生成树的终点
int[] ends = new int[edgeNum];
// 创建结果数组,保存最后的最小生成树
EData[] rets =new EData[edgeNum];

// 获取图中所有的边的集合,初始一共有12条边
EData[] edges = getEdges();

// 按照边的权值大小进行排序(这里我们从小打大)
sortEdges(edges);
// 遍历edges数组,判断准备加入的边是否构成回路,如果没有构成就加入rets,否则不加入
for (int i = 0; i < edgeNum; i++) {
// 获取到第i条边的第一个顶点(起点)
int p1 = getPosition(edges[i].start);
// 获取到第i条变的第2个顶点
int p2 = getPosition(edges[i].end);

// 获取p1顶点在已有最小生成树中的终点
int m = getEnd(ends,p1);
// 获取p2顶点在已有最小生成树中的终点
int n = getEnd(ends,p2);
// 判断是否构成回路
if(m != n){// 不构成回路
ends[m] = n; // 设置m在已有最小生成树中的终点
rets[index++] = edges[i];// 有一条边加入到rets数组
}
}
// 统计并打印 最小生成树
System.out.println("最小生成树:");
for (int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}

// 按照边的权值进行排序,这里用冒泡
/**
* @param edges 边的集合
*/
public 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;
}
}
}
}
// 根据顶点返回对应的下标
/**
*
* @param ch 传入一个顶点,例如'C','D'
* @return 返回顶点对应的下标,找不到返回-1
*/
private int getPosition(char ch){
for (int i = 0; i < vertexs.length; i++) {
if(vertexs[i] == ch){
return i;
}
}
return -1;
}

/**
* 功能:获取图中的边,放到EData[]数组中,然后遍历该数组
* 通过matrix邻接矩阵来获取
* EData[] 形式[['A','B',12],......]
* @return
*/
private EData[] getEdges(){
int index = 0;
EData[] edge = 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){
edge[index++] = new EData(vertexs[i],vertexs[j],matrix[i][j]);
}
}
}
return edge;
}
/**
* 功能:获取下标为i的顶点的终点,来判断两个顶点的终点是否相同
* @param 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;
}
// 重写toString方法,便于输出边
@Override
public String toString() {
return "EData{" +
"<" + start +
"," + end +
">=" + weight +
'}';
}
}

运行结果

1
2
3
4
5
6
7
8
9
最小生成树:
EData{<E,F>=2}
EData{<C,D>=3}
EData{<D,E>=4}
EData{<B,F>=7}
EData{<E,G>=8}
EData{<A,B>=12}

Process finished with exit code 0