最短路问题
最短路问题是图论的经典问题,有以下常用算法:
- Dijkstra算法/迪杰斯特拉算法 适用于正权图的单源最短路径
- Bellman-Ford算法 含负权边的带权有向图的单源最短路问题。不能处理带负权边的无向图
- SPFA算法 使用队列优化的Bellman-Ford算法
- Floyd算法 求出图中每两点之间的最短路
- A*算法 启发式的路径搜索算法
这里先从最简单的Dijkstra算法讲起
一. Dijkstra算法
1.1 基本思想与算法
Dijkstra可用于正权图上的单源最短路径,同时适用于有向图和无向图
算法将顶点集V分成两部分:已找到最短路的顶点集合S,还未找到最短路的集合V-S
伪代码如下
清除所有点的标号(vis[]置零/集合S置空)
初始化源点到每个顶点的距离d[]
循环n次{
在所有未标号节点中(在集合V-S中),选出d值最小的顶点p
将p加入集合S
对于从p出发的所有边(p,j),更新d[j]=min{d[j],d[p]+w(p,j)}
}//每一轮将一个新顶点p加入集合S
c++语言代码
memset(vis,0,sizeof(vis));
for(int i=0;i<n;i++) d[i]=(i==start?0:INF);
for(int i=0;i<n;i++){
int p,min=INF;
for(int j=0;j<n;j++){
if(!vis[j]&&d[j]<min){
p=j;
min=d[j];
}
}
vis[p]=1;
for(int j=0;j<n;j++){
d[j]=min(d[j],d[p]+w(p,j));
path[j]=p;//记录节点j的父节点,可以打印出最短路径内容
}
}
可以看到上述代码的时间复杂度为(O(n^2))
Dijkstra算法可以有多种推广,例如:
- 打印出源点到每个顶点的最短路径内容
- 当顶点A到B的最短路径不止一条时,计算最短路径条数
- 当有多条最短路径时,求经过顶点权值之和最大的那条(相当于多加了一个约束)
以PAT甲级1003Emergency为例说明
https://pintia.cn/problem-sets/994805342720868352/problems/994805523835109376
直接贴代码:
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<stack>
const int maxn=1e3;
const int INF=1e8;
int n,m,st,en,x,y,z;//st-start,en-end
/*
val-每个点救援队数量
valsum-到点i之前的数量之和
path-点i前那个点的编号(即最短路径上的父节点编号)
pathsum-到点i的最短路径的条数
*/
int val[maxn],valsum[maxn],path[maxn],pathsum[maxn];
/*
d-该点到start的距离
vis-是否加入S集合
G-邻接矩阵
*/
int d[maxn],vis[maxn],G[maxn][maxn];
//建图初始化
void init();
//迪杰斯特拉算法
void dijkstra();
//输出路径
void print();
int main(){
scanf("%d%d%d%d",&n,&m,&st,&en);
for(int i=0;i<n;i++) scanf("%d",&val[i]);
init();
for(int i=0;i<m;i++){//考虑到是无向图
scanf("%d%d%d",&x,&y,&z);
G[x][y]=z;
G[y][x]=z;
}
dijkstra();
printf("%d %d
",pathsum[en],valsum[en]);
//print();
}
void init(){
memset(path,0,sizeof(path));
memset(vis,0,sizeof(vis));
memset(pathsum,0,sizeof(pathsum));
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(i==j) G[i][j]=0;
else G[i][j]=INF;
}
valsum[i]=val[i];
}
}
void dijkstra(){
for(int i=0;i<n;i++){
d[i]=G[st][i];//初始化源点到每个顶点的距离
}
d[st]=0;
pathsum[st]=1;//初始化起点的最短路的条数是1
int min1,p;
for(int i=0;i<n;i++){
min1=INF;
for(int j=0;j<n;j++){
if(!vis[j]&&d[j]<min1){
min1=d[j];
p=j;
}
}//找到V-S集合中最小的d[p]
vis[p]=1;//将顶点p加入S集合中
//更新与顶点p邻接的顶点的最短路距离
for(int j=0;j<n;j++){
if(!vis[j]&&d[j]>d[p]+G[p][j]){
d[j]=d[p]+G[p][j];
pathsum[j]=pathsum[p];//当松弛时,到j和到p的最短路条数相同
valsum[j]=valsum[p]+val[j];//松弛时,直接加上更新后的点值
path[j]=p;//记录父节点
}
else if(!vis[j]&&d[j]==d[p]+G[p][j]){
pathsum[j]+=pathsum[p];
if(valsum[j]<valsum[p]+val[j]){
valsum[j]=valsum[p]+val[j];
path[j]=p;
}//当都是最短路时,记录经过救援队最多的路径
}
}
}
}
void print(){
int tmp;
std::stack<int> stk;
stk.push(en);
while(st!=en){
tmp=path[en];
stk.push(tmp);
en=tmp;
}
while(!stk.empty()){
printf("%d ",stk.top());
stk.pop();
}
printf("
");
}
1.2复杂度优化
上述算法复杂度为(O(n^2)),在最坏情况下(m)和(n^2)是同阶的,但是对于稀疏图而言有(mll n^2)成立,因此可将时间复杂度由 (O(n^2)) 优化至(O(mlog n))
主要的优化点在于"在所有未标号节点中(在集合V-S中),选出d值最小的顶点p",这一步可以维护一个小根堆来代替遍历操作寻找d值最小的顶点p
struct HeapNode{
int d,u;//d-顶点离start的距离,u-顶点的序号
bool oprator < (const HeapNode& rhs) const{
return d>rhs.d;
}//priority_queue默认是优先级最大的元素在堆顶
}
void dijkstra(int start){
priority_queue<HeapNode> Q;
for(int i=0;i<n;i++) d[i]=INF;
d[start]=0;
memset(vis,0,sizeof(vis));
Q.push((HeapNode){0,start});
while(!Q.empty()){
HeapNode x=Q.top();
Q.pop();
int u=x.u;
if(vis[u]) continue;
vis[u]=1;
for(int i=0;i<G[u].size;i++){
Edge& e=edges[G[u][i]];
if(d[e.to]>d[u]+e.dist){
d[e.to]=d[u]+e.dist;
path[e.to]=u;
Q.push((HeapNode){d[e.to],e.to});
}
}
}
}