1、单源汇最短路(单起点)
<1>边权均为正数
(1)朴素Dijkstra算法
思路:
①用dis[]数组来维护所有点到起点的最短距离,假设起点为1,则初始化dis[1]=0,其余各点dis[i]='+∞'(大过题目范围即可,一般取INF=0x3f3f3f3f)
②迭代i:1~n(n个点,m条边)
对每轮迭代,将已经确定了最短距离的点打上标记放入集合S中,再找到S外的点离起点最近距离的点t,再用t维护其他点到起点的距离,更新dis[i]
在第k轮迭代中dis[i]的意义表现为从起点经过不超过k条边到达i点的最短距离
迭代完n次后即可得到每个点到起点的最短距离dis[i]
注意事项:
①朴素Dijkstra算法一般用于稠密图,体现为点少边多(相对而言),采用邻接矩阵d[a][b]存储a->b的有向边
②注意初始化各数组
时间复杂度:O(mn)
附上代码:
//单源汇最短路:正权边+稠密图
//朴素Dijkstra算法
//邻接矩阵存有向图
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e3+10,M=1e5+10,INF=0x3f3f3f3f;//存取相关参数,具体情况依题目而定
//N代表点数,M代表边数,INF代表+∞
int d[N][N];//邻接矩阵存有向图
int dis[N];//维护每个点到起点的最短距离
bool st[N];//标记已经确定了最短距离的点集,集合内的点标记为true
int n,m;//点数、边数,这里一律默认起点为1号点,终点为号点,故不再设起点终点,具体实际情况可能不一样,有可能需要再设两个变量存储起点终点
int dijkstra()
{
//初始化dis[]
memset(dis,0x3f,sizeof dis);
//把起点加进来
dis[1]=0;
//开启n轮迭代
for(int i=1;i<=n;i++)
{
int t=-1;//t将存储集合外的最近点
for(int j=1;j<=n;j++)
{
if(!st[j]&&(t==-1||dis[t]>dis[j]) t=j; //集合外+最近点
}
//把t先加进集合
st[t]=true;
//然后更新其他点到起点最近点的距离
for(int j=1;j<=n;j++) dis[j]=min(dis[j]+d[t][j]);
}
return dis[n];
}
int main()
{
cin>>n>>m;
//维护d[][]
memset(d,0x3f,sizeof d);
for(int i=1;i<=n;i++) d[i][i]=0;
while(m--)
{
int a,b,w;
cin>>a>>b>>w;//从a->b长度为w的有向边
d[a][b]=min(d[a][b],w); //防止重边和自环
}
//运行朴素Dijkstra算法
int t=dijkstra();
if(t!=INF) cout<<t<<endl;
else cout<<"impossible"<<endl; //如果返回INF,说明从起点到不了终点
return 0;
}
上述代码中仍有以下缺陷:
①对于邻接矩阵存储有向边的方法,当点数过于多时会出现存储困难(数组开不了太大)
②每轮迭代采用枚举寻找最近点,得把每条边遍历一遍,不够高效
但朴素Dijkstra算法代码简洁,在处理稠密图时具有相当大的优势,对于其他情形,需要对该算法进行优化
(2)堆优化版Dijkstra算法--->稀疏图(点多边少)
优化途径:
①用邻接表存有向图,可以存更多的点,更加高效
②采用小根堆来存储每个点到起点的距离,堆顶元素即为最近点,直接弹出堆顶元素即可,不用枚举
其余细节均与朴素Dijkstra同
思路:
①用dis[]数组来维护所有点到起点的最短距离,假设起点为1,则初始化dis[1]=0,其余各点dis[i]='+∞'(大过题目范围即可,一般取INF=0x3f3f3f3f)
②迭代i:1~n
对每轮迭代,将已经确定了最短距离的点打上标记放入集合S中,再找到S外的点离起点最近距离的点t,再用t维护其他点到起点的距离,更新dis[i]
在第k轮迭代中dis[i]的意义表现为从起点经过不超过k条边到达i点的最短距离
迭代完n次后即可得到每个点到起点的最短距离dis[i]
注意事项:
①朴素Dijkstra算法一般用于稀疏图,体现为点多边少(相对而言),采用邻接表存储a->b的有向边
②注意初始化各数组
时间复杂度:O(nlogm)
附上代码:
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10,M=2e5+10,INF=0x3f3f3f3f;
int h[N],ne[M],e[M],w[M],idx;
int dis[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra()
{
//初始化dis[]
memset(dis,0x3f,sizeof dis);
dis[1]=0;
//声明小根堆
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,1});//把起点插入堆,第1个参数表示到起点距离,第2个参数是点的编号
while(heap.size())//当堆中还有元素,迭代继续
{
auto t=heap.top();//找到集合外最近点,并弹出
heap.pop();
int p=t.second,d=t.first;
if(st[p]) continue;
//如果p标记了true,说明之前已经更新过p点了,这条边为重边,肯定比之前那条便长,不用继续更新
st[p]=true;//标记p点
//用t维护其他点到起点最近距离
for(int i=h[p];i!=-1;i=ne[i]) //更新所有p点连接的点
{
int j=e[i];
int d1=w[i];
//p可到达j点,距离为d1
if(dis[j]>d+d1)
{
dis[j]=d+d1;
heap.push({dis[j],j});//把更新后的距离插入到堆中
}
}
}
return dis[n];
}
int main()
{
cin>>n>>m;
//初始化邻接表
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t=dijkstra();
if(t!=INF) cout<<t<<endl;
else cout<<"-1"<<endl;
return 0;
}
<2>存在负权边
(1)Bellman-Ford算法
思路:
①用结构体数组存储每条a->b的边w,初始化dis[1]=0,其余各边dis[i]=‘+∞'
②迭代i:1~n (对每轮迭代k,dis[i]仍表现为经过不超过k条边到达i点的最短距离)
枚举每一条边a,b,w,更新dis[b]
注意:
上式子代表起点到b有两个方案:①走上一轮迭代留下的dis[b] ②走不超过k-1条边留下的dis[a]+a->b 也就是说dis[a]必须是在上一轮迭代中(k-1)留下的,但实际上在第k轮迭代中,可能dis[a]已经更新到了第k轮迭代,故需要备份 备份 备份!(重要的事情说三遍~)
Bellman-Ford算法其实就是把到达一个点的所有情况都枚举一遍,然后存储所有情况中最小的值
时间复杂度:O(mn)
附上代码:
//Bellman-Ford算法:负权边最短路Ⅰ
#include<iostream>
#include<cstring>
using namespace std;
const int N=510,M=1e5+10,INF=0x3f3f3f3f;
struct Edge
{
int a,b,w;
}edges[M]; //结构体数组存储边
int dis[N];
int last[N]; //备份上一轮迭代的值
int n,m,k;
int bellman_ford()
{
//初始化dis[]
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=1;i<=n;i++)
{
memcpy(last,dis,sizeof dis);
//枚举每条边
for(int j=0;j<m;j++)
{
int a=edges[j].a,b=edges[j].b,w=edges[j].w;
dis[b]=min(dis[b],last[a]+w);
//last[a]为上一轮留下的dis[a]
}
}
return dis[n];
}
int main()
{
cin>>n>>m>>k;
//存储每条边
for(int i=0;i<m;i++)
{
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};
}
//运行Bellman-Ford算法
int t=bellman_ford();
if(t>INF/2) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
该算法具有以下缺陷:
①为找到最短路,枚举了所有的情况,效率过低
(但由于这种算法可以求出经过不超过k条边的最短路,大多仅使用在有边数限制的负权最短路问题中)
(2)SPFA算法
优化途径:
在Bellman-Ford算法的
显然,只有当第k-1轮迭代dis[a]有更新,在第k轮才有可能更新dis[b]
(证明如下:
在第k-1轮中:
若第k-1轮迭代没更新dis[a],即
所以
即dis[b]不会再更新)
故SPFA算法中用一个队列来存储每轮迭代中更新过的点,以便于确定下一轮迭代中有可能需要再更新距离的点,而不是枚举所有情况
思路:
①用邻接表存储a->b的有向边w
②用队列来存储每轮迭代中更新过dis[]距离的点,直至队列不再含有元素时代表所有最短距离已经更新完毕
时间复杂度:大部分情况为O(m),最坏情况下为O(mn)
附上代码:
//SPFA算法:负权边最短路Ⅱ
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10,M=1e5+10,INF=0x3f3f3f3f;
int h[N],e[M],ne[M],w[M],idx;
int dis[N];
int st[N];//标记队列中元素,避免重边时重复入队
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa()
{
//初始化dis[]
memset(dis,0x3f,sizeof dis);
dis[1]=0;
queue<int>q;
q.push(1);//起点入队
while(q.size())//当队列中有元素,说明最短路还需要维护
{
int t=q.front();//弹出队头
q.pop();
st[t]=false;//t已经出队了,消除标记
for(int i=h[t];i!=-1;i=ne[i])
{
//更新所有t可以到达的边
int j=e[i];
int d=w[i];
if(dis[j]>dis[t]+d)
{
dis[j]=dis[t]+d;
if(!st[j])
{
//当j不在队列里,打上标记并入队
st[j]=true;
q.push(j);
}
}
}
}
return dis[n];
}
int main()
{
cin>>n>>m;
//初始化邻接表
memset(h,-1,sizeof h);
while(m--)
{
int a,b,w;
cin>>a>>b>>w;
add(a,b,w);
}
int t=spfa();
if(t>INF/2) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}