最短路径问题
定义
给定图G<V,E>,求一条从起点到终点的路径,使得路径上经过的所有的边权值之和最小。称为最短路径问题。
常用算法有Dijkstra算法、Bellman-Ford算法、SPFA算法和Floyd算法。
Dijkstra算法
迪杰斯特拉算法用来解决单源最短路问题(即给定图G和起点S,求S到其他每个顶点的最短距离)。同时无法处理负权。
策略:
设置集合S存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数):
① 每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S
② 之后,令u为中介点,更新起点s与所有能经过u到达的顶点v之间的最短距离。
具体实现:
考虑集合S的实现,起点s到达顶点Vi的最短距离的实现
- 集合S使用bool型数组vis[]来实现,当vis[i] == true时,表示Vi已被访问;vis[i] == false时,表示Vi未被访问。
- 令int型数组d[ ]表示起点s到顶点Vi的最短距离,初始时除了起点s的d[s] = 0,其他的d[i] 是一个很大的数,经常使= 0x3fffffff 来表示inf
Dijkstra伪代码:
//设G为图,一般设成全局变量;数组d为源点到达各点的最短路径长度,s为起点
Dijkstra (G,d[], s){
初始化;
for(循环n次){
u = 使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发能够到达所有顶点v){
if(v未被访问 && 以u中转使s到v的最短距离d[v]更小){
更新优化d[v];
}
}
}
}
具体实现代码
此时图可以使用邻接矩阵或者邻接表两种形式存储。当顶点数<1000时,使用邻接矩阵(也较为简单)。
邻接矩阵表示:
枚举所有顶点来查看v是否可由u到达
C++:
#include<bits/stdc++.h>
using namespace std;
const int MAXV = 1000;//最大顶点数
const int INF = 0x3fffffff;//设INF为一个很大的数
int n,G[MAXV][MAXV]; //n为顶点数
int d[MAXV]; //起点到达各个顶点的最短路径长度
bool vis[MAXV] = {
false}; //标记是否访问的数组
void Dijkstra(int s){
//s为起点
fill(d,d+MAXV,INF);//fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0;
for(int i=0;i<n;i++){
//循环n次
int u =-1 , MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j =0;j<n;j++){
//找到未被访问的顶点中d[u]最小的
if(vis[j]==false && d[j]<MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return; //找不到更小的顶点
vis[u] = true; //标记u为已访问
for(int v = 0;v<n;v++){
//如果v未访问 && u能到达v && 以u为中转可以优化d[v]距离
if(vis[v]== false && G[u][v] != INF && d[u]+G[u][v] < d[v]){
d[v] = d[u]+G[u][v];//优化d[v]
}
}
}
}
邻接表表示:
可以直接得到u能够到达的顶点v,无须遍历所有顶点,当顶点较多的时候可以使用
#include<bits/stdc++.h>
using namespace std;
const int MAXV = 1000;//最大顶点数
const int INF = 0x3ffffff;//设INF为一个很大的数
struct Node{
int v,dis; //v为边的目标顶点,dis为边权值
};
vector<Node> Adj[MAXV];//存放图G,Adj[u]存放从顶点u出发的所有可达顶点
int n;//顶点数
int d[MAXV]; //起点到达各个顶点的最短路径长度
bool vis[MAXV] = {
false}; //标记是否访问的数组
void Dijkstra(int s){
//s为起点
fill(d,d+MAXV,INF);//fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0;
for(int i=0;i<n;i++){
//循环n次
int u =-1 , MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j =0;j<n;j++){
//找到未被访问的顶点中d[u]最小的
if(vis[j]==false && d[j]<MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return; //找不到更小的顶点
vis[u] = true; //标记u为已访问
//只有下面这个for循环与邻接矩阵不同
for(int j = 0;j<Adj[u].size();j++){
int v = Adj[u][j].v;//通过邻接表直接获得u能到达的顶点v
if(vis[v] == false && d[u]+Adj[u][j].dis <d[v]){
//如果v未访问&&以u为中转可以优化d[v]距离
d[v] = d[u]+Adj[u][j].dis;//优化d[v]
}
}
}
}
如果题目给出的无向边而不是有向边,这时只需要把无向边当成两条指向相反的有向边即可。对于邻接矩阵来说,一条u到v之间的无向边,在输入时可以分别对G[u][v] = d[v][u] = value ; 对于邻接表来说,只需要在u的邻接表Adj[u]的末尾添加上v,并在v的邻接表Adj[v]的末尾加上u即可。
求最短路径本身
设置一个数组pre[ ],pre[v]表示从起点s到顶点v的最短路径上v的前一个顶点的编号。这样,当伪代码中的条件成立时,就可以将u赋值给pre[v],最终就能把最短路径上每一个顶点的前驱顶点记录下来。
伪代码
for(从u出发能够到达所有顶点v){
if(v未被访问 && 以u中转使s到v的最短距离d[v]更小){
更新优化d[v];
令v的前驱为u;
}
}
邻接矩阵表示:
C++:
#include<bits/stdc++.h>
using namespace std;
const int MAXV = 1000;//最大顶点数
const int INF = 0x3fffffff;//设INF为一个很大的数
int n,G[MAXV][MAXV]; //n为顶点数
int d[MAXV]; //起点到达各个顶点的最短路径长度
int pre[MAXV]; //pre[v]表示从起点到顶点v的最短路径上v的前一个顶点【新添加】
bool vis[MAXV] = {
false}; //标记是否访问的数组
void Dijkstra(int s){
//s为起点
fill(d,d+MAXV,INF);//fill函数将整个d数组赋值为INF(慎用memset)
for(int i =0;i<n;i++) pre[i] = i; //初始状态设每个点的前驱为自身【新添加】
d[s] = 0;