单源最短路径——迪杰斯特拉(Dijkstra)算法 C++实现

本文深入讲解了Dijkstra算法,一种用于解决单源最短路径问题的经典算法。文章详细阐述了算法的基本思想、实现过程及复杂度分析,同时提供了C++代码示例。此外,还探讨了算法与Prim算法的联系,以及如何应对边权为负数的情况。

求最短路径之Dijkstra算法

Dijkstra算法是用来求单源最短路径问题,即给定图G和起点s,通过算法得到s到达其他每个顶点的最短距离。

基本思想:对图G(V,E)设置集合S,存放已被访问的顶点,然后每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令u为中介点,优化起点s与所有从u能够到达的顶点v之间的最短距离。这样的操作执行n次(n为顶点个数),直到集合S已经包含所有顶点。
 

由于图可以使用邻接矩阵或者邻接表来实现,因此会有两种写法。以下图为例来具体实现代码:

 

代码:

main.cpp

#include <iostream>
#include <vector>
using namespace std;

const int INF = 1e9; // int范围约为 (-2.15e9, 2.15e9)

/*Dijkstra算法解决的是单源最短路径问题,即给定图G(V,E)和起点s(起点又称为源点),边的权值为非负,
求从起点s到达其它顶点的最短距离,并将最短距离存储在矩阵d中*/
void Dijkstra(int n, int s, vector<vector<int>> G, vector<bool> &vis, vector<int> &d, vector<int> &pre)
{
    /*
     *   n:        顶点个数
     *   s:        源点
     *   G:        图的邻接矩阵
     * vis:        标记顶点是否已被访问
     *   d:        存储源点s到达其它顶点的最短距离
     * pre:        最短路径中v的前驱结点
     */

    // 初始化
    fill(vis.begin(), vis.end(), false);
    fill(d.begin(), d.end(), INF);
    d[s] = 0;
    for (int i = 0; i < n; ++i)
    {
        pre[i] = i;
    }

    // n次循环,确定d[n]数组
    for (int i = 0; i < n; ++i)
    {
        // 找到距离s最近的点u,和最短距离d[u]
        int u = -1;
        int MIN = INF;
        for (int j = 0; j < n; ++j)
        {
            if (!vis[j] && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }

        // 找不到小于INF的d[u],说明剩下的顶点与起点s不连通
        if (u == -1)
        {
            return;
        }

        vis[u] = true;
        for (int v = 0; v < n; ++v)
        {
            // 遍历所有顶点,如果v未被访问 && 可以达到v && 以u为中介点使d[v]更小
            if (!vis[v] && G[u][v] != INF && d[u] + G[u][v] < d[v])
            {
                d[v] = d[u] + G[u][v];   // 更新d[v]
                pre[v] = u;              // 记录v的前驱顶点为u(新添加)
            }
        }
    }
}

// 输出从起点s到顶点v的最短路径
void DFSPrint(int s, int v, vector<int> pre)
{
    if (v == s)
    {
        cout << s << " ";
        return;
    }
    DFSPrint(s, pre[v], pre);
    cout << v << " ";
}


int main()
{
    int n = 6;
    /*邻接矩阵*/
    vector<vector<int>> G = {{  0,  4,INF,INF,  1,  2},
                             {  4,  0,  6,INF,INF,  3},
                             {INF,  6,  0,  6,INF,  5},
                             {INF,INF,  6,  0,  4,  5},
                             {  1,INF,INF,  4,  0,  3},
                             {  2,  3,  5,  5,  3,  0}};
    vector<bool> vis(n);
    vector<int> d(n);
    vector<int> pre(n);

    Dijkstra(n, 0, G, vis, d, pre);

    for (size_t i = 0; i < d.size(); ++i)
    {
        cout << "the shortest path " << i << " is: " << d[i] << endl;
    }
    cout << endl;

    // v = 2: 0->5->2  cost = 2 + 5 = 7
    // v = 3: 0->4->3  cost = 1 + 4 = 5
    int v = 2;
    DFSPrint(0, v, pre);
    cout << endl << "cost = " << d[v] << endl;

    return 0;
}

运行结果:

 

复杂度分析:

主要是外层的循环O(V)(V就是顶点个数n)与内层循环(寻找最小的d[u]需要O(V)、枚举需要O(V)产生的),总的时间复杂度为O(V*(V+V))=O(V^2)

 

Dijkstra算法与Prim算法的联系:

前者每次寻找与最近的结点

后者每次寻找与最近的结点

 

总结:

Dijkstra算法只能应对所有边权都是非负数的情况,如果边权出现负数,那么Dijkstra算法很可能会出错,这是最好使用SPFA算法。

上面的做法复杂度为O(V^2)级别,其中由于必须把每个顶点都标记已访问,因此外层循环的O(V)时间是无法避免的,但是寻找最小d[u]的过程却可以不必达到O(V)的复杂度,而可以使用对优化来降低复杂度。最简单的写法是直接使用STL中的优先队列priority_queue,这样使用邻接表实现Dijkstra算法的时间复杂度可以降低为O(VlogV+E)。


如果题目给出的是无向边(即双向边)而不是有向边,又该如何解决呢?其实很简单,只需要把无向边当成两条指向相反的有向边即可。对邻接矩阵来说,一条u与v之间的无向边在输入时可以分别对G[u][v]和G[v][u]赋以相同的边权;而对于邻接表来说,只需要在u的邻接表Adj[u]末尾添加上v,并在v的邻接表Adj[v]末尾添加上u即可。
 

(4)、Dijkstra算法求解实际问题
之前讲的是最基本的Dijkstra算法,那么平时考试笔试等遇到的题目肯定不会这么“裸”,更多时候会出现这样一种情况,即从起点到终点的最短距离最小的路径不止一条。
那么碰到这种两条以上可以达到最短距离的路径,题目就会给出一个第二标尺(第一标尺是距离),要求在所有最短路径中选择第二标尺最优的一条路径,而第二标尺常见的是以下三种出题方法或者其组合:
给每条边在增加一个边权(比如说花费),然后要求在最短路径有多条时要求路径上的花费之和最小(当然如果边权是其它含义,也可以是最大)
给每个点增加一个点权(例如每个城市能收集到的物资),然后在最短路径有多条时要求路径上的点权之和最大(当然如果是其它含义,也可以是最小)
直接问有多少条最短路径

解决思路:都只需要增加一个数组来存放新增的边权或点权或最短路径条数,然后在Dijkstra算法中修改优化d[v]的那个步骤即可,其它部分不需要改动。
如下:
新增边权。以新增的边权代表花费为例,用cost[u][v]表示u->v的花费(由题目输入),并增加一个数组c[],令从起点s到达顶点u的最少花费为c[u],初始化时只有c[s]=0,其余均为INF(一个很大的值),这样就可以在更新d[v]时更新c[v]. 代码如下:
 


for(int v=0; v<n; v++)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               c[v] = c[u] + cost[u][v]; 
          }
          else if(d[u]+G[u][v] == d[v] && c[u]+cost[u][v] < c[v])
               c[v]=c[u]+cost[u][v];          //最短距离相等时看能都使c[v]更优
     }

 

