最小生成树的算法:
- 普里姆算法
- 克鲁斯卡尔算法
联通图#
联通图,英语为 Connected Graph,通常指的是无向图。如果一个无向图中的任意两个顶点都存在一条可以相互到达的路径,那么这个图称为“联通图”。反之则成为“非联通图”。也就是说:
- 联通图:整张图只构成一个联通块,任意两顶点之间都能到达。
- 非联通图:图中出现“孤立”或“分块”的情况。
如下图所示:

那么有向图中存不存在联通的概念呢?其实也存在,分为强联通和弱联通:
- 强联通:对于图中的任意两个顶点 V1 和 V2,都有从 V1 到 V2 的有向路径,并且也有从 V2 到 V1 的有向路径。这样的图被称之为强联通图。
- 弱联通:如果将有向图的所有有向边都看作无向边,能形成一幅联通图,则称该有向图是弱连通的。也就是说,忽略了方向后,任意两点间还是能连通,但不一定存在相互可达的有向路径。
虽然有向图也有联通的概念,不过一般说联通更多的还是指的无向图的场景。
最小生成树#
普里姆算法是一种最小生成树(Minimum Spanning Tree,MST)的算法,那什么是最小生成树呢?
举个例子:
在上面的无向图中,我们期望将所有的点都联通起来,也就是从任意一个顶点,都可以通过某一条路径连接到另外一个顶点,具体该如何连接?当然如何仅仅是联通,那么很简单,例如下图都能满足这个需求:

可以看到,方案是很多的,但是现在要添加一个需求,那就是 边权和最小。这就得仔细思考一下了,上面四种连接方式对应的方式分别是 24、25、27、23,可以看到,不同的联通方式,对应的边权和 是不一样的。
为什么要探讨这个问题呢?因为在现实生活中这就是真实的需求。假设上面各个顶点是不同的村庄,各个村庄之间原本没有路,现在要修路,不同的村庄之间修路的费用不一样的,如下图:

