Dijkstra算法+打印路径

原理

Dijkstra 算法(中文名:迪杰斯特拉算法)是由荷兰计算机科学家 Edsger Wybe Dijkstra 提出。该算法常用于路由算法或者作为其他图算法的一个子模块。举例来说,如果图中的顶点表示城市,而边上的权重表示城市间开车行经的距离,该算法可以用来找到两个城市之间的最短路径。
设G=(V,E)是一个带权有向图,把图中顶点集合V分为两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),
第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径的的递增次序依次把第二组中的顶点加入S中。在加入的过程中,总保持从源点v到S中各个顶点的最短路径长度不大于从源点v到U中任何路径的长度。
此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前路径的最短长度。

一般用于求单源无负权最短路径

算法步骤

1)初始时,只包括源点,即S = {v},v的距离为0。U包含除v以外的其他顶点,即:U ={其余顶点},若v与U中顶点u有边,则(u,v)为正常权值,若u不是v的出边邻接点,则(u,v)权值 ∞;
2)从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
3)以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
4)重复步骤b和c直到所有顶点都包含在S中。

朴素Dijkstra(邻接矩阵)

测试常量

在这里插入图片描述

private static final int N = Integer.MAX_VALUE;   // 最大值
    private static char[] vexs = {'0', '1', '2', '3', '4', '5', '6', '7', '8'};       // 顶点集合
    private static int[][] matrix = {
            {0, 1, 5, N, N, N, N, N, N},
            {1, 0, 3, 7, 5, N, N, N, N},
            {5, 3, 0, N, 1, 7, N, N, N},
            {N, 7, N, 0, 2, N, 3, N, N},
            {N, 5, 1, 2, 0, 3, 6, 9, N},
            {N, N, 7, N, 3, 0, N, 5, N},
            {N, N, N, 3, 6, N, 0, 2, 7},
            {N, N, N, N, 9, 5, 2, 0, 4},
            {N, N, N, N, N, N, 7, 4, 0}};
朴素Dijkstra算法代码(O(n2)).

参数定义:
prev[j:经过点j
dist[j]:到j点距离

public static void dijkstra(int vs) {
        int[] prev = new int[vexs.length];
        int[] dist = new int[vexs.length];
        boolean[] visit = new boolean[vexs.length];
//初始化
        for (int i = 0; i < vexs.length; i++) {
            prev[i] = 0;
            dist[i] = matrix[vs][i];
        }

        visit[vs] = true;
        dist[vs] = 0;
        for (int i = 1; i < vexs.length; i++) {
            int min = N, k = 0;
            for (int j = 0; j < vexs.length; j++) {
                if (!visit[j] && min > dist[j]) {
                    k = j;
                    min = dist[j];
                }
            }

            if (min == N) break;
            visit[k] = true;
            for (int j = 0; j < vexs.length; j++) {
                if (matrix[k][j] == N) continue;
                if (!visit[j] && dist[j] > min + matrix[k][j]) {
                    prev[j] = k;
                    dist[j] = min + matrix[k][j];
                }
            }
        }
        // 打印dijkstra最短路径的结果
        System.out.printf("dijkstra(%c): \n", vexs[vs]);
        for (int i = 0; i < vexs.length; i++)
            System.out.printf("  shortest(%c, %c)=%d , p =%d\n", vexs[vs], vexs[i], dist[i], prev[i]);
    }

堆优化 Dijkstra(邻接表)

堆优化的主要思想就是使用一个优先队列(就是每次弹出的元素一定是整个队列中最小的元素)来代替最近距离的查找,用邻接表代替邻接矩阵,这样可以大幅度节约时间开销。

这也是一种在图论中十分常见的存图方式,与数组存储单链表的实现一致(头插法)。

这种存图方式又叫「链式前向星存图」。

适用于边数较少的「稀疏图」使用,当边数量接近点的数量,即  时,可定义为「稀疏图」。

int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
int idx;

void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = he[a];
    he[a] = idx;
    w[idx] = c;
    idx++;
}
首先 idx 是用来对边进行编号的,然后对存图用到的几个数组作简单解释:

he 数组:存储是某个节点所对应的边的集合(链表)的头结点;
e  数组:由于访问某一条边指向的节点;
ne 数组:由于是以链表的形式进行存边,该数组就是用于找到下一条边;
w  数组:用于记录某条边的权重为多少。
因此当我们想要遍历所有由 a 点发出的边时,可以使用如下方式:

