啥是生成树?
在图论的数学领域中,如果连通图 G(V,E) 的一个子图是一棵包含 G 的所有顶点及 |V|-1 条边的树,则该子图称为 G(V,E) 的生成树(SpanningTree)。生成树是连通图的包含图中的所有顶点的极小连通子图。一个图的生成树可以有多颗。
最小生成树
也称最小权重生成树,在生成树的概念上加一个限制条件,即生成树的所有边的权值总和最小的树,最小生成树也可以有多颗。
应用
- 乡镇间电缆布线设计
- 办公楼房网络设计
- 建筑间电路设计
适用对象
- 加权无向图
- 连通图
- 对于不连通的图,可以求所有是 连通分量 的最小生成树形成最小森林
最小生成树有两个经典的算法: Prim 算法 和 Kruskal 算法。
Prim 算法(节点扩展算法)
基本思想:从图中任意一个顶点开始,每次选择与当前顶点集距离最近的顶点,将对应的边加入到树中,直至所有顶点被处理完。
代码实现:
const INF = Number.MAX_SAFE_INTEGER;
const minKey = (graph, key, visited) => {
let min = INF;
let minIndex = 0;
for (let v = 0; v < graph.length; v++) {
if (visited[v] === false && key[v] < min) {
min = key[v];
minIndex = v;
}
}
return minIndex;
};
export const prim = graph => {
const parent = [];
const key = [];
const visited = [];
const { length } = graph;
for (let i = 0; i < length; i++) {
key[i] = INF;
visited[i] = false;
}
key[0] = 0;
parent[0] = -1;
for (let i = 0; i < length - 1; i++) {
const u = minKey(graph, key, visited);
visited[u] = true;
for (let v = 0; v < length; v++) {
if (graph[u][v] && !visited[v] && graph[u][v] < key[v]) {
parent[v] = u;
key[v] = graph[u][v];
}
}
}
return parent;
};
// test
const graph = [
[0, 2, 4, 0, 0, 0],
[2, 0, 2, 4, 2, 0],
[4, 2, 0, 0, 3, 0],
[0, 4, 0, 0, 3, 2],
[0, 2, 3, 3, 0, 2],
[0, 0, 0, 2, 2, 0]
];
const parent = prim(graph);
console.log('Edge Weight');
for (let i = 1; i < graph.length; i++) {
console.log(parent[i] + ' - ' + i + ' ' + graph[i][parent[i]]);
}
// result
Edge Weight
0 - 1 2
1 - 2 2
5 - 3 2
1 - 4 2
4 - 5 2
Kruskal 算法(边扩展算法)
与 Prim 算法不同的是,Prim 是以顶点为关键来生成最小树的,而 Kruskal 是以边为关键来生成最小数。
Kruskal 算法的过程:
(1) 将全部边按照权值由小到大排序。
(2) 按顺序(边权由小到大的顺序)考虑每条边,只要这条边和我们已经选择的边不构成环,就保留这条边,否则放弃这条边。
代码实现:
const INF = Number.MAX_SAFE_INTEGER;
const find = (i, parent) => {
while (parent[i]) {
i = parent[i];
}
return i;
};
const union = (i, j, parent) => {
if (i !== j) {
parent[j] = i;
return true;
}
return false;
};
const initializeCost = graph => {
const cost = [];
const { length } = graph;
for (let i = 0; i < length; i++) {
cost[i] = [];
for (let j = 0; j < length; j++) {
if (graph[i][j] === 0) {
cost[i][j] = INF;
} else {
cost[i][j] = graph[i][j];
}
}
}
return cost;
};
export const kruskal = graph => {
const { length } = graph;
const parent = [];
let ne = 0;
let a;
let b;
let u;
let v;
const cost = initializeCost(graph);
while (ne < length - 1) {
for (let i = 0, min = INF; i < length; i++) {
for (let j = 0; j < length; j++) {
if (cost[i][j] < min) {
min = cost[i][j];
a = u = i;
b = v = j;
}
}
}
u = find(u, parent);
v = find(v, parent);
if (union(u, v, parent)) {
ne++;
}
cost[a][b] = cost[b][a] = INF;
}
return parent;
};
// test
const graph = [
[0, 2, 4, 0, 0, 0],
[2, 0, 2, 4, 2, 0],
[4, 2, 0, 0, 3, 0],
[0, 4, 0, 0, 3, 2],
[0, 2, 3, 3, 0, 2],
[0, 0, 0, 2, 2, 0]
];
const parent = prim(graph);
console.log('Edge Weight');
for (let i = 1; i < graph.length; i++) {
console.log(parent[i] + ' - ' + i + ' ' + graph[i][parent[i]]);
}
// result
Edge Weight
0 - 1 2
1 - 2 2
5 - 3 2
1 - 4 2
4 - 5 2
Kruskal 和Prim 算法对比:
Kruskal 算法主要针对边展开,时间复杂度为 O(elog e),e为图的边数,Prim 算法的时间复杂度为 O(n²),n 为最小生成树的边数。所以,边数少(稀疏图)用Kruskal 算法,边数多(稠密图)用 Prim 算法。
最小生成树各算法可视化演示
最小生成树动画演示--canvas 实现