因此现在就是想要找一种方法,既能够联通所有的村庄(例如从 A 村一定能够到达 E 村),花费的费用又是最少的,这就是最小生成树的问题。那么为什么叫做最小生成树呢?原因很简单,最终生成的结构,一定是无环的,而 树本身就是一种无环图。
普里姆算法核心思想#
普里姆算法(Prim’s Algorithm),是一种用于在 加权无向联通图 中寻找最小生成树的经典算法,其核心思想为:
从一个顶点开始,逐步“扩张”树,把与当前树相连的最小权重边加入进来,直到包含图中所有顶点为止。
因此普里姆算法又被称之为 加点法。假设现在要针对这个图寻找最小生成树:
(1)首先先随便指定一个顶点,假设这里选择 A,接下来就看和 A 顶点联通的顶点,哪一个边权值低,这里很明显是 B 顶点,因此这里选择 B 顶点。
(2)目前就有 2 个顶点了,接下来去看这两个顶点能联通的顶点,哪一个边权值低,就点亮哪一个顶点,这里很明显点亮顶点D
(3)接下来还是相同的步骤,所有点亮的顶点,看有哪些与之联通的点,找到一个边权值最低的,然后点亮
(4)最后出现一种情况,那就是两个相同的边权值,那么由于顶点 A 和顶点 C 都点亮了,因此直接点亮顶点 E 即可
(5)因此,最终得到的最小生成树如下图
也就是说,这样连接下来的边权值的和,一定是最小的。
课堂练习
针对下面的图画出对应的最小生成树
能够生成的最小生成树的结果不是唯一:
虽然一个图的最小生成树会根据起始顶点的不同,最终生成的结果不同。但是,不管最终生成整样的最小生成树,边权值的总和一定是相等的。另外还有一个规律:针对 n 个顶点的图,最终的最小生成树的边为 n - 1.
代码实现
假设我们就用下面的加权无向联通图:
对应的 this.adjacencyList 的 Map 结构如下:
Map(5) { 'A' => [ { node: 'B', weight: 4 }, { node: 'C', weight: 7 } ], 'B' => [ { node: 'A', weight: 4 }, { node: 'D', weight: 6 }, { node: 'C', weight: 8 } ], 'D' => [ { node: 'B', weight: 6 }, { node: 'E', weight: 7 }, { node: 'C', weight: 5 } ], 'E' => [ { node: 'D', weight: 7 } ], 'C' => [ { node: 'A', weight: 7 }, { node: 'B', weight: 8 }, { node: 'D', weight: 5 } ]}/** * startVertex - 指定的起始顶点 * returns - 一个对象 { edges: [], totalWeight: 0} * edges 记录有哪些边,totalWeight 记录最小生成树的总权重 */primMST(startVertex){ if(this.isDirected){ // 进入此分支,说明当前是有向图 // 有向图一般不太适用 throw new Error("普里姆算法一般适用于无向图"); }
// 接下来判断一下图中是否存在这个起始顶点 if(!this.adjacencyList.has(startVertex)) return { edges: [], totalWeight: 0}
// 获取所有的顶点 const vertices = this.getVertices(); // ['A', 'B', 'C', 'D', 'E']
const mstSet = new Set(); // 用来存储已经加入到了 MST 的顶点 const dist = new Map(); // 记录一个顶点连接到已经生成的 MST 的最小权重 const parent = new Map(); // 记录一个顶点连接到 MST 最优的那条边的“父顶点”
// 接下来初始化 dist 和 parent for(let v of vertices){ dist.set(v, Infinity); parent.set(v, null); } dist.set(startVertex, 0); // dist = { // A: 0, // B: Infinity, // C: Infinity, // D: Infinity, // E: Infinity // } // parent = { // A: null, // B: null, // C: null, // D: null, // E: null // } // mstSet = { } (空)
// 遍历所有的顶点,每一次遍历都会将一个顶点加入到 MST 里面 for(let i = 0; i < vertices.length; i++){ // 1. 首先需要找到还没有加入到 MST 并且 dist 是最小的顶点 let current = null; let minDist = Infinity;
for(let v of vertices){ if(!mstSet.has(v) && dist.get(v) < minDist){ minDist = dist.get(v); current = v; } }
// 如果没有找到这样的顶点(可能图不联通之类的),提前结束 if(current === null) break;
// 代码来到这里,说明找到了,将选定的顶点加入到 MST 集合里面 mstSet.add(current);
// 现在 current(顶点)已经找到了,需要根据 current 顶点来更新 dist 以及 parent for(let neighborObj of this.adjacencyList.get(current)){ const neighbor = neighborObj.node; // 拿到该顶点(current)对应的邻居顶点 const weight = neighborObj.weight; // 拿到该顶点(current)到邻居顶点的加权值
// 接下来更新那些还没有加入 MST,并且存在最小边权的顶点 if(!mstSet.has(neighbor) && weight < dist.get(neighbor)){ dist.set(neighbor, weight); parent.set(neighbor, current); } } } // i = 0, current = A // mstSet = { 'A' } // 找到 A 的邻居 B(weight = 4),C(weight = 7) // dist = { A:0, B:4, C:7, D:Infinity, E:Infinity } // parent = { A:null, B:'A', C:'A', D:null, E:null }
// i = 1, current = B // mstSet = { 'A', 'B' } // 找到 B 的邻居 A(weight = 4) D(weight=6) C(weight=8) // dist = { A:0, B:4, C:7, D:6, E:Infinity } // parent = { A:null, B:'A', C:'A', D:'B', E:null }
// 生成 MST 的边信息 const mstEdges = []; let totalWeight = 0;
for(let v of vertices){ const p = parent.get(v); if(p !== null){ const w = dist.get(v); // 获取到当前这个顶点加入到 MST 时的最小边权值 mstEdges.push([p, v, w]); totalWeight += w; } }
return { mstEdges, totalWeight }}-EOF-