一文读懂Dijkstra(迪克斯特拉)单源最短路径算法

一 概述        

在图论中,寻找从一个顶点到另一个顶点的最短路径是一个非常常见的问题。Dijkstra算法主要用于计算带权重图中单源最短路径,即找到从给定起点出发到达其他顶点的最短路径。

二 思路分析

首先不直接给出算法,先想一想如何解决这种问题,我们都知道从数组中求最大值,给定一个临时变量temp,然后遍历数组和temp比较,使得temp每次都保留最大值,最终结果便是最大值。根据这种思想,我们也可以每次遍历图的顶点的时候,都比较一下计算出的距离和当前顶点保存的距离,保存距离小的值,最终遍历完所有的顶点,便得出了从起点到其它顶点的最短路径,本质上是一种贪心算法。

三 实例分析

如图所示,如何找到从V1作为起点到其它节点的最短路径呢?

先说明一下,每个节点都有一个属性dist代表最短路径, 每条边都有权重weight代表距离

第一步:初始化,设定起始节点的最短路径dist为0,其余节点的最短路径dist为无穷大

第二步:从未被访问过的节点中选出距离最小的一个节点,这里是V1,检查这个节点能直接到达的所有邻居节点,可以看到是V6,V2,V3。

第三步,遍历的所有邻居,计算邻居的距离(dist+weight),即当前节点的最短路径+边的权重(如果计算的距离小于邻居本身的距离,则更新),可以看出距离最短的是V2。

V60+14=14
V20+7=7
V30+9=9

第四步:把V2当做选中的节点。V6和V3作为未处理的节点,V1则作为已处理过的节点, 继续这个步骤

划重点:这三个概念很重要,选中的节点就是当前dist最小的节点(目前是V2,因为V1已经处理过,不包含V1)未处理的节点就是已经计算出dist,但是不是目前的最短路径的节点(V6,V3),已处理过的节点就是曾经成为最短路径的节点(V1)

(1)接下来计算V2邻居的距离:

V47+15=22

此时未处理的节点,最短路径的节点是V3

V614
V422
V39

(2)继续计算V3的邻居V2和V4,由于V2是被处理过的节点,它的距离是7,我们这里其实没有必要再计算V2的距离了,因为被处理过的节点作为曾经的最短路径,肯定比现在计算的值要小(不考虑负权重的情况),因此这里只计算V4的距离(代码实现的时候直接会跳过V2)

V49+11=20

由于20小于22,更新V4的距离为20,此时未处理的节点

V614
V420

(3)选择V6作为选中的节点,最终计算出V5的最短路径是14+9=23

(4)继续计算其它节点的最短路径,直到没有未处理节点

四 步骤

好了思想有了,接下来想一想步骤,步骤对于写算法很重要,思路不清晰,代码大概率也是有问题的,最好写代码时每次都把步骤列出来,闲话少说,上步骤

1 初始化

设定起始节点的最短路径为0,其余节点的最短路径为无穷大

2 选择

未处理节点中挑选出具有最小临时距离值的节点

3 更新

对于当前节点u的每一个邻居节点v,检查通过u到达v的距离(即dist[u] + weight(u, v),其中weight(u, v)是边uv的权重)是否比已知的dist[v]小。如果更小,则更新dist[v]为新的更低的距离值

4 重复

重复步骤2和3,当没有剩余未处理的节点时,算法结束

五 代码

接下来上代码,先看下类的数据结构



/**
 * 节点
 */
public class Vertex {
    public String name;
    /**
     * 节点的最短路径,初始化为整数最大值
     */
    public int dist = Integer.MAX_VALUE;
    /**
     * 标记节点为是否已访问过,避免重复处理
     */
    public boolean visited;
    public List<Edge> edges = new ArrayList<>();

    public Vertex(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Vertex{" +
                "name='" + name + '\'' +
                '}';
    }
}




/**
 * 边
 */
public class Edge {
    public Vertex linked;
    public int weight;

    public Edge(Vertex linked, int weight) {
        this.linked = linked;
        this.weight = weight;
    }
}


/**
 * 图
 */
public class Graph {

    public final Map<String, Vertex> vertices = new HashMap<>();

    /**
     * 将指定名称的节点放入map
     * @param name 节点名称
     * @return 存放的节点
     */
    public Vertex getOrCreateVertex(String name) {
        return vertices.computeIfAbsent(name, Vertex::new);
    }

    /**
     * 添加节点和边
     * @param source 源节点
     * @param target 目标节点
     * @param weight 边的权重
     */
    public void addEdge(String source, String target, Integer weight) {
        Vertex sourceVertex = getOrCreateVertex(source);
        Vertex targetVertex = getOrCreateVertex(target);
        sourceVertex.edges.add(new Edge(targetVertex, weight));
    }
}

接下来是算法的具体实现


public class Dijkstra {

