最短路径算法

最短路径

我们把边具有权重的图称为带权图,权重可以理解为两点间的距离。一个图中任意两点会有多条路径联通,最短路径就是这些路径中最短的一条。

负环:环中所有边权之重和小于0的环

Floyed算法

算法思想

如何让两个点(假设a到b)的距离变短,只能引入第三个点k,通过k进行中转即a->k->b,当然中转点可以是多个。

算法描述

image

使用邻接矩阵E保存图,每个格子E(i, j)表示点i到点j的最短距离,不相邻的距离为Infinity。遍历每一个点作为中转点k,更新点i与点j的最短路径。

/**
 * @param matrix - 邻接矩阵,matrix[i][j]表示点i到j的距离
 */
function Floyed(matrix: number[][]) {
  const n = matrix.length
  for (let k = 0; k < n; k++) { // 遍历中转点
    // 点i到点j的最短距离更新
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        matrix[i][j] = Math.min(matrix[i][j], matrix[i][k] + matrix[k][j])
      }
    }
  }
}

算法总结

  • 时间复杂度:O(n^3)
  • Floyed算法适用于有负权边的图;但不适用于有负权环的图。

Dijkstra算法

其主要思路如下:

  1. 将顶点分为两部分:已经知道当前最短路径的顶点集合S和无法到达顶点集合U。

  2. 定义一个距离数组(distance)记录源点到各顶点的距离,下标表示顶点,元素值为距离。源点(s)到自身的距离为0,源点无法到达的顶点的距离就Infinity。

  3. 以distance中值最小且在U中(即当前距离最短,非infinity)的顶点v为中转跳点,假设v跳转至顶点w的距离加上源点s到v的距离还小于s到w的距离,那么就可以更新顶点w至源点的距离并把v从U中移除。即:

if(distance[v] + matrix[v][w] < distance[w]) { // matrix邻接矩阵
	distance[w] = distance[v] + matrix[v][w]
}
  1. 重复上一步骤,直到集合U为空。

算法实现

/**
 * @param matrix - 邻接矩阵,matrix[i][j]表示点i到j的距离
 * @param s  - 源点
 */
function Dijkstra(matrix: number[][], s: number) {
  const n = matrix.length
  const distance: number[] = matrix[s].slice()
  const U = new Set<number>()
  for (let i = 0; i < n; i++) {
    if (distance[i] = Infinity) U.add(i)
  }

  while (U.size) {
    let v: number = -1
    U.forEach(drop => {
      if (v === -1 || distance[drop] < distance[v]) v = drop
    })

    if (distance[s] + matrix[s][v] < distance[v]) {
      distance[v] = distance[s] + matrix[s][v]
    }

    U.delete(v)
  }

  return distance
}

总结

  • 时间复杂度O(n^2)
  • 需要一个源点
  • 不适用于具有负权边的图

Bellman-Ford算法

核心实现:看看引入边side能否使的源点s到点side[1]的距离变短

side数组表示点side[0]到点side[1]的边长为side[2]

算法思路

  1. 设s为源点,distance[v]为s到v的最短路径,pre[v]为v的前驱,side数组表示点side[0]到点side[1]的边长为side[2]
  2. 初始化distance[s] = 0, pre[s] = 0。
  3. 遍历每一条边,如果s到side[0]的距离加上side[2]小于s到side[1]的距离,更新s到side[1]的距离与side[1]的前驱,即:
const start = side[0]
const end = side[1]
const w = side[2]
if (distance[start] + w < distance[end]) {
	distance[end] = distance[start] + w
	pre[end] = start
}
  1. 步骤3重复松弛n-1次,如果第i次松弛distance没有更新则已完成。

算法实现

type side = [number, number, number] // side表示点side[0]到点side[1]的边长为side[2]

/**
 * 
 * @param sideList - 边的数组
 * @param n  - 顶点数,1-n
 * @param s  - 源点
 */
function Bellman(sideList: side[], n: number, s: number) {
  const distance: number[] = new Array(n + 1).fill(Infinity)
  const pre: number[] = new Array(n + 1).fill(Infinity)
  distance[s] = 0 // 点s到点i的最短距离
  pre[s] = 0 // 点s到点i的最短距离前驱

  for (let i = 1; i <= n - 1; i++) { // 松弛n-1次
    let check = false
    sideList.forEach(side => {
      const start = side[0]
      const end = side[1]
      const w = side[2]
      if (distance[start] + w < distance[end]) {
        distance[end] = distance[start] + w
        pre[end] = start
        check = true
      }
    })
    if (!check) break
  }
}

算法总结

  • 时间复杂度O(nm), n定点数,m边数
  • 可以处理负权边

SPFA算法--队列优化的Bellman-Ford算法

SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。
主要思想:
初始时将源点加入队列,每次从队列取出一个顶点v,并对v相邻的点k进行修改(与Bellman-Ford算法相同),若相邻点修改成功将其入队,直到队列为空

算法实现

type side = [number, number, number] // side表示点side[0]到点side[1]的边长为side[2]

/**
 *
 * @param sideList - 边的数组
 * @param n  - 顶点数,1-n
 * @param s  - 源点
 */
function SPFA(sideList: side[], n: number, s: number) {
  const distance: number[] = new Array(n + 1).fill(Infinity)
  const pre: number[] = new Array(n + 1).fill(Infinity)
  distance[s] = 0
  pre[s] = 0
  const queue: number[] = [s]

  while (queue.length) {
    const v = queue.shift()
    sideList.forEach(side => {
      const start = side[0]
      const end = side[1]
      const w = side[2]
      if (start === v && distance[start] + w < distance[end]) {
        distance[end] = distance[start] + w
        pre[end] = start
        queue.push(end)
      }
    })
  }
}

算法总结

  • 时间复杂度O(kE),E为边数,k为常数,平均值为2,最坏为O(VE)
  • 适用于有负权边的图;但不适用于有负权环的图。
posted @ 2024-03-07 09:12  冰凉小手  阅读(33)  评论(0编辑  收藏  举报