破圈法求最小生成树_JavaScript 实现 最小生成树

cbe75b225ddbc86cebbbbb656eacb80f.png

一 : 了解最小生成树

最小生成树(MST)问题是网络设计中常见的问题。想象一下,你的公司有几间办公室,要 以最低的成本实现办公室电话线路相互连通,以节省资金,最好的办法是什么?

这也可以应用于岛桥问题。设想你要在 n 个岛屿之间建造桥梁,想用最低的成本实现所有岛 屿相互连通。

这两个问题都可以用 MST 算法来解决,其中的办公室或者岛屿可以表示为图中的一个顶点, 边代表成本。下面有一个图的例子,其中较粗的边是一个 MST 的解决方案

6149749df73cfd6b7217a8d6f600f81c.png

本文我们将学习两种主要的求最小生成树的算法:Prim 算法Kruskal 算法。

下面的算法都是按照 上图为参考

二 :普里姆(prim)算法

Prim 算法是一种求解加权无向连通图的 MST 问题的贪心算法 ,他和我之前的一篇文章中的 迪杰斯特拉算法 特别相似 ,建议先阅读 迪杰斯特拉 再来了解 prim

前端陈嘉豪:JavaScript实现最短路径算法​zhuanlan.zhihu.com
10024f2006c906531dfc575b5de8a8a9.png

先来看下函数的输入与输出

输入 :传入 加权无向连通图 , 和源顶点 0 ( A )

fc74079f656929cfb44364f9018641ac.png

输出 :

4f32a1883cf777ff79be12c3fc558ec0.png

表示最小生成树 有 五 条边(-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 : 并查集

排序算法 我后面会出文章来统一介绍 ,如果不理解 不影响此算法阅读

来看下并查集:这是一种可以用来检测 一个图是否是环的算法.

470f14ffd0f205fda9dd6f383b9e5d17.png

假设 我们 连接 0=>1=>2=>3=> 4 此时 不构成环 我们用 右边的树结构模拟图

这个树 用 parent 数组来表示 ,

0 的父节点 是 1 ,

1 的父节点是 4 .... 依次类推

4没有父节点 我们存储-1

这个时候 假设 连接 2 => 4

2a1562db125b008a7b5c67cfc3bda2ba.png

此时 我们从数组中 查询 2 的根 和 4的根 相同 =>构成了环

OK :我们 掌握了前面得到基础之后 来看下 神秘的克鲁斯卡尔算法

先看下他的输入 和输出

输入 :我们直接把加权的无向图丢进去就好了

809f3438258ebbe6751df4fe4f2976e3.png

输出

5c24440f48c35b424f5a4df09029e666.png

我封装的这个和别人的可能不太一样 ,

我直接把每条边用 顶点的方式存进数组中返回

这些顶点组成的边 构成下面的 最小生成树(粗线表示)

aad70c398b6e46ae2f3c54946e20dac2.png

来看下函数内部是如何工作的

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 (循环的出口)

恭喜您 最后的图 就是最小生成树, 证明部分 我们就不给出了 看下面一张图展示了这个过程

c6825d0f215c942c44db301efa8da255.png

1d6603f282bb3ba319a0fb6d44e01a70.png

构成了环 , 8=>6 被丢弃 . 插入后续的边

四 :小结

如果 文字很无力 很难理解 ,我找到了 几个资料供大家学习

最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​www.bilibili.com
bb6ca6dc8c76f2231db0f14c0e019b1b.png
【算法】并查集(Disjoint Set)[共3讲]_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​www.bilibili.com
ba6046dcf88a4b6174eddd1f9f6fe87b.png

加油!!!!!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值