Dijkstra

最短路径问题 : 给定一个带权有向图 G = (V, E, W),同时给定一个源点 u (u ∈ V),我们要找出从源点 u 出发到其它各点的最短路径距离,并得出这些最短路径的具体路径有哪些边构成。

其实我们要求的就是从 源点 u 出发到 其它各点 str的最短路径所组成的路线网络,也就是一个 最短路径树

最短路径问题 : 给定一个带权有向图 G = (V, E, W),同时给定一个源点 u (u ∈ V),我们要找出从源点 u 出发到其它各点的最短路径距离,并得出这些最短路径的具体路径有哪些边构成。

我们以下面这个带权有向图为示例

我们若以 A 为源点,得到如下的最短路径

我们可以把源点到各点最短路径用绿色标记一下

我们可以看出所有的最短路径构成了一个最短路径树

我们要求的从 源点 到 其它各点 的最短路径所组成的路线网络,就是这个最短路径树。

在上面的图中,我们不难发现,当我们确定了源点 u 到某个其它的点 v 的最短路径时,在这个最短路径的具体路线中,若有一个中转点 t,那么在这个最短路径中从源点 ut 的路径也一定是 ut最短路径(之一)。也就是说,假设源点 uv 的最短路径为 p,那么p任意的前缀路径 q 一定是最优的(最短路径之一)。如果 q 不是最优的,那么就会存在另一个更短的路径比 p 更短。

这个性质还是很重要的,是解决单源最短路径问题的核心

我们画个图来理解一下

歧义性

在上面的阐述中也稍微提到一点,就是最短路径其实不一定是唯一的,有可能存在两个路径,它们的路径距离一样且都是最短的,那么此时我们二选其一就可以啦。还有一个问题就是,我们的边权都应当是正数,如果边权存在非正数,那么我们是无法定义这个图中的最短路径的(距离确实不能是非正数呀,除了自己到自己)。

无环性

这个性质其实很好理解,既然我们得到的所有最短路径构成的是一个 最短路径树,那么作为一个树,它必不会存在环。也可以由之前的 单调性 得出这个性质。


Dijkstra 算法是由荷兰计算机科学家 Edsger Wybe Dijkstra 在1956年提出的,一般解决的是 带权有向图单源最短路径问题

接下来介绍如何用 Dijkstra 算法求解 单源最短路径问题

Dijkstra 算法将会充分利用 最短路径树单调性 这一性质。先定下源点 u,然后采用 贪心 的策略,不断去访问与源点 u 相接且之前未被访问过的最近的顶点 v(这句话里相接的意思是指可以从 u 到达 v),使得当前的最短路径树得到扩充,一直到所有顶点都在当前的最短路径树中,那么就得到了源点 u 到其他所有顶点 v 的最短路径。

我们将当前最短路径树所有的顶点所构成的集合称为 集合S,而不在当前最短路径树中的顶点所构成的集合称为集合V-S

1、首先需要定义一个辅助数组 flag[],用于标记每个顶点是否处于当前的 最短路径树 中,后续我们将 最短路径树 称为 集合S。在初始情况下,我们会先将源点 u 划入 集合S;

2、然后我们需要再定义一个数组 dist[],用于记录当前从源点 uv (v∈V-S)的最短路径距离,比如dist[vi]就表示 uvi 的当前最短路径距离。

集合S每一次扩充都需要选择当前不在集合S中且到源点 u 最短距离的顶点 t 作为扩充点,并且将其划入集合S。之后的扩充操作中,就以这个 t 作为中转点对 dist[v] 进行更新,使其记录的距离减小。在不断扩充集合S的过程中,dist[v]的记录的距离大小不断减小(可能不变),直到最后,其记录的便是整个图中uv 的最短的距离;

另外,一开始我们要先初始化源点 u 到其邻接的顶点的距离。

3、为了还原具体路径,我们还需要一个辅助数组 pre[],用于记录最短路径中每个顶点的前驱顶点。比如 pre[v],其记录的是 uv 的最短路径中,顶点 v 的前驱顶点。在不断扩充集合S的过程中,如果可以借助当前的扩充点 t 到达 v 的距离更短,我们也要更新 v 的前驱为 t,即 pre[v] = t

同样的,我们也要初始化源点 u 为其每个邻接顶点的前驱。

(2)

(3)

(4)

(5)

(6)

(7)


以下程序是基于 图的邻接矩阵 实现的