for (int i = he[a]; i != -1; i = ne[i]) {
    int b = e[i], c = w[i]; // 存在由 a 指向 b 的边,权重为 c
}
    void dijkstra() {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{k, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int id = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[id]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[id] = true;
            for (int i = he[id]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[id] + w[i]) {
                    dist[j] = dist[id] + w[i];
                    q.add(new int[]{j, dist[j]});
                }
            }
        }
    }
}
打印路径
 // 打印Dijkstra最短路径的结果
        System.out.printf("bellman(%c): \n", vexs[vs]);
        for (int i = 0; i < vexs.length; i++) {
            System.out.printf("  shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
            StringBuilder sb = new StringBuilder();
            sb.append(i);
            for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
                sb.append(">--");
                sb.append(prev[j]);
            }
            System.out.println(sb.reverse());
        }

缺陷

若u→v间存在一条负权回路(负权回路含义为:如果存在一个环(从某个点出发又回到自己的路径),而且这个环上所有权值之和是负数,那这就是一个负权环,也叫负权回路),那么只要无限次地走这条负权回路,便可以无限制地减少它的最短路径权值,这就变相地说明最短路径不存在。一个不存在最短路径的图,Dijkstra 算法无法检测出这个问题,其最后求解的dist[]也是错的。

对比

Dijkstra: 不含负权。运行时间依赖于优先队列的实现,如 O((∣V∣+∣E∣)log∣V∣)
SPFA: 无限制。运行时间O(k⋅∣E∣) (k≪∣V∣)
Bellman-Ford:无限制。运行时间O(∣V∣⋅∣E∣)
ASP: 无圈。运行时间O(∣V∣+∣E∣)
Floyd-Warshall: 无限制。运行时间O(∣V∣^3)

Dijkstra:适用于权值为非负的图的单源最短路径,用斐波那契堆的复杂度O(E+VlgV)

BellmanFord:适用于权值有负值的图的单源最短路径,并且能够检测负圈,复杂度O(VE)

SPFA:适用于权值有负值,且没有负圈的图的单源最短路径,论文中的复杂度O(kE),k为每个节点进入Queue的次数,且k一般<=2,但此处的复杂度证明是有问题的,其实SPFA的最坏情况应该是O(VE).

Floyd:每对节点之间的最短路径。

这里给出结论:

(1)当权值为非负时,用Dijkstra。

(2)当权值有负值,且没有负圈,则用SPFA,SPFA能检测负圈,但是不能输出负圈。

(3)当权值有负值,而且可能存在负圈,则用BellmanFord,能够检测并输出负圈。

(4)SPFA检测负环:当存在一个点入队大于等于V次,则有负环,后面有证明。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Dijkstra算法是一种经典的单源最短路径算法,以下是C语言实现Dijkstra算法打印路径的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <limits.h> #define V 6 // 图的顶点个数 int minDistance(int dist[], int visited[]) { int min = INT_MAX, min_index; for (int v = 0; v < V; v++) { if (!visited[v] && dist[v] <= min) { min = dist[v]; min_index = v; } } return min_index; } void printPath(int parent[], int j) { if (parent[j] == -1) return; printPath(parent, parent[j]); printf("%d ", j); } void printSolution(int dist[], int parent[], int src) { printf("Vertex\t Distance\t Path\n"); for (int i = 0; i < V; i++) { if (i != src) { printf("%d -> %d\t %d\t\t %d ", src, i, dist[i], src); printPath(parent, i); printf("\n"); } } } void dijkstra(int graph[V][V], int src) { int dist[V]; int visited[V]; int parent[V]; for (int i = 0; i < V; i++) { dist[i] = INT_MAX; visited[i] = 0; parent[i] = -1; } dist[src] = 0; for (int i = 0; i < V - 1; i++) { int u = minDistance(dist, visited); visited[u] = 1; for (int v = 0; v < V; v++) { if (!visited[v] && graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) { dist[v] = dist[u] + graph[u][v]; parent[v] = u; } } } printSolution(dist, parent, src); } int main() { int graph[V][V] = {{0, 4, 0, 0, 0, 0}, {4, 0, 8, 0, 0, 0}, {0, 8, 0, 7, 0, 4}, {0, 0, 7, 0, 9, 14}, {0, 0, 0, 9, 0, 10}, {0, 0, 4, 14, 10, 0}}; dijkstra(graph, 0); return 0; } ``` 该示例代码使用邻接矩阵存储图,并以0号顶点作为源点。运行该代码,将输出每个顶点到源点的最短距离和路径

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值