一 : 了解最小生成树
最小生成树(MST)问题是网络设计中常见的问题。想象一下,你的公司有几间办公室,要 以最低的成本实现办公室电话线路相互连通,以节省资金,最好的办法是什么?
这也可以应用于岛桥问题。设想你要在 n 个岛屿之间建造桥梁,想用最低的成本实现所有岛 屿相互连通。
这两个问题都可以用 MST 算法来解决,其中的办公室或者岛屿可以表示为图中的一个顶点, 边代表成本。下面有一个图的例子,其中较粗的边是一个 MST 的解决方案
本文我们将学习两种主要的求最小生成树的算法:Prim 算法和 Kruskal 算法。
下面的算法都是按照 上图为参考
二 :普里姆(prim)算法
Prim 算法是一种求解加权无向连通图的 MST 问题的贪心算法 ,他和我之前的一篇文章中的 迪杰斯特拉算法 特别相似 ,建议先阅读 迪杰斯特拉 再来了解 prim
前端陈嘉豪:JavaScript实现最短路径算法zhuanlan.zhihu.com先来看下函数的输入与输出
输入 :传入 加权无向连通图 , 和源顶点 0 ( A )
输出 :
表示最小生成树 有 五 条边(-1表示没有边) 分别是 index=>arr[index]
即 以数组索引和 索引对应的值 表示边的两顶点
来看下 代码 是如何实现的
export function prim(graph: number[][], src: number) {
let dist = []
let visited = []
let parent = []
//比迪杰斯特拉多的一个列表 存前朔点
const INF = Number.MAX_SAFE_INTEGER
const length = graph.length
//初始化
for (let i = 0; i < length; i++) {
dist[i] = INF
visited[i] = false
}
dist[src] = 0
parent[src] = -1
let index = 0
//只用循环length -1 次
while (index < length - 1) {
visited[src] = true
let currentEdges = graph[src]
for (let i = 0; i < currentEdges.length; i++) {
if (currentEdges[i] !== 0) {
if (currentEdges[i] < dist[i]) {
//注意和迪杰斯特拉不同 只比较边的值
dist[i] = currentEdges[i]
parent[i] = src
}
}
}
let minIndex = -1
let min = INF
for (let i = 0; i < dist.length; i++) {
if (dist[i] < min && !visited[i]) {
min = dist[i]
minIndex = i
}
}
src = minIndex
index++
}
return parent
}
他的核心思路还是源于迪杰斯特拉 的探路模式 。
对比迪杰斯特拉 你 很快就能理解prim算法啦!
三 :克鲁斯卡尔( kruskal )算法
学习克鲁斯卡尔算法前 你需要有 两个基础
1:冒泡排序
2 : 并查集
排序算法 我后面会出文章来统一介绍 ,如果不理解 不影响此算法阅读
来看下并查集:这是一种可以用来检测 一个图是否是环的算法.
假设 我们 连接 0=>1=>2=>3=> 4 此时 不构成环 我们用 右边的树结构模拟图
这个树 用 parent 数组来表示 ,
0 的父节点 是 1 ,
1 的父节点是 4 .... 依次类推
4没有父节点 我们存储-1
这个时候 假设 连接 2 => 4
此时 我们从数组中 查询 2 的根 和 4的根 相同 =>构成了环
OK :我们 掌握了前面得到基础之后 来看下 神秘的克鲁斯卡尔算法
先看下他的输入 和输出
输入 :我们直接把加权的无向图丢进去就好了
输出 :
我封装的这个和别人的可能不太一样 ,
我直接把每条边用 顶点的方式存进数组中返回
这些顶点组成的边 构成下面的 最小生成树(粗线表示)
来看下函数内部是如何工作的
const find = (i: number, parent: number[]) => {
//查找 i元素的父节点 ,如果没有 返回 i
while (parent[i] !== -1) {
i = parent[i]
}
return i
}
const union = (i: number, j: number, parent: number[]) => {
if (i !== j) {
//父节点不相同 不是环 合并 否则是环 返回false
parent[j] = i
return true
}
return false
}
const getEdges = (graph: number[][]) => {
//将图的所有边取出
const length = graph.length
//用两个数组来表示
let edges: number[] = [] //存边的权
let vertices: number[][] = [] // 存顶点
for (let i = 0; i < length; i++) {
for (let j = 0; j < length; j++) {
if (graph[i][j] !== 0) {
graph[j][i] = 0 //防止重复取边
vertices.push([i, j]) //存取边的顶点
edges.push(graph[i][j]) //存取边的权
}
}
}
return {
edges,
vertices
}
}
const sortEdges = (edges: number[] = [], vertices: number[][] = []) => {
//冒泡排序 将所有的边从小到大排列
for (let i = 0; i < edges.length - 1; i++) {
for (let j = 0; j < edges.length - i - 1; j++) {
if (edges[j] > edges[j + 1]) {
;[edges[j], edges[j + 1]] = [edges[j + 1], edges[j]]
;[vertices[j], vertices[j + 1]] = [vertices[j + 1], vertices[j]]
}
}
}
return {
edges,
vertices
}
}
const kruskal = (graph: number[][]) => {
//克鲁斯卡尔 算法
//第一步 将图的 所有边 取出
let initEdeges = getEdges(graph)
//第二步 将所有的边 按权值 排序 由小到大
let { vertices } = sortEdges(initEdeges.edges, initEdeges.vertices)
//第三步 初始化变量
let res = []
let parent = [] //存储并查集
let k = 0 //每次取一条边 K递增
for (let i = 0; i < graph.length; i++) {
parent[i] = -1
}
// 第四步 取出边 插入图中,
// 直到插入 n-1条边 n代表 顶点的个数
while (res.length < graph.length - 1) {
let v = vertices[k]
//注意 要避免产生环 采用 并查集的方式 判断是否生成了环
const i = find(v[0], parent)
const j = find(v[1], parent)
if (union(i, j, parent)) {
//如果不是环 存入res中
res.push(v)
}
k++
}
return res
}
我们从 function kruskal() 开始读
总共有 三步核心的操作
第一步 : 将用户输出的图 所有边 (不重复的 ) 取出
第二步:将所有边 按照权值 有小到大 排序 (冒泡排序发生的地方)
第三步 :将边从小到大 依次 取出 插入 图中 ,如果 构成了环 (并查集发生的地方) , 取消插入
直到 插入的边 满足 顶点数n-1 (循环的出口)
恭喜您 最后的图 就是最小生成树, 证明部分 我们就不给出了 看下面一张图展示了这个过程
构成了环 , 8=>6 被丢弃 . 插入后续的边
四 :小结
如果 文字很无力 很难理解 ,我找到了 几个资料供大家学习
最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示_哔哩哔哩 (゜-゜)つロ 干杯~-bilibiliwww.bilibili.com加油!!!!!!!!