//距离记录数组 , 前驱数组
int dist[MAX], pre[MAX];
//集合S标记数组。如果flag[i]=true,说明该顶点i已经加入到集合S(最短路径集合);否则i属于集合V-S
bool flag[MAX]; void Dijkstra(Graph *G, int u){
for(int v = 0; v < G->nodenums; v++){
dist[v] = G->edge[u][v]; //初始化源点u到各邻接点v的距离
flag[v] = false;
if(dist[v] != INF)
pre[v] = u; //若有邻接边,顶点v有前驱顶点u
else
pre[v] = -1; //若没有,先初始化为-1
}
flag[u] = true; //初始化集合S,只有一个元素: 源点u
dist[u] = 0; //初始化源点u到自己的最短路径为0 for(int i = 0; i < G->nodenums; i++){
int tmp = INF, t = u;
/* 在集合V-S中寻找距离源点u最近的顶点t,使当前最短路径树最优 */
for(int v = 0; v < G->nodenums; v++){
if(!flag[v] && dist[v] < tmp){
//不在集合S中 并且 更小距离
t = v;
//记录在V-S中距离源点u最近的顶点v
tmp = dist[v];
}
} if(t == u)
return; //未找到直接终止
flag[t] = true; //否则, 将t加入集合S /* 更新集合V-S中与t邻接的顶点到u的距离,扩展当前最短路径树 */
for(int v = 0; v < G->nodenums; v++){
//不在集合S中 且 有边
if(!flag[v] && G->edge[t][v] != INF){
if(dist[v] > dist[t] + G->edge[t][v]){
//源点u可以借助t到达v的距离更短
dist[v] = dist[t] + G->edge[t][v];
pre[v] = t;
}
}
}
}
}

还原具体路径代码

我使用了 C++ 自带的 栈 stack,来实现最短路径具体路径的还原。因为记录的是每个顶点的前驱,所以恰好可以利用 栈 stack 的先进后出的性质。

//还原源点u到各点具体路径
void ShowShortPath(Graph G, int u){
for(int v = 0; v < G.nodenums; v++){
if(dist[v] == INF || dist[v] == 0)
continue;
cout<<"\n点"<<G.apex[u]<<" 到 点"<<G.apex[v]<<" 的最短路径距离为: "<<dist[v]<<endl;
cout<<"点"<<G.apex[v]<<"的前驱顶点为: 点"<<G.apex[pre[v]]<<endl;
cout<<"具体路径为: "<<endl; int t = pre[v]; //终点的前驱下标
//用栈存储终点前驱们 一直到 源点
stack<int> st;
while(t != u){
st.push(t);
t = pre[st.top()];
} cout<<G.apex[u]; //源点
while(!st.empty()){
t = st.top();
cout<<" --> "<<G.apex[t]; //中间点
st.pop();
}
cout<<" --> "<<G.apex[v]<<endl; //终点
cout<<"———————————————————"<<endl;
}
}

完整程序(含图的邻接矩阵)