新增点权。以新增的点权代表城市中能收集到的物资为例,用weight[u]表示城市u中的物资数目(由题目输入),并增加一个数组w[],令起点s到达顶点u可以收集到的最大物资为w[u],初始化时只有w[s]为weight[s],其余均为0,这样就可以在更新d[v]时更新w[v].代码如下:
 


for(int v=0; v<n; ++v)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               w[v] = w[u]+weight[v]; 
          }
          else if(d[u]+G[u][v] == d[v] && w[u]+weight[v]>w[v])
               w[v] = w[u]+weight[v];
     }
}

求最短路径条数。只需要添加一个数组num[],令从起点s到达顶点u的最短路径条数为num[u],初始化时只有num[s]=1,其余均为0,这样就可以在更新d[v]时让num[v]=num[u],而当d[u]+G[u][v] =d[v]时,让num[v]+=num[u].代码如下:


for(int v=0; v<n; ++v)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               num[v]=num[u];
          }
          else if(d[u]+G[u][v] == d[v])
               num[v]+=num[u];
     }
}

若需要将多条最短路径打印出来,则需要将记录前驱结点的数组int pre[n]改为二维数组vector<vector<int>> pre(n, vector<int>());

并在查找到相同路径时,采用push_back()同时保存多个前驱结点,而在找到更短路径时,需要clear()清空之前所保持的前驱结点,并再保存当前最短路径下的前驱结点,在打印路径时同样采用DFS即可,保存路径部分代码如下:


for(int v=0; v<n; ++v)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               num[v]=num[u];
        
               // 清空前驱结点,并只保留当前这一个前驱
               pre[v].clear();
               pre[v].push_back(u);
          }
          else if(d[u]+G[u][v] == d[v])
               num[v]+=num[u];
        
               // 同时保留多个前驱结点
               pre[v].push_back(u);
     }
}

参考资料:

https://blog.youkuaiyun.com/YF_Li123/article/details/74090301

普林斯顿算法公开课:Algorithms - Robert Sedgewick, Kevin Wayne

 

 