    public static void main(String[] args) {
        Graph graph = new Graph();
        graph.addEdge("v1", "v3", 9);
        graph.addEdge("v1", "v2", 7);
        graph.addEdge("v1", "v6", 14);
        graph.addEdge("v2", "v4", 15);
        graph.addEdge("v3", "v4", 11);
        graph.addEdge("v3", "v2", 2);
        graph.addEdge("v4", "v5", 6);
        graph.addEdge("v6", "v5", 9);

        Vertex source = graph.getOrCreateVertex("v1");
        minDist(source);
        Vertex target = graph.getOrCreateVertex("v5");
        System.out.println(target.dist);
    }

    public static void minDist(Vertex start) {
        start.dist = 0;
        // 使用小顶堆存放未处理的节点,提高查找最短路径的性能
        PriorityQueue<Vertex> queue = new PriorityQueue<>(Comparator.comparing(o1 -> o1.dist));
        queue.add(start);
        while (!queue.isEmpty()) {
            // 最短路径的节点
            Vertex current = queue.poll();
            // 如果已经处理过,则跳过
            if (current.visited) {
                continue;
            }
            // 标记为已处理
            current.visited = true;
            // 遍历邻居
            for (Edge edge : current.edges) {
                Vertex neighbor = edge.linked;
                // 计算邻居的距离
                int potentialDist = current.dist + edge.weight;
                // 判断是否更新邻居的最短路径
                if (potentialDist < neighbor.dist) {
                    neighbor.dist = potentialDist;
                    // 如果更新了邻居的最短路径,说明邻居可以作为未处理的节点加入堆
                    queue.add(edge.linked);
                }
            }
        }

    }
}

六 改进

上面只是一个简易版的算法,我们还可以给节点类加上previous信息,每次更新邻居距离时,都记录一下previous,这样可以记录最短路径的具体节点都有哪些。

当然,大家工作中不必自己实现图的数据结构,可以使用第三方的库,比如jgrapht

<dependency>
    <groupId>org.jgrapht</groupId>
    <artifactId>jgrapht-core</artifactId>
    <version>1.2.0</version>
</dependency>

下面是使用类库提供的算法,效果是一样的


import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;


public class Test {
    public static void main(String[] args) {
        SimpleDirectedWeightedGraph<String, DefaultWeightedEdge> graph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);

        // 添加顶点
        String v1 = "v1";
        String v2 = "v2";
        String v3 = "v3";
        String v4 = "v4";
        String v5 = "v5";
        String v6 = "v6";
        graph.addVertex(v1);
        graph.addVertex(v2);
        graph.addVertex(v3);
        graph.addVertex(v4);
        graph.addVertex(v5);
        graph.addVertex(v6);

        // 添加边
        DefaultWeightedEdge edge12 = graph.addEdge(v1, v2);
        graph.setEdgeWeight(edge12, 7.0);

        DefaultWeightedEdge edge16 = graph.addEdge(v1, v6);
        graph.setEdgeWeight(edge16, 14.0);

        DefaultWeightedEdge edge13 = graph.addEdge(v1, v3);
        graph.setEdgeWeight(edge13, 9.0);

        DefaultWeightedEdge edge24 = graph.addEdge(v2, v4);
        graph.setEdgeWeight(edge24, 15.0);

        DefaultWeightedEdge edge34 = graph.addEdge(v3, v4);
        graph.setEdgeWeight(edge34, 11.0);

        DefaultWeightedEdge edge23 = graph.addEdge(v2, v3);
        graph.setEdgeWeight(edge23, 2.0);

        DefaultWeightedEdge edge45 = graph.addEdge(v4, v5);
        graph.setEdgeWeight(edge45, 6.0);

        DefaultWeightedEdge edge65 = graph.addEdge(v6, v5);
        graph.setEdgeWeight(edge65, 9.0);

        // 使用 Dijkstra 算法计算最短路径
        DijkstraShortestPath<String, DefaultWeightedEdge> dijkstraAlg = new DijkstraShortestPath<>(graph);
        double distance = dijkstraAlg.getPath(v1, v5).getWeight();
        System.out.println("Shortest path from v1 to v5 has a weight of " + distance);

    }
}

七 思考

最后对于负权重的情况,Dijkstra算法无能为力,大家可以思考一下原因(已经黄色加粗给过提示了),以及如何解决,尝试自己思考出如何实现贝尔曼-福特算法(Bellman-Ford)

