迪科斯特拉(Dijkstra)算法

128 阅读2分钟

前言

迪科斯特拉(Dijkstra)是一种用于解决图中最短路径问题的算法,由计算机科学家艾兹赫尔·迪科斯特拉(Edsger W. Dijkstra)在1956年发明并提出。迪科斯特拉算法可以找到从一个起始顶点到其他所有顶点的最短路径。

迪科斯特拉算法可以应用于带有非负权重的有向图或无向图中。在包含负权边的图中,要找出最短路径,可使用贝尔曼-福特(Bellman-Ford)算法

基本思想是通过遍历图中的节点来逐步确定起始节点到其他节点的最短路径长度,并记录下最短路径。算法使用了一种贪心的策略,每次选择当前距离起始节点路径长度最短的节点进行扩展。通过不断更新节点的最短路径和最短路径长度,最终得到起始节点到其他节点的最短路径和长度。

具体的算法步骤如下:

  1. 初始化:将起始节点的最短路径长度设为0,其他节点的最短路径长度设为正无穷大。
  2. 选择当前最短路径长度最小的节点,并标记为已访问。
  3. 更新与该节点相邻节点的最短路径长度:如果经过当前节点到达相邻节点的路径比已记录的最短路径更短,则更新最短路径长度。
  4. 重复步骤2和步骤3,直到所有节点都被访问过或者不存在从起始节点到未访问的节点的路径。

为了找到最短路径的具体移动路径,需要一个存储当前节点对应的上个节点的 parents 散列表,找到最短路径后进行回溯

function dijkstra(graph,startNode) {
    const visited = new Set() // 保存访问过的节点
    const distance = {} // 记录起始节点到各个节点的最短距离
    const parents = {} // 记录每个节点对应的前一个节点

    // 初始化起始节点到其他节点的距离为正无穷大
    for (const node in graph) {
        distance[node] = Infinity
    }

    // 起始节点到自身的距离为0
    distance[startNode] = 0

    // 遍历所有节点
    while(visited.size < Object.keys(graph).length) {
        let minDistance = Infinity
        let closestNode = null;
        // 选择当前距离最小且未被访问过的节点
        for (const node in distance) {
            if (!visited.has(node) && distance[node] < minDistance) {
                minDistance = distance[node]
                closestNode = node
            }
        }

        // 标记选择的节点为已访问
        visited.add(closestNode);

        // 更新与选择节点相邻节点的最短距离和前驱节点
        for (const neighbor in graph[closestNode]) {
            const weight = graph[closestNode][neighbor];
            const newDistance = distance[closestNode] + weight;
            if (newDistance < distance[neighbor]) {
                distance[neighbor] = newDistance;
                parents[neighbor] = closestNode;
            }
        }
    }

    return { distance, parents }
}

// 回溯最短路径
function shortestPaths(parents,startNode,endNode) {
    const path = [endNode];
    let currentNode = endNode;

    // 从终点往前遍历最短路径
    while (currentNode !== startNode) {
        currentNode = parents[currentNode];
        path.unshift(currentNode);  // 在数组开头添加节点
    }

    return path;
}


// 示例数据
const graph = {
    A: { B: 5, C: 2 },
    B: { A: 5, C: 2, D: 3 },
    C: { A: 2, B: 2, D: 1 },
    D: { B: 3, C: 1 },
};

const startNode = 'A';
const endNode = 'D';

const { distance, parents } = dijkstra(graph, startNode);
console.log(distance)
console.log(shortestPaths(parents, startNode,endNode));

go代码演示,大体思路一致,不过代码使用了邻接矩阵表示图,使用数组记录信息。我相信通过不同的语言和方式来实现同一套算法,更能加深我们的理解

package main

import (
	"fmt"
	"math"
)

const INF = math.MaxInt32 // 正无穷大,表示两个节点之间没有直接连接的边

func dijkstra(graph [][]int, start, end int) ([]int, []int) {
	numVertices := len(graph)             // 顶点的数量
	distances := make([]int, numVertices) // 记录起点到每个节点的最短距离
	visited := make([]bool, numVertices)  // 记录每个节点是否被访问
	parents := make([]int, numVertices)   // 记录最短路径上的每个节点的父节点

	// 初始化起点到其他节点的距离为正无穷大
	// 所有节点的上一个节点是顶点
	for i := range distances {
		distances[i] = INF
		parents[i] = -1
	}
	// 起始节点到自身的距离为0
	distances[start] = 0

	// 重复执行直到找到目标节点或所有节点都被访问过
	for !visited[end] {
		// 找到距离起点最近的并且未被访问过的节点
		min := INF
		minIndex := -1
		for v := 0; v < numVertices; v++ {
			if !visited[v] && distances[v] <= min {
				min = distances[v]
				minIndex = v
			}
		}
		if minIndex == -1 {
			break
		}
		// 将最近的节点设置为已访问
		visited[minIndex] = true
		for v := 0; v < numVertices; v++ {
			// 更新与选择节点相邻节点的最短距离和前驱节点
			if !visited[v] && graph[minIndex][v] != INF && distances[minIndex]+graph[minIndex][v] < distances[v] {
				distances[v] = distances[minIndex] + graph[minIndex][v]
				parents[v] = minIndex
			}
		}
	}
	// 回溯最短路径
	path := make([]int, 0)
	node := end // 终点
	for node != -1 {
		path = append(path, node)
		node = parents[node]
	}

	// 反转数组
	i, j := 0, len(path)-1
	for i < j {
		path[i], path[j] = path[j], path[i]
		i++
		j--
	}

	return distances, path
}

func main() {
	graph := [][]int{
		// A -> B -> C ->D
		{INF, 5, 2, INF}, // A
		{5, INF, 2, 3},   // B
		{2, 2, INF, 1},   // C
		{INF, 3, 1, INF}, // D
	}
	start := 0
	end := 3
	distances, path := dijkstra(graph, start, end)
	fmt.Println("distances:", distances)
	fmt.Println("parents:", path)
}