<think>我们被要求使用C语言实现Dijkstra算法求解单源最短路径问题。 根据引用[1]:Dijkstra算法是一种贪心算法,用于在带权图中从一个源点找到所有其他顶点的最短路径。它要求图中边的权重非负。 引用[2]描述了Dijkstra算法类似于Prim算法,它维护两个集合:一个是最短路径树(SPT)的顶点集合,另一个是尚未包含在SPT中的顶点集合。在每一步中,从尚未包含的顶点集合中选取一个距离源点最近的顶点加入SPT。 引用[3]提到了Dijkstra算法的详细步骤。 引用[4]说明Dijkstra算法适用于有向图和无向图,但要求边权非负。 引用[5]给出了Dijkstra算法的伪代码。 我们将按照以下步骤实现: 1. 初始化:将源点的距离设为0,其他所有顶点的距离设为无穷大(用一个大数表示),并将所有顶点标记为未访问。 2. 使用一个优先队列(最小堆)来高效地选取当前距离最小的顶点。但C标准库中没有优先队列,因此我们可以使用数组来存储,每次遍历数组来选取最小距离的顶点(适用于顶点数不多的情况)。 3. 对于当前处理的顶点u,遍历其所有邻接顶点v,如果通过u到v的路径比当前已知的v的距离更短,则更新v的距离(松弛操作)。 4. 重复上述过程,直到所有顶点都被访问。 由于C语言没有内置的优先队列,我们将使用数组实现。如果顶点数很大,效率可能较低(O(V^2)),但这是最基础的实现方式。 假设图的表示:使用邻接矩阵。顶点数量为V,源点为s。 步骤: 1. 创建大小为V的数组dist,存储从源点到每个顶点的最短距离。 2. 创建大小为V的数组visited,标记顶点是否已处理。 3. 初始化dist:dist[s] = 0,其他为无穷大(例如INT_MAX)。 4. 初始化visited:全部为false(0)。 5. 循环V次: a. 从未处理的顶点中选取dist最小的顶点u(如果使用数组遍历,复杂度为O(V))。 b. 标记u为已访问。 c. 遍历u的所有邻接点v(即遍历所有顶点,检查邻接矩阵中是否有边): - 如果存在边(graph[u][v]!=0且graph[u][v]不为无穷)且visited[v]为false,并且dist[u] + graph[u][v] < dist[v],则更新dist[v] = dist[u] + graph[u][v]。 6. 输出dist数组,即源点到各顶点的最短距离。 注意:如果图是稀疏图,使用邻接表会更高效,但这里我们先使用邻接矩阵。 下面给出C语言代码实现: 注意:我们假设图中没有负权边,且图是连通的(或者非连通则不可达的顶点距离保持无穷大)。 代码实现: */ #include <stdio.h> #include <limits.h> // 定义顶点个数 #define V 9 // 辅助函数:找到当前未处理顶点中距离最小的顶点 int minDistance(int dist[], int visited[]) { int min = INT_MAX, min_index; for (int v = 0; v < V; v++) { if (visited[v] == 0 && dist[v] <= min) { min = dist[v]; min_index = v; } } return min_index; } // 打印最短距离 void printSolution(int dist[]) { printf("Vertex \t Distance from Source\n"); for (int i = 0; i < V; i++) printf("%d \t\t %d\n", i, dist[i]); } // Dijkstra算法实现(邻接矩阵表示) void dijkstra(int graph[V][V], int src) { int dist[V]; // 存储最短距离 int visited[V]; // 记录顶点是否已处理 // 初始化所有距离为无穷大,visited为0(未访问) for (int i = 0; i < V; i++) { dist[i] = INT_MAX; visited[i] = 0; } // 源点到自己的距离为0 dist[src] = 0; // 循环V-1次,因为源点已经处理,剩下V-1个顶点 for (int count = 0; count < V - 1; count++) { // 选取未处理顶点中最小距离的顶点 int u = minDistance(dist, visited); visited[u] = 1; // 标记为已处理 // 更新u的所有邻接点的距离 for (int v = 0; v < V; v++) { // 如果v未被访问,且u到v有边(graph[u][v]非0),且从源点到u的距离不是无穷大(否则相加会溢出), // 并且通过u到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]; } } } // 打印最短距离 printSolution(dist); } // 测试代码 int main() { // 用邻接矩阵表示图,0表示没有直接边 int graph[V][V] = { {0, 4, 0, 0, 0, 0, 0, 8, 0}, {4, 0, 8, 0, 0, 0, 0, 11, 0}, {0, 8, 0, 7, 0, 4, 0, 0, 2}, {0, 0, 7, 0, 9, 14, 0, 0, 0}, {0, 0, 0, 9, 0, 10, 0, 0, 0}, {0, 0, 4, 14, 10, 0, 2, 0, 0}, {0, 0, 0, 0, 0, 2, 0, 1, 6}, {8, 11, 0, 0, 0, 0, 1, 0, 7}, {0, 0, 2, 0, 0, 0, 6, 7, 0} }; dijkstra(graph, 0); return 0; } /* 说明: 1. 该实现使用邻接矩阵,因此空间复杂度为$O(V^2)$,时间复杂度为$O(V^2)$(因为每次选取最小距离需要$O(V)$,总共$V$次,而更新邻接点需要遍历所有顶点,也是$O(V)$,所以总时间$O(V^2)$)。 2. 对于稀疏图,可以使用邻接表配合优先队列(最小堆)来优化,选取最小距离的操作可以降到$O(\log V)$,总时间复杂度为$O((V+E)\log V)$。但在C语言中,需要自己实现堆,这里为了简单,使用数组实现。 注意:如果图中存在负权边,Dijkstra算法无法正确工作,需要使用Bellman-Ford算法。 相关问题: 1. 如何修改上述代码以支持有向图? 2. 如果图是稀疏图,如何优化Dijkstra算法的时间复杂度? 3. 如何输出最短路径(而不仅仅是距离)? 4. 如果图中存在负权边,应该使用什么算法? 5. Dijkstra算法和Bellman-Ford算法有什么区别? 引用说明: [^1]: 引用1说明Dijkstra算法使用优先队列实现高效。 [^2]: 引用2描述了Dijkstra算法维护两个集合。 [^3]: 引用3提到Dijkstra算法的详细步骤。 [^4]: 引用4说明Dijkstra算法适用于非负权图。 [^5]: 引用5给出了Dijkstra算法的伪代码。 */</think>### Dijkstra算法C语言实现 以下是使用C语言实现Dijkstra算法求解单源最短路径的完整代码: ```c #include <stdio.h> #include <limits.h> #include <stdbool.h> #define V 6 // 顶点数量 // 找到当前未处理顶点中距离最小的顶点 int minDistance(int dist[], bool 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 printSolution(int dist[]) { printf("Vertex \t Distance from Source\n"); for (int i = 0; i < V; i++) printf("%d \t\t %d\n", i, dist[i]); } // Dijkstra算法实现 void dijkstra(int graph[V][V], int src) { int dist[V]; // 存储最短距离 bool visited[V]; // 记录顶点是否已处理 // 初始化所有距离为无穷大,visited为false for (int i = 0; i < V; i++) { dist[i] = INT_MAX; visited[i] = false; } dist[src] = 0; // 源点到自身的距离为0 // 循环处理所有顶点 for (int count = 0; count < V - 1; count++) { // 选取未处理顶点中最小距离的顶点 int u = minDistance(dist, visited); visited[u] = true; // 标记为已处理 // 更新u的所有邻接点的距离 for (int v = 0; v < V; v++) { // 如果v未被访问,且u到v有边,且从源点到u的距离不是无穷大 // 且通过u到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]; } } } printSolution(dist); } int main() { // 图的邻接矩阵表示(0表示没有直接边) int graph[V][V] = { {0, 2, 0, 0, 0, 5}, {2, 0, 4, 1, 0, 0}, {0, 4, 0, 3, 6, 0}, {0, 1, 3, 0, 2, 0}, {0, 0, 6, 2, 0, 3}, {5, 0, 0, 0, 3, 0} }; dijkstra(graph, 0); // 从顶点0开始计算最短路径 return 0; } ``` ### 代码说明: 1. **数据结构**:使用邻接矩阵`graph[V][V]`表示图,其中`V`是顶点数 2. **核心变量**: - `dist[]`:存储从源点到各顶点的最短距离 - `visited[]`:标记顶点是否已处理 3. **算法流程**: - 初始化所有距离为无穷大(`INT_MAX`),源点距离为0 - 循环选择未访问顶点中距离最小的顶点`u` - 更新`u`的所有邻接点`v`的距离:$dist[v] = min(dist[v], dist[u] + graph[u][v])$ - 重复直到所有顶点被处理 ### 算法特点: 1. **时间复杂度**:$O(V^2)$(使用邻接矩阵) 2. **空间复杂度**:$O(V)$ 3. **适用条件**:仅适用于非负权重的图[^4] ### 示例输出: ``` Vertex Distance from Source 0 0 1 2 2 6 3 3 4 5 5 5 ``` ### 优化方向: 1. **优先队列优化**:使用最小堆可将时间复杂度优化到$O((V+E)\log V)$[^1] 2. **邻接表存储**:对稀疏图更高效 3. **路径记录**:添加`parent[]`数组可回溯完整路径
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值