Skip to content

普里姆算法

最小生成树的算法:

  1. 普里姆算法
  2. 克鲁斯卡尔算法

联通图#

联通图,英语为 Connected Graph,通常指的是无向图。如果一个无向图中的任意两个顶点都存在一条可以相互到达的路径,那么这个图称为“联通图”。反之则成为“非联通图”。也就是说:

如下图所示:

image-20250313105014827

那么有向图中存不存在联通的概念呢?其实也存在,分为强联通和弱联通:

虽然有向图也有联通的概念,不过一般说联通更多的还是指的无向图的场景。

最小生成树#

普里姆算法是一种最小生成树(Minimum Spanning Tree,MST)的算法,那什么是最小生成树呢?

举个例子:

image-20250313135149109

在上面的无向图中,我们期望将所有的点都联通起来,也就是从任意一个顶点,都可以通过某一条路径连接到另外一个顶点,具体该如何连接?当然如何仅仅是联通,那么很简单,例如下图都能满足这个需求:

image-20250313135706494

可以看到,方案是很多的,但是现在要添加一个需求,那就是 边权和最小。这就得仔细思考一下了,上面四种连接方式对应的方式分别是 24、25、27、23,可以看到,不同的联通方式,对应的边权和 是不一样的。

为什么要探讨这个问题呢?因为在现实生活中这就是真实的需求。假设上面各个顶点是不同的村庄,各个村庄之间原本没有路,现在要修路,不同的村庄之间修路的费用不一样的,如下图:

image-20250313141131969

因此现在就是想要找一种方法,既能够联通所有的村庄(例如从 A 村一定能够到达 E 村),花费的费用又是最少的,这就是最小生成树的问题。那么为什么叫做最小生成树呢?原因很简单,最终生成的结构,一定是无环的,而 树本身就是一种无环图

普里姆算法核心思想#

普里姆算法(Prim’s Algorithm),是一种用于在 加权无向联通图 中寻找最小生成树的经典算法,其核心思想为:

从一个顶点开始,逐步“扩张”树,把与当前树相连的最小权重边加入进来,直到包含图中所有顶点为止。

因此普里姆算法又被称之为 加点法。假设现在要针对这个图寻找最小生成树:

image-20250313135149109

(1)首先先随便指定一个顶点,假设这里选择 A,接下来就看和 A 顶点联通的顶点,哪一个边权值低,这里很明显是 B 顶点,因此这里选择 B 顶点。

image-20250313150449672

(2)目前就有 2 个顶点了,接下来去看这两个顶点能联通的顶点,哪一个边权值低,就点亮哪一个顶点,这里很明显点亮顶点D

image-20250313150531340

(3)接下来还是相同的步骤,所有点亮的顶点,看有哪些与之联通的点,找到一个边权值最低的,然后点亮

image-20250313151328817

(4)最后出现一种情况,那就是两个相同的边权值,那么由于顶点 A 和顶点 C 都点亮了,因此直接点亮顶点 E 即可

image-20250313151429327

(5)因此,最终得到的最小生成树如下图

image-20250313151553633

也就是说,这样连接下来的边权值的和,一定是最小的。

课堂练习

针对下面的图画出对应的最小生成树

image-20250313161535241

能够生成的最小生成树的结果不是唯一:

image-20250313161934288

虽然一个图的最小生成树会根据起始顶点的不同,最终生成的结果不同。但是,不管最终生成整样的最小生成树,边权值的总和一定是相等的。另外还有一个规律:针对 n 个顶点的图,最终的最小生成树的边为 n - 1.

代码实现

假设我们就用下面的加权无向联通图:

image-20250313135149109

对应的 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-