#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int MAX = 100;
const int INF = 1e7; typedef char ApexType; //顶点名称数据类型
typedef int EdgeType; //边权数据类型 typedef struct { ApexType apex[MAX]; //顶点表
EdgeType edge[MAX][MAX]; //矩阵图
int nodenums, edgenums; //顶点个数,边个数 }Graph; //创建邻接矩阵
void CreateGraph(Graph *G){
int i, j, k;
int w;
cout<<"输入顶点个数和边的条数: ";
cin>>G->nodenums>>G->edgenums;
//输入顶点信息
for(i = 0; i < G->nodenums; i++){
cout<<"输入第 "<<i + 1<<" 个顶点的名称: ";
cin>>G->apex[i];
}
//初始化各顶点之间的边为无穷大
for(i = 0; i < G->nodenums; i++)
for(j = 0; j < G->nodenums; j++)
G->edge[i][j] = INF;
//录入有向边的信息
for(k = 0; k < G->edgenums; k++){
EdgeType w;
cout<<"输入<vi, vj>的对应点下标及权值: ";
cin>>i>>j>>w;
G->edge[i][j] = w;
}
} //打印图的邻接矩阵
void ShowGraphInMatrix(Graph *G){
cout<<" ";
for(int i = 0; i < G->nodenums; i++)
printf("%4c",G->apex[i]);
cout<<endl; for(int i = 0; i < G->nodenums; i++){
printf("%3c", G->apex[i]);
for(int j = 0; j < G->nodenums; j++){
if(G->edge[i][j] == INF)
cout<<"∞ ";
else
printf("%4d", G->edge[i][j]);
}
cout<<endl;
}
} //距离记录数组 , 前驱数组
int dist[MAX], pre[MAX];
//集合S标记数组。如果flag[i]=true,说明该顶点i已经加入到集合S(最短路径集合);否则i属于集合V-S
bool flag[MAX]; void Dijkstra(Graph *G, int u){
for(int v = 0; v < G->nodenums; v++){
dist[v] = G->edge[u][v]; //初始化源点u到各邻接点v的距离
flag[v] = false;
if(dist[v] != INF)
pre[v] = u; //若有邻接边,顶点v有前驱顶点u
else
pre[v] = -1; //若没有,先初始化为-1
}
flag[u] = true; //初始化集合S,只有一个元素: 源点u
dist[u] = 0; //初始化源点u到自己的最短路径为0 /* 在集合V-S中寻找距离源点u最近的顶点t,使当前最短路径树最优 */
for(int i = 0; i < G->nodenums; i++){
int tmp = INF, t = u;
for(int v = 0; v < G->nodenums; v++){
if(!flag[v] && dist[v] < tmp){
//不在集合S中 并且 更小距离
t = v;
//记录在V-S中距离源点u最近的顶点v
tmp = dist[v];
}
} if(t == u)
return; //未找到直接终止
flag[t] = true; //否则, 将t加入集合S /* 更新集合V-S中与t邻接的顶点到u的距离,扩展当前最短路径树 */
for(int v = 0; v < G->nodenums; v++){
if(!flag[v] && G->edge[t][v] != INF){
//不在集合S中 且 有边
if(dist[v] > dist[t] + G->edge[t][v]){
//源点u可以借助t到达v的距离更短
dist[v] = dist[t] + G->edge[t][v];
pre[v] = t;
}
}
}
}
} //还原源点u到各点具体路径
void ShowShortParth(Graph G, int u){
for(int v = 0; v < G.nodenums; v++){
if(dist[v] == INF || dist[v] == 0)
continue;
cout<<"\n点"<<G.apex[u]<<" 到 点"<<G.apex[v]<<" 的最短路径距离为: "<<dist[v]<<endl;
cout<<"点"<<G.apex[v]<<"的前驱顶点为: 点"<<G.apex[pre[v]]<<endl;
cout<<"具体路径为: "<<endl; int t = pre[v]; //终点的前驱下标
//用栈存储终点前驱们 一直到 源点
stack<int> st;
while(t != u){
st.push(t);
t = pre[st.top()];
} cout<<G.apex[u]; //源点
while(!st.empty()){
t = st.top();
cout<<" --> "<<G.apex[t]; //中间点
st.pop();
}
cout<<" --> "<<G.apex[v]<<endl; //终点
cout<<"———————————————————"<<endl;
}
} main(){
Graph G;
CreateGraph(&G);
ShowGraphInMatrix(&G); int u;
cout << "\n输入出发的源点下标: ";
cin>>u; Dijkstra(&G, u); cout<<"\n源点到所有点的单源最短路径距离:"<<endl;
ShowShortParth(G, v);
}

结果

单源最短路径及具体路径

原文链接:[最短路径问题]Dijkstra算法(含还原具体路径) - Amαdeus - 博客园 (cnblogs.com)