<think>好的,我现在要处理用户关于Dijkstra算法的问题。用户想了解这个算法用于计算最短路径的相关信息,包括算法本身、图论中的实现等。首先,我需要回忆一下Dijkstra算法的基本概念和步骤,确保自己理解正确。 Dijkstra算法是由Edsger Dijkstra提出的,用于解决带权有向图的单源最短路径问题。它的核心思想是贪心算法,每次选择距离源点最近的节点,并更新其邻接节点的最短距离。这个算法适用于边权非负的情况,所以如果有负权边,可能需要用其他算法比如Bellman-Ford。 接下来,我需要整理算法的步骤。根据已有的知识,算法通常需要一个距离数组dist,记录源点到各节点的当前最短距离,还有一个visited数组标记节点是否已经确定最短路径。初始化时,源点的距离设为0,其他设为无穷大。然后循环选择未访问节点中距离最小的,更新其邻居的距离,直到所有节点都被访问。 用户提供的引用中,引用[3]提到维护dist和visited数组,引用[1]和[2]说明这是单源最短路径的解决方案,而引用[4]提到Python实现的代码。可能需要参考这些内容来构建回答的结构。 然后,我需要考虑如何将步骤转化为清晰的文字描述,并给出一个具体的例子帮助理解。例如,用一个简单的图来演示算法执行过程,分步骤说明每个节点的处理顺序和距离的更新情况。 实现部分,用户可能希望看到代码示例。根据引用[4],用户提到Python实现的源码,但原引用中的代码可能没有给出。我需要回忆或查找一个典型的Dijkstra算法Python实现,确保使用优先队列来提高效率,因为传统的数组实现在时间复杂度上较高。需要注意的是,代码要正确,并符合用户提供的格式要求,比如使用$...$和$$...$$的数学表达式,以及正确的中文注释。 此外,用户可能关心算法的时间复杂度和优化方法。比如,使用优先队列可以将复杂度从O(V^2)降低到O(E + V log V),其中V是顶点数,E是边数。这里需要明确说明不同实现方式的区别。 还要注意用户提到的系统级指令,比如所有数学表达式用行内格式,独立公式单独成段,中文回答,生成相关问题。检查是否有遗漏的格式要求,比如代码块的正确使用,引用标识的位置是否正确。 最后,生成相关问题需要考虑用户可能的进一步疑问,比如算法适用条件、与其他算法的比较(如Floyd算法)、处理负权边的情况,或者实际应用场景如路由协议、地图导航等。 总结一下,回答的结构应该包括:算法概述、核心思想、详细步骤、具体例子、代码实现、时间复杂度分析、适用条件,最后是相关问题。确保各部分引用正确的引用来源,如步骤部分引用[3],实现代码引用[4],单源最短路径的问题引用[1][2]等。同时,避免使用Markdown格式,保持自然的中文表达,并检查LaTeX语法是否正确。</think>### Dijkstra算法详解 #### 一、算法概述 Dijkstra算法用于求解**带权有向图或无向图**中**单源最短路径**问题,要求所有边权非负[^1]。其核心思想是**贪心策略**,通过逐步扩展已确定最短路径的顶点集,动态更新其他顶点的最短距离[^3]。 #### 二、核心思想与步骤 1. **初始化**: - 定义距离数组$dist[]$,记录源点到各顶点的当前最短距离,初始时$dist[源点] = 0$,其余为$\infty$。 - 定义集合$visited[]$标记顶点是否已确定最短路径。 2. **迭代过程**: - 从$dist[]$中选择未访问的**最小距离顶点**$u$,标记$u$为已访问。 - 遍历$u$的所有邻接顶点$v$,若$dist[v] > dist[u] + w(u,v)$($w$为边的权重),则更新$dist[v]$[^3]。 3. **终止条件**: - 重复上述步骤直到所有顶点被访问。 #### 三、实例演示(以图1为例) 假设源点为A,邻接关系如下: ``` A→B(6), A→C(2) B→D(1) C→B(3), C→D(5) ``` 执行过程: 1. 初始$dist = [A:0, B:∞, C:∞, D:∞]$ 2. 选择A→C(距离2),更新B的距离为$2+3=5$,D为$2+5=7$ 3. 选择C→B(距离5),更新D为$5+1=6$ 最终最短路径:$A→C→B→D$,总权重6。 #### 四、代码实现(Python) ```python import heapq def dijkstra(graph, start): dist = {node: float('infinity') for node in graph} dist[start] = 0 heap = [(0, start)] visited = set() while heap: current_dist, u = heapq.heappop(heap) if u in visited: continue visited.add(u) for v, w in graph[u].items(): if dist[v] > current_dist + w: dist[v] = current_dist + w heapq.heappush(heap, (dist[v], v)) return dist ``` *说明:使用优先队列优化时间复杂度至$O(E + V \log V)$,适用于稀疏图[^4]。* #### 五、关键特性 1. **时间复杂度**: - 基础实现(数组遍历找最小值):$O(V^2)$ - 优先队列优化:$O(E + V \log V)$ 2. **适用条件**:边权必须非负,否则可能无法得到正确结果。 #### 六、与其他算法对比 | 算法 | 类型 | 时间复杂度 | 适用场景 | |-------------|------------|--------------|------------------------| | Dijkstra | 单源 | $O(E+V\log V)$| 无负权边 | | Floyd | 多源 | $O(V^3)$ | 任意权(可含负权) | | Bellman-Ford| 单源 | $O(VE)$ | 含负权边检测负环 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不良人-小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值