概念
- 最小生成树(Minimum Spanning Tree)
- 最小生成树存在 ↔ 图连通
- 是一棵树
- 无回路
- |V|个顶点一定有|V|-1条边
- 向生成树中任加一条边都一定构成回路
- 是生成树
- 边的权重和最小
贪心算法
- 什么是“贪”:每一步都要最好的
- 什么是“好”:权重最小的边
- 需要约束:
- 只能用图里有的边
- 只能正好用掉|V|-1条边
- 不能有回路
- 两种算法:
- Prim算法(普里姆算法)
- Kruskal算法(克鲁斯卡尔算法)
Prim算法(普里姆算法) — 让一棵小树长大
- 从起始节点开始,每次都加入最小权重的边和节点
- 参考博客
- 和最短路径问题的 Dijkstra 算法(迪杰斯特拉算法)有相似之处
- T = O( |V|2 )
- 稠密图合算
void Prim()
{
MST = {s};
while (1) {
V = 未收录顶点中dist最小者;
if ( 这样的V不存在 )
break;
将V收录进MST: dist[V] = 0;
for ( V 的每个邻接点 W )
if ( W未被收录 )
if ( E(V,W) < dist[W] ){
dist[W] = E(V,W) ;
parent[W] = V;
}
}
if ( MST中收的顶点不到|V|个 )
Error ( “生成树不存在” );
}
代码实现
/*
* prim最小生成树
*
* 参数说明:
* start -- 从图中的第start个元素开始,生成最小树
*/
public void prim2(int start) {
boolean[] collected = new boolean[size];
collected[start] = true;
int[] dist = new int[size];
System.arraycopy(edges[start], 0, dist, 0, size);
dist[start] = 0;
// 这里path和以上不同,以上的path是记录索引路过所在节点的上一个节点的索引
// 这里path是路过的节点顺序
List<Integer> path = ListUtil.toList(start);
while (true) {
// 寻找最短路径相接的节点
int min = Integer.MAX_VALUE, v = -1;
for (int j = 0; j < size; j++) {
if (!collected[j] && min > dist[j]) {
min = dist[j];
v = j;
}
}
if (v == -1) {
// 无法找到
break;
}
collected[v] = true;
path.add(v);
// 当第w个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
for (int w = 0; w < size; w++) {
// 当第w个节点没有被处理,并且需要更新时才被更新。
if (!collected[w] && edges[v][w] < dist[w]) {
dist[w] = edges[v][w];
}
}
}
if (path.size() != size) {
throw new RuntimeException("生成树不存在");
}
// 打印最小生成树
Console.log("start:", start);
Console.log("path:", path);
Console.log("dist:", dist);
Console.log("最小生成树权重:", NumberUtil.add(Arrays.stream(dist).boxed().toArray(Number[]::new)));
}
Kruskal算法(克鲁斯卡尔算法) — 将森林合并成树
- 每次选择权重最小的边,判断是否构成回路
- 参考博客
- T = O( |E| log |E| )
void Kruskal ( Graph G )
{
MST = { } ;
while ( MST 中不到 |V|-1 条边 && E 中还有边 ) {
从 E 中取一条权重最小的边 E(v,w) ; /* 最小堆 */
将E(v,w)从 E 中删除;
if ( E(V,W)不在 MST 中构成回路) /* 并查集 */
将E(V,W) 加入 MST;
else
彻底无视 E(V,W);
}
if ( MST 中不到 |V|-1 条边 )
Error ( “生成树不存在” );
}
代码实现
/*
* 克鲁斯卡尔(Kruskal)最小生成树
*/
public void kruskal2() {
// 并查集
int[] parent = new int[size];
Arrays.fill(parent, -1);
List<Pair<Integer, Integer>> retList = ListUtil.toList();
// 最小堆,(start,end) -> weight
PriorityQueue<Pair<Pair<Integer, Integer>, Integer>> edgePriorityQueue = new PriorityQueue<>(
Comparator.comparingInt(Pair::getValue));
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (edges[i][j] != 0 && edges[i][j] != Integer.MAX_VALUE) {
edgePriorityQueue.offer(new Pair<>(new Pair<>(i, j), edges[i][j]));
}
}
}
int weightSum = 0;
while (!edgePriorityQueue.isEmpty()) {
Pair<Pair<Integer, Integer>, Integer> pair = edgePriorityQueue.poll();
Pair<Integer, Integer> startEndPair = pair.getKey();
Integer start = startEndPair.getKey();
Integer end = startEndPair.getValue();
Integer weight = pair.getValue();
// 并查集判断是否存在回路
int startRoot = find(parent, start);
int endRoot = find(parent, end);
if (startRoot != endRoot) {
// 不构成回路
retList.add(startEndPair);
weightSum += weight;
union(parent, start, end);
}
}
if (retList.size() != size - 1) {
throw new RuntimeException("生成树不存在");
}
Console.log(weightSum, retList);
}
public static int find(int[] parent, int i) {
if (parent[i] == -1) {
return i;
}
return find(parent, parent[i]);
}
public static void union(int[] parent, int x, int y) {
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}