贪心算法Dijkstra的更多相关文章

  1. [C++]单源最短路径:迪杰斯特拉(Dijkstra)算法(贪心算法)

    1 Dijkstra算法 1.1 算法基本信息 解决问题/提出背景 单源最短路径(在带权有向图中,求从某顶点到其余各顶点的最短路径) 算法思想 贪心算法 按路径长度递增的次序,依次产生最短路径的算法 ...

  2. [C++]多源最短路径(带权有向图):【Floyd算法(动态规划法)】 VS n*Dijkstra算法(贪心算法)

    1 Floyd算法 1.1 解决问题/提出背景 多源最短路径(带权有向图中,求每一对顶点之间的最短路径) 方案一:弗洛伊德(Floyd算法)算法 算法思想:动态规划法 时间复杂度:O(n^3) 形式上 ...

  3. 贪心算法之Dijkstra

    贪心算法的主要思想就是通过不断求解局部最优解,最后求出最优解或者最优解的近似值,不能保证一定为最优解. Dijistra算法,选取没有选择过的点到已经选择过得点组成的集合中最短的距离的点.然后更新已选 ...

  4. 带权图的最短路径算法(Dijkstra)实现

    一,介绍 本文实现带权图的最短路径算法.给定图中一个顶点,求解该顶点到图中所有其他顶点的最短路径 以及 最短路径的长度.在决定写这篇文章之前,在网上找了很多关于Dijkstra算法实现,但大部分是不带 ...

  5. 最短路径算法Dijkstra和A*

    在设计基于地图的游戏,特别是isometric斜45度视角游戏时,几乎必须要用到最短路径算法.Dijkstra算法是寻找当前最优路径(距离原点最近),如果遇到更短的路径,则修改路径(边松弛). Ast ...

  6. 最短路径算法-Dijkstra算法的应用之单词转换(词梯问题)(转)

    一,问题描述 在英文单词表中,有一些单词非常相似,它们可以通过只变换一个字符而得到另一个单词.比如:hive-->five:wine-->line:line-->nine:nine- ...

  7. leetcode 贪心算法

    贪心算法中,是以自顶向下的方式使用最优子结构,贪心算法会先做选择,在当时看起来是最优的选择,然后再求解一个结果的子问题. 贪心算法是使所做的选择看起来都是当前最佳的,期望通过所做的局部最优选择来产生一 ...

  8. 10行实现最短路算法——Dijkstra

    今天是算法数据结构专题的第34篇文章,我们来继续聊聊最短路算法. 在上一篇文章当中我们讲解了bellman-ford算法和spfa算法,其中spfa算法是我个人比较常用的算法,比赛当中几乎没有用过其他 ...

  9. 贪心算法(Greedy Algorithm)

    参考: 五大常用算法之三:贪心算法 算法系列:贪心算法 贪心算法详解 从零开始学贪心算法 一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以 ...

  10. 算法导论----贪心算法,删除k个数,使剩下的数字最小

    先贴问题: 1个n位正整数a,删去其中的k位,得到一个新的正整数b,设计一个贪心算法,对给定的a和k得到最小的b: 一.我的想法:先看例子:a=5476579228:去掉4位,则位数n=10,k=4, ...

随机推荐

  1. 洛谷P3690 (动态树模板)

    一位大佬写的代码.(加上我自己的一些习惯性写法) 存个模板. 1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N= ...

  2. Bing 广告平台迁移到 .net6

    原文链接 https://devblogs.microsoft.com/dotnet/bing-ads-campaign-platform-journey-to-dotnet-6/ 广告组件平台对于微 ...

  3. Mybatis PageHelper 使用的注意事项

    什么时候会导致不安全的分页? PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的. 只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查 ...

  4. envoy开发调试环境搭建

    image 前段时间研究envoy的filter开发,在windows机器环境上面折腾了会,这里记录一下,希望能够帮助到大家少走一些坑 主要是使用vscode devContainer的方式来搭建开发 ...

  5. 解决在JS中阻止定时器“重复”开启问题、Vue中定时器的使用

    1.问题描述 在一些需求开发中.需要设定软件提供服务的时间段(营业时间).这时可以选择定时器来实现.可以选择让定时器每隔一段时间检测当前时间是否在服务时间.到达服务时间.进入服务状态.未到服务时间.进 ...

  6. HTML+CSS基础知识(2)选择器的使用、盒子模型的讲解、列表的使用

    文章目录 1.CSS基础知识 2.css样式 2.1.代码: 2.2 测试结果 3.CSS的语法 3.1 代码 4.块元素和行内元素 4.1 代码 4.2 测试结果 5.常用的选择器 5.1 代码块 ...

  7. 齐博x1如何取消某个标签的缓存时间

    标签默认会有缓存, 如果你要强制取消缓存时间的话, 可以加上下面的参数 time="-1"如下图所示 标签默认缓存时间是10分钟, 你也可以改成其它时间 比如 time=" ...

  8. .NET Core C#系列之XiaoFeng.Data.IQueryableX ORM框架

    ​ 当前对象操作数据库写法和EF Core极度类似,因为现在大部分程序员都懒得去写SQL,再一个就是项目作大了或其它原因要改数据库,每次改数据库,那么写的SQL语句大部分要作调整,相当麻烦,并且写SQ ...

  9. 16.-admin管理后台

    一.admin管理后台 Django提供给了比较完善的后台管理数据库接口,可供开发过程中调用和测试使用 Django会搜集所有已注册的模型类,为这些模型类提供数据管理界面,供开发者使用   命令:py ...

  10. 这次彻底读透 Redis

    1. Redis 管道 我们通常使用 Redis 的方式是,发送命令,命令排队,Redis 执行,然后返回结果,这个过程称为Round trip time(简称RTT, 往返时间).但是如果有多条命令 ...