目录
目录
1.单源最短路
1.1所有权边都是正数
1.1.1朴素Dijsktra算法
a.算法描述
(1)初始化dist数组,dist[1]=0,dist[∞]。集合S={已经求得最短路径的结点},集合V={还未求得最短路径的结点}。
(2)设当前检索的结点下标为i,对于结点j∈V,若(i,j)存在且dist[i]+(i,j)<dist[j]的值,则更新dist[j]。
(3)在全部更新结束后,从集合V里选取dist值最小的结点作为下一个扩展结点,并将其加入集合S。
(4)若此时结点已经全部遍历完,算法结束,否则重复执行步骤(2).
b.代码实现
#include<iostream>
#include<cstring>
#include<math.h>
using namespace std;
const int N=510;
int g[N][N];//邻接矩阵
int dist[N];//the minimum distance of each point
bool str[N];//在集合S还是在集合V里面的标志,str=true代表已经求得最短路径,false代表还在集合V中
int n,m;
void dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;//初始化
int count=n-1;
//注意是n-1此循环,在扩展到n-1个结点之后,求得的dist[n]的值就是结点1到结点n的最短路径
while(count--){//n-1次循环,找最小dist[]→更新dist[]
//开始寻找dist[j]最小的结点
int t=-1;
for(int i=1;i<=n;i++){
if(!str[i] && (t==-1 || dist[i]<dist[t])){//如果还没有求得最短路径并且是当前dist最小的结点
t=i;
}
}
str[t]=true;//加入集合S中
for(int i=1;i<=n;i++){
dist[i]=min(dist[t]+g[t][i],dist[i]);//更新dist的值
}
}
if(dist[n]==0x3f3f3f3f) cout<<-1;
else cout<<dist[n];
}
int main(){
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--){
int a,b,c;
cin>>a>>b>>c;
if(a!=b) g[a][b]=min(g[a][b],c);//由于重边与自环的存在,自环可以忽略,重边需要取最短
}
dijsktra();
}
c.时间复杂度分析
找到当前dist值最小的结点需要遍历n次,而我们需要进行n次这样的过程,朴素dijstra算法时间复杂度为O(n²)。
1.1.2 堆优化Dijkstra算法
a.算法描述
朴素的dijkstra算法主要在寻找dist最小的结点上耗费时间,而堆取得最小值只需要O(1)的时间复杂度。
b.代码实现
采用模拟堆的方式,堆存放的内容为二元组pair<int,int>,first表示下标,second表示对应的dist的值,根据dist的值来排序。
1.定义向上调整函数up:
只要堆的序号x满足:不是根结点、父亲结点的值更大,就交换heap[x]与heap[x/2]的内容。
void up(int x) {
while(x / 2 && heap[x / 2].second > heap[x].second){
swap(heap[x/2],heap[x]);
x=x/2;
}
}
2.定义向下调整函数down:
用t来记录当前需要堆调整的序号,检索heap[2*t].second和heap[2*t+1].second的内容,完成交换。如果此时x=t,说明已经调整完毕;如果此时x!=t,当前需要调整的位置为t,在调用down(t)函数。
void down(int x) {
int t = x;
if (2 * x<= Size && heap[2 * x].second < heap[t].second) t = 2 * x;
if (2 * x + 1 <= Size && heap[2 * x + 1].second < heap[t].second) t = 2 * x + 1;
swap(heap[t], heap[x]);
if (t != x)down(t);
}
3.dijkstra()
void dijkstra(){
heap[++Size]={1,0};//将1号结点插入优先队列中
memset(dist,0x3f,sizeof dist);
dist[1]=0;
int count=n-1;
while(count--){
auto t=heap[1];
//删除堆顶元素:用最后一个元素覆盖堆顶元素,然后向下调整
swap(heap[1],heap[Size]);
Size--;
down(1);
if(!str[t.first]){//如果是在集合V中,没有被遍历过,因为dist的值会被更新很多次,有可能会取到同一个点
//放入集合S中
str[t.first]=true;
int point=Head[t.first];
while(point!=-1){//该结点所有的扩展结点,更新dist值
int j=e[point];
int distance=w[point];
if(!str[j] && t.second+distance<dist[j]){
dist[j]=t.second+distance;
//插入元素,在堆的末尾插入元素,再向上调整
heap[++Size]={j,dist[j]};
up(Size);
}
point=Next[point];
}
}
}
if(dist[n]==0x3f3f3f3f) cout<<-1;
else cout<<dist[n];
}
c.时间复杂度分析
堆中查找一个元素只需要O(1)的时间复杂度,若维护堆的结点数为n,即更新dist值时是修改堆中的值,堆查找的时间复杂度是O(logn),有m条边,那么时间复杂度是O(mlogn)。
1.2存在负权边的最短路径问题
1.2.1 Bellman-Ford算法
a.算法描述
Bellman-Ford算法的策略是对于有n个结点且有负权边的图,进行n次循环,每一次循环,若结点a到结点b存在一条边,进行一次更新:dist[b]=min{dist[a]+w(a,b),dist[b]}.
Bellman-Ford算法类似与广度优先搜索策略。在一个含有n个结点的图中,任意两点之间的最短路径最多只包括n-1条边。进行第一次更新,可以得到源点经过一条边到达其他结点的最短距离;进行第二次更新,可以得到源点经过两条边到达其他结点的最短距离……因而,至多进行n次循环,可以求出源点到所有其他结点的最短距离。
b.代码实现
在实现Bellman-Ford算法时,需要有以下几方面要注意:
1.当图中有负权回路时,最短路径有可能不存在。这是因为从源点到结点可以经过无限次这个负权回路,那么dist值会不断减小。当然,若这个负权回路不在任何到达这个结点的路径上,最短路径依然存在。而判断是否存在负权回路,只需要进行n次松弛操作(dist[b]=min{dist[a]+w(a,b),dist[b]}称为一次松弛),若此时仍然有结点的dist值进行了更新,那么就存在负权回路。这是因为对于一个只有n个结点的图,源点到该结点经过了n条路径,那么该路径上一定存在回路,而dist值又减小了,说明经过的该回路一定时负权回路。下面的代码求的是经过k条路径,即有边数限制的最短路径,那有负权回路也不会影响。
2.在每一次循环前,需要对dist数组进行备份。这是因为在第i次循环的时候,我们是在第i-1次循环后得到的dist数组的基础上修改的。如果直接使用dist数组,会有可能用到本次循环前面的结果,那相当于第i次循环,得到了源点经过i+1条边到达该结点的最短距离。
3.在初始化dist数组时,首先是要提前修改dist[1]为0。另外,对于最短路径不存在的情况,不能再使用判定条件:if(dist[n]>0x3f3f3f3f),这是因为尽管源点到结点n是不可达的,但是有可能存在同样不可达的结点到结点n之间有负边,此时dist[n]会比0x3f3f3f3f小.
#include<iostream>
#include<cstring>
#include<math.h>
using namespace std;
const int N=510;
const int M=10010;
struct Edge{
int a;
int b;
int w;
}edges[M];//定义结构体Edge,存储图的结构
int n,m,k,Index;
int dist[N];//距离
int backup[N];
void Bellman_Ford(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=k;i++){
memcpy(backup, dist, sizeof dist);//复制,将dist的所有内容复制到backup中
//warning1:而要复制的原因是,在遍历的过程中,有可能用到了本次遍历前面的更新结果,是不允许的
for(int j=0;j<m;j++){
dist[edges[j].b]=min(backup[edges[j].a]+edges[j].w,dist[edges[j].b]);//松弛
}
}
if(dist[n]>0x3f3f3f3f/2) cout<<"impossible";
//warning2:尽管源点到结点n是不可达的,但是存在同样不可达的结点到结点n之间有负边,此时
//dist[n]不再是0x3f3f3f3f
else cout<<dist[n];
//warning3:如果有负权回路的存在,可以无限次经过这个回路,会没有最短距离
//但是本题有限制经过k条边,因而有负权边也没有关系
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=m;i++){
int a,b,w;
cin>>a>>b>>w;
edges[Index++]={a,b,w};
}
Bellman_Ford();
}
c.时间复杂度分析
Bellman-Ford算法所耗费的时间主要来源于两层循环,进行n次循环,每次循环检索n条边,因而时间复杂度为O(mn).
1.2.2 SPFA(Bellman-Ford优化)
a.算法描述
在Bellman-Ford算法中,松弛操作dist[b]=min{dist[a]+w(a,b),dist[b]}.只有在dist[a]发生了变化的时候,松弛操作才有意义。因而SPFA算法即是在该方面对Bellman-Ford算法做优化:定义一个队列,只有当某个结点的dist值发生变化时,才将其加入队列,每次取队列头元素,找它的扩展结点。
换句话说,Bellman-Ford算法也将dist值没有变化的结点仍然进行了扩展(遍历每条边,事实上对每一个结点都进行了扩展)。SPFA算法使得在进行“广度搜索”时,只将dist值发生了变化的结点进行扩展,dist值没有发生变化的结点是没有意义的,也就是说,本质就是,dist[i]每变化(减小)一次,与其有边相连的结点的dist的值就有可能会发生变化,那么我们就要把它加入到队列中进行扩展(修改)
b.代码实现
SPFA代码实现最主要的就是队列,这里采用数组模拟队列,只不过这里加入队列的判定条件是:不仅你要和它有边相连,还要满足:dist[t]+w[point]<dist[j]
#include<iostream>
#include<math.h>
#include<cstring>
using namespace std;
const int N=100010;
int Head[N],e[2*N],Next[2*N],w[2*N],Index;
int dist[N];
int n,m;
int Queue[N];
int tail,head;
bool str[N];
void add(int a,int b,int c){
e[Index]=b;
Next[Index]=Head[a];
w[Index]=c;
Head[a]=Index++;
}
void SPFA(){
while(head<tail){
int t=Queue[head++];//取出元素,扩展
int point=Head[t];
str[t]=false;
while(point!=-1){
int j=e[point];
if(dist[t]+w[point]<dist[j]){
dist[j]=dist[t]+w[point];
if(!str[j]){
Queue[tail++]=j;
str[j]=true;
}
//warning:str数组的作用是,如果队列中已经有结点j了,而现在发现
//dist[t]+w[point]<dist[j],按理来说dist[j]发生了修改,要加入队列,
//但是此时可以不必加入队列,在后续要扩展结点j时,dist已经修改了
//如果此时再加入队列,如果在扩展两个dist[j]之间,dist[j]没有发生变化
//那相当于重复操作,如果dist[j]发生了变化,我又会把新的dist[j]加入进来,节省时间~
}
point=Next[point];
}
}
if(dist[n]==0x3f3f3f3f) cout<<"impossible";
else cout<<dist[n];
}
int main(){
memset(Head,-1,sizeof Head);
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
memset(dist,0x3f,sizeof dist);
dist[1]=0;
Queue[tail++]=1;
str[1]=true;
SPFA();
}
c.时间复杂度分析
一般情况下为O(m),最差为O(mn).(?)
1.2.3 SPFA判断负权环
a.算法描述
在SPFA算法求最短路径的基础上,我们增加一个count数组,可以类似于搜索的深度,即count[j]表示结点1从结点j的最短路径上的边数。每当由结点t使得dist[j]更新,即dist[t]+w(t,j)<dist[j]时,执行操作:count[j]=count[t]+1;
这样,当某个count[j]>=n时,我们就可以判断存在负环。首先,由于图的结点只有n个,当路径长度为n时,说明该条路径一定存在环。而这条路径又是最短路径,说明该环一定是负环(不然我为什么要走这个环)……
b.代码实现
在spfa算法判断负环时要注意以下几点
1.在用spa算法求最短路径时,我们会把头节点先加入到队列中,但spfa算法判断负环在队列初始化时,要把所有的结点都加入到队列中。这是因为图并不一定是连通的,该负环不一定是从源点1出发的负环。因而将所有的结点加入队列后,我们一定可以遍历到环中的某个结点,而整个环是负环,dist数组的更新就会在这个环里面打圈圈~,最后count[j]会大于n;和源点1相连的结点放入队列也没有影响,反正迟早也是要更新的...
2.会存在有情况说,从队列出去的元素再更新dist值时重新回到队列,因为队列的长度肯定不能定义为N..(如果使用数组模拟队列的话),最好是使用循环队列,即head和tail在到达长度时,重新置0;
#include<iostream>
#include<cstring>
#include <algorithm>
using namespace std;
const int N=4000000,M=10010;
int Head[N],e[M],Next[M],w[M],Index;
int Queue[N];//warning1:在手写队列中,出队列只是把head挪后了,因而插入队列总的结点数可能会多于N个
int head,tail;
int dist[N];
bool str[N];
int Count[N];//一旦超过n,就会出现负环
int m,n;
void add(int a,int b,int c){
e[Index]=b;
w[Index]=c;
Next[Index]=Head[a];
Head[a]=Index++;
}
bool spfa(){
for(int i=1;i<=n;i++){
Queue[tail++]=i;
str[i]=true;
}
dist[1]=0;
while(head<tail){
int t=Queue[head++];
if(head==N) head=0;
str[t]=false;
int point=Head[t];
while(point!=-1){
int j=e[point];
if(dist[j]>dist[t]+w[point]){
Count[j]=Count[t]+1;//其实就相当于深度,到结点1经过了多少条边
dist[j]=dist[t]+w[point];
if(Count[j]>=n){
return true;
}
if(!str[j]){
Queue[tail++]=j;
if(tail==N) tail=0;
str[j]=true;
}
}
point=Next[point];
}
}
return false;
}
int main(){
memset(dist,0x3f,sizeof dist);
memset(Head,-1,sizeof Head);
cin>>n>>m;
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa()) cout<<"Yes";
else cout<<"No";
}
2.多源最短路问题
Floyd算法
a.算法描述:
Floyd算法是基于动态规划,基本过程是进行三层循环:
b.代码实现
#include<iostream>
#include<math.h>
using namespace std;
const int N=210;
int g[N][N];//邻接矩阵
int n,m,k;
void Floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
}
}
}
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j) g[i][j]=0;//自己到自己肯定不能定义为无穷啊...
else g[i][j]=0x3f3f3f3f;
}
}
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);//重边
}
Floyd();
while(k--){
int x,y;
cin>>x>>y;
if(g[x][y]>0x3f3f3f3f/2) cout<<"impossible"<<endl;
else cout<<g[x][y]<<endl;
}
}
c.时间复杂度为O(n³)