![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8e2ac0c7ade3c479c9343bbc7e626a0b.png)
dijkstra算法
单源最短路径算法
伪码描述:
邻接矩阵实现
- 基本代码
#include<iostream>
using namespace std;
#define MAXSIZE 1000
#define NoEdge 1000
int n,m;
int G[MAXSIZE][MAXSIZE]; //权重
int dist[MAXSIZE]; //单源点最短路径
bool visited[MAXSIZE];
int FindMinAdj()
{
int min_i=-1,min=NoEdge; //找离已经有的最短路径结点最近的点
for(int i=0;i<n;i++)
{
if(!visited[i]&&dist[i]<min)
{
min_i=i;min=dist[i];
}
}
return min_i;
}
void Solve(int s)
{
for(int i=0;i<n;i++) //初始化就当做只有s结点到各结点的情况,考虑路径,路径个数等
{
dist[i]=G[s][i];
visited[i]=false;
}
visited[s]=true;
while(true)
{
int min_i=FindMinAdj(); //找离已经确定的最短路径最近的结点
if(min_i==-1) break;
// cout<<min_i<<endl;
visited[min_i]=true;
for(int i=0;i<n;i++)
{
if(!visited[i]&&G[min_i][i]!=NoEdge)
{
if(dist[min_i]+G[min_i][i]<dist[i]) //最短路径优先考虑
{
dist[i]=dist[min_i]+G[min_i][i]; //更新最短路径长度
}
}
}
}
}
int main()
{
int s;
cin>>n>>m>>s;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
G[i][j]=NoEdge;
}
for(int i=0;i<m;i++)
{
int v1,v2,len;cin>>v1>>v2>>len;
G[v1][v2]=len;
G[v2][v1]=len;
}
Solve(s); //求起点s到其他所有顶点的最短路径
return 0;
}
- 时间复杂度:O(n^2)
- 用来求所有顶点每两个顶点间的最短距离,对每个顶点调用Dijistra,时间复杂度为O(n^3)
邻接表实现
1.代码
#include<bits/stdc++.h>
using namespace std;
#define MAXSIZE 10005
#define INFI 100000000
int n,m;
typedef struct Node
{
int id;
int W;
}AdjNode,*Adj;
vector<Adj>List[MAXSIZE]; //vector数组实现邻接表
int dist[MAXSIZE];
bool visited[MAXSIZE];
int FindMinAdj()
{
int min_i=-1,min=INFI; //找离已经有的最短路径结点最近的点
for(int i=0;i<n;i++)
{
if(!visited[i]&&dist[i]<min)
{
min_i=i;min=dist[i];
}
}
return min_i;
}
void Dijistra()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) //初始化
{
dist[j]=INFI;
visited[j]=false;
}
dist[i]=0;
while(true)
{
int min_i=FindMinAdj(); //找离已经确定的最短路径最近的结点
if(min_i==-1) break;
visited[min_i]=true; //纳入集合
int len=List[min_i].size();
for(int j=0;j<len;j++) //判断该点受影响的邻接点
{
int t=List[min_i][j]->id;
if(!visited[t]&&dist[min_i]+List[min_i][j]->W<dist[t])
{
dist[t]=dist[min_i]+List[min_i][j]->W;
}
}
}
for(int j=1;j<=n;j++)
{
cout<<dist[j]<<" ";
}
}
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=0;i<m;i++)
{
int u,v,w;
//cin>>u>>v>>w;
scanf("%d %d %d",&u,&v,&w);
if(u==v) continue;
Adj a=new AdjNode; //无向图,两个顶点都要进行操作
a->id=v;
a->W=w;
List[u].push_back(a);
Adj b=new AdjNode;
b->id=u;
b->W=w;
List[v].push_back(b);
}
Dijistra();
return 0;
}
- 时间复杂度:O(E+V)*V ----->O(n^2)
- 求多源最短路径,时间复杂度:O(E+V) * V * V----->O(n^3)
最小堆优化
思路就是将遍历查找dist数组中的最小值,改成将这些最小值装在最小堆里,直接取堆顶元素,
由于最小堆的查找最小值的时间复杂度是O(logn)的,将原来的O(n)的查找时间缩短了;
typedef pair<int,int>PII;
priority_queue<PII,vector<PII>,greater<PII>>que; //最小堆简单版声明方法,存储dist中的最小值,first存dist数组值,second存结点编号,默认按照first排序
1.代码
#include<bits/stdc++.h>
using namespace std;
#define MAXSIZE 10005
#define INFI 100000000
int n,m;
typedef struct Node
{
int id;
int W;
}AdjNode,*Adj;
struct cmp{ //自定义优先级,返回true表示前者优先级更低(与排序刚好相反)
bool operator()(const Adj a,const Adj b){
return a->W > b->W;
}
};
vector<Adj>List[MAXSIZE]; //vector数组实现邻接表
int dist[MAXSIZE];
bool visited[MAXSIZE];
priority_queue<Adj,vector<Adj>,cmp>que; //按边权值从小到大排序队列
void Dijistra()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) //初始化
{
dist[j]=INFI;
visited[j]=false;
}
dist[i]=0;
Adj t=new AdjNode;
t->id=i;
t->W=0;
que.push(t); //将起点纳入最小堆中,堆顶元素为到当前到起点最近的点
while(!que.empty())
{
Adj t_adj=que.top(); //获得堆顶元素(未收纳的顶点距离起点最近的点)
que.pop();
if(visited[t_adj->id]) continue; //该点已经收纳到结果集合了,获取下一个
int min_i=t_adj->id;
visited[min_i]=true; //纳入集合
int len=List[min_i].size();
for(int j=0;j<len;j++) //判断该点受影响的邻接点
{
int t=List[min_i][j]->id;
if(!visited[t]&&dist[min_i]+List[min_i][j]->W<dist[t])
{
dist[t]=dist[min_i]+List[min_i][j]->W;
Adj p=new AdjNode;
p->id=t;
p->W=dist[t];
que.push(p); //将离起点距离变短的点纳入最小堆
}
}
}
for(int j=1;j<=n;j++)
{
cout<<dist[j]<<" ";
}
cout<<endl;
}
}
int main()
{
//cin>>n>>m;
scanf("%d %d",&n,&m);
for(int i=0;i<m;i++)
{
int u,v,w;
//cin>>u>>v>>w;
scanf("%d %d %d",&u,&v,&w);
if(u==v) continue;
Adj a=new AdjNode; //无向图,两个顶点都要进行操作
a->id=v;
a->W=w;
List[u].push_back(a);
Adj b=new AdjNode;
b->id=u;
b->W=w;
List[v].push_back(b);
}
Dijistra();
return 0;
}
- 时间复杂度:O(E+V)*logV
- 算多源最短路径: O(E+V)* logV *V
应用:多权值+多路径+路径输出
PTA题目链接:
https://pintia.cn/problem-sets/15/problems/862
#include<iostream>
using namespace std;
#define MAXSIZE 1000
#define NoEdge 1000
int n,m;
int G[MAXSIZE][MAXSIZE]; //权重1
int Num[MAXSIZE]; //权重2
int dist[MAXSIZE]; //单源点最短路径
int Cnt[MAXSIZE]; //记录最短路径的条数
int Path[MAXSIZE]; //存储最短路径
bool visited[MAXSIZE];
int Que[MAXSIZE]; //权重2最优
int FindMinAdj()
{
int min_i=-1,min=NoEdge; //找离已经有的最短路径结点最近的点
for(int i=0;i<n;i++)
{
if(!visited[i]&&dist[i]<min)
{
min_i=i;min=dist[i];
}
}
return min_i;
}
void Solve(int s,int d)
{
for(int i=0;i<n;i++) //初始化就当做只有s结点到各结点的情况,考虑路径,路径个数等
{
dist[i]=G[s][i];
if(G[s][i]!=NoEdge)
{
Path[i]=s;
Cnt[i]=1;
Que[i]=Num[i]+Num[s];
}
else
{
Cnt[i]=0;
Que[i]=Num[i];
}
visited[i]=false;
}
Cnt[s]=1;
visited[s]=true;
while(true)
{
int min_i=FindMinAdj(); //找离已经确定的最短路径最近的结点
if(min_i==-1) break;
// cout<<min_i<<endl;
visited[min_i]=true;
for(int i=0;i<n;i++)
{
if(!visited[i]&&G[min_i][i]!=NoEdge)
{
if(dist[min_i]+G[min_i][i]<dist[i]) //最短路径优先考虑
{
dist[i]=dist[min_i]+G[min_i][i]; //更新最短路径长度
Que[i]=Que[min_i]+Num[i]; //更新最优权值2
Path[i]=min_i; //更新最短路径
Cnt[i]=Cnt[min_i]; //更新最短路径条数
}
else if(dist[min_i]+G[min_i][i]==dist[i]) //路径长度相等考虑第二个权值
{
Cnt[i]+=Cnt[min_i]; //更新最短路径条数
if(Que[min_i]+Num[i]>Que[i])
{
Que[i]=Que[min_i]+Num[i];
Path[i]=min_i; //更新最短路径
}
}
}
}
}
cout<<Cnt[d]<<" "<<Que[d]<<endl;
int r=Path[d];
cout<<d<<" ";
while(r!=s)
{
cout<<r<<" ";
r=Path[r];
}
cout<<s<<endl;
}
int main()
{
int s,d;
cin>>n>>m>>s>>d;
for(int i=0;i<n;i++)
cin>>Num[i];
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
G[i][j]=NoEdge;
}
for(int i=0;i<m;i++)
{
int v1,v2,len;cin>>v1>>v2>>len;
G[v1][v2]=len;
G[v2][v1]=len;
}
Solve(d,s);
return 0;
}
Floyd算法
求多源最短路径的算法,只能用邻接矩阵实现
- 代码
#include<bits/stdc++.h>
using namespace std;
#define INIFITE 10000
#define MAXSIZE 105
int n,m;
int G[MAXSIZE][MAXSIZE];
int Min_Path[MAXSIZE][MAXSIZE]; //记录i到j的最短路径值,
int path[MAXSIZE][MAXSIZE]; //记录最短路径,递归输出最短路径
bool Floyd() //求任意两点之间的最短距离O(n^3),比对每一个点调用单源点最短路径算法快一点
{
for(int i=1;i<=n;i++) //初始化
{
for(int j=1;j<=n;j++)
{
Min_Path[i][j]=G[i][j];
path[i][j]=j; //初始化i->j一步到位
}
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i==j&&Min_Path[i][j]<0) return false; //发现负值圈,不能正常解决,返回错误标记
if(Min_Path[i][k]+Min_Path[k][j]<Min_Path[i][j])
{
Min_Path[i][j]=Min_Path[i][k]+Min_Path[k][j];
path[i][j]=path[i][k];
}
}
}
}
for(int i=1;i<=n;i++) //输出最短路径值矩阵
{
for(int j=1;j<=n;j++)
{
if(i==j) Min_Path[i][j]=INIFITE;
cout<<Min_Path[i][j]<<" ";
}
cout<<endl;
}
cout<<"****"<<endl;
for(int i=1;i<=n;i++) //输出最短路径矩阵
{
for(int j=1;j<=n;j++)
cout<<path[i][j]<<" ";
cout<<endl;
}
return true;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
G[i][j]=INIFITE;
}
for(int i=0;i<m;i++)
{
int v1,v2,weight;
cin>>v1>>v2>>weight;
G[v1][v2]=weight;
G[v2][v1]=weight;
}
if(Floyd()) cout<<"yes"<<endl; //没有负权值
int v1,v2;cin>>v1>>v2; //输出v1到v2的路径值和路径
while(v1&&v2) //递归输出最短路径
{
cout<<v1<<"到"<<v2<<"的最短路径值为:"<<Min_Path[v1][v2]<<" ";
// cout<<v1<<"-->";
int k=v1; //输出v1到v2的最短路径
while(k!=v2)
{
cout<<k<<"-->";
k=path[k][v2];
}
cout<<v2<<endl;
cin>>v1>>v2;
}
return 0;
}
- 算法复杂度:O(n^3) 无法进行优化
Bellman ford算法
求单源最短路径的算法
时间复杂度高,为O(E*V),比优化后的Dijistra算法差
作用:(1)求带有负权值的图的最短路;(2)检测负权环
bellman ford在dijistra算法不能用时才用,比如存在负权边,如果有负权边,dijistra算法求得的最短路的值是错误的;另外如果有负权环,dijistra算法就会陷入负循环(Negative Cycles)中,因为每次总能找到一个更短的路径,就没法得到正确结果,此时往往需要将其检测出来;
- 基本思想:每次遍历所有的边,对每条边的到达点进行松弛,一共遍历n-1(n为顶点个数)那么多次,双重循环,外层循环为顶点个数-1,内层循环遍历所有的边;
这个遍历的次数i,有特殊的含义,遍历到第i次表示求出来了从1号点经过不超过i条边到达每个点的最短路的距离,第n - 1次更新的最短路径的边数一定是n - 1,第n次如果更新了某条最短路的长度,那么这条最短路上就包含n条边,所以就包含n + 1个点,那么必然会有两个点的编号相同,所以必然存在环,再者如果在第n-1次已经把最短路求出来了,第n次更新的时候最短路径的最小值就不会再更新了,再更新就说明存在负环。
如果存在负权值,第一次操作后(n-1次遍历)能得到基本的最短路径的数组,此时需要重复进行刚才的步骤,判断是否存在负权环,但是在松弛步骤时不进行更新,而是将能进行更新的点的距离值设置为负无穷,表示该点就是负循环中的顶点(负权环或者负权环所影响的那些顶点),并且就表示图中有负权值 - 如果图中存在负权环,也就是一个环的总权值和为负值,并且这个负环还在1->n的路径当中的,那最后求得的1->n的最短路径是不存在的,dist[n]=负无穷。
- 代码
#define INF 0x3f3f3f3f
struct Edge{
int u;//起
int v;//终
int weight;//长度
};
Edge edge[maxm];//用来存储所有的边
int dis[maxn];//dis[i]表示源点到i的最短距离
int n,m;//n个点,m条边
int s;//源点
bool Bellmen_ford()
{
for(int i=1;i<=n;i++)//初始化
dis[i]=INF;
dis[s]=0;//源节点到自己的距离为0
for(int i=1;i<n;i++)//松弛过程,计算最短路径
{
for(int j=0;j<m;j++) //边从下标为0开始存储
{
if(dis[edge[j].v]>dis[edge[j].u]+edge[j].weight)//比较s->v与s->u->v大小
dis[edge[j].v]=dis[edge[j].u]+edge[j].weight;
}
}
for(int j=0;j<m;j++)//判断是否有负边权的边
{
if(dis[edge[j].v]>dis[edge[j].u]+edge[j].weight)
return false; //设置为负无穷,最后数组中为负无穷的顶点就是负循环中的点
}
return true;
}
bellman-ford的思想和dijkstra很像,其关键点都在于不断地对所有边进行松弛,不加以选择。而最大的区别就在于前者能作用于负边权的情况。其检测负权环实现思路是在求出最短路径后,判断此刻是否还能对边进行松弛,如果还能进行松弛,便说明含有负权环。
有边数限制的最短路
SPFA算法
最强大,最快的单源最短路径算法
- 算法思想 :设立一个队列用来保存待优化的点,优化时每次取出队首结点min_i,并且用min_i点当前的最短路径估计值对min_点所指向的结点t进行松弛操作,如果t点的最短路径估计值有所调整,且t点不在当前的队列中,就将t点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
外循环控制出队,内循环控制更新和入队
更新的点不一定会入队(要满足不在队列中) 在队列中的加进去了也没啥事儿,只是会重复很多无用的计算,导致时间增多
已经出队了的点也可能再次入队(满足不在队列中) - 时间复杂度:O(KE),K为常数,平均值为2,如果题目故意卡,就会导致是O(EV)
- 用来算多源最短路径,时间复杂度:O(KE*V)
- 代码
#include<bits/stdc++.h>
using namespace std;
#define MAXSIZE 10005
#define INFI 100000000
int n,m,k;
typedef struct Node
{
int id;
int W;
}AdjNode,*Adj;
vector<Adj>List[MAXSIZE]; //vector数组实现邻接表
int dist[MAXSIZE];
bool inq[MAXSIZE];
queue<int>node;
void Spfa()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) //初始化
{
dist[j]=INFI;
inq[j]=false;
}
dist[i]=0;
node.push(i); //将起点纳入队列中
inq[i]=true;
while(!node.empty())
{
int min_i=node.front(); //得到队列元素并出队
node.pop();
inq[min_i]=false; //元素出队
int len=List[min_i].size();
for(int j=0;j<len;j++) //对该点有影响的邻接点都进行松弛
{
int t=List[min_i][j]->id;
if(dist[t]>dist[min_i]+List[min_i][j]->W)
{
dist[t]=dist[min_i]+List[min_i][j]->W;
if(!inq[t]) //把不在队列里的元素入队
{
node.push(t);
inq[t]=true;
}
}
}
}
for(int j=1;j<=n;j++)
{
cout<<dist[j]<<" ";
}
}
}
int main()
{
//cin>>n>>m>>k;
scanf("%d %d",&n,&m);
for(int i=0;i<m;i++)
{
int u,v,w;
//cin>>u>>v>>w;
scanf("%d %d %d",&u,&v,&w);
if(u==v) continue;
Adj a=new AdjNode; //无向图,两个顶点都要进行操作
a->id=v;
a->W=w;
List[u].push_back(a);
Adj b=new AdjNode;
b->id=u;
b->W=w;
List[v].push_back(b);
}
Spfa();
return 0;
}
检测负权环的方法:跟Bellman-ford算法一致,就是存一下到达当前更新点的最短路的长度,如果超过了n-1,就说明路径中存在环,再者到达当前点路径最小值还在更新,那说明这个环是负环
#include<iostream>
#include<queue>
using namespace std;
int n,m;
const int N=100005;
const int INF=0x3f3f3f3f;
int dist[N];
queue<int>que;
bool in[N];
int cnt[N];
typedef pair<int,int>PII;
vector<PII>adj[N];
bool spfa(int s)
{
for(int i=1;i<=n;i++)
{
dist[i]=INF;
in[i]=true;
que.push(i); //这个地方不能只存储1,因为负环不一定是从1开始的路径上的,还可能跟1不连通
} //因此要把所有的点都存进去
dist[s]=0;
while(!que.empty())
{
int min=que.front();
que.pop();
in[min]=false; //出队列了
for(int i=0;i<adj[min].size();i++)
{
int v=adj[min][i].first;
if(dist[v]>dist[min]+adj[min][i].second)
{
dist[v]=dist[min]+adj[min][i].second;
cnt[v]=cnt[min]+1; //存在超过n-1条边的最短路径,那肯定就存在环,并且这个地方能再次更新说明这个环是负环
if(cnt[v]>=n) return true; //存在负权环
if(!in[v])
{
que.push(v);
in[v]=true;
}
}
}
}
return false;
}
- SPFA算法是Bellman-ford算法的队列优化,比较常用。优化点在于Bellman-ford算法每次对所有的点都进行判断是否可以松弛,但其实在 dist[t]=dist[min_i]+List[min_i][j]->W; 中dist[t]不一定就会被更新,因为他在这个式子中不一定会变小,可以发现只有dist[min_i]变小了,dist[t]才会变小,因此队列里存储的就是之前操作中dist值变小的结点编号,下一次就用它来更新它的邻接结点的dist值。因此队列只是个存储的作用,不一定要用队列,用啥容器都可以,用队列效果比较好
- SPFA算法在负边权图上可以完全取代Bellman-ford算法,另外在稀疏图中也表现良好。但是在非负边权图中,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法,以及它的使用堆优化的版本。通常的SPFA算法在一类网格图中的表现不尽如人意。不是很稳定,不如Dijkstra。
并且使用SPFA算法是严格要求图中不存在负权环的,使用Bellman-ford算法可以存在负权环并且可以检测出来,但是不管什么算法存在负权环都无法求出包含该负权环的最短路径,不包含该负权环的路径还是能求出来的。 - SPFA算法是很快的,一般用Dijkstra能做的正权图它也能做,但是如果出题人卡时间复杂度导致SPFA成了O(nm)就换成优化版的Dijkstra就行。如果包含负权图又要求较低的时间复杂度就只能用SPFA了
经典练习题:csp认证的第五题:
csp-201903
一般图用邻接表的形式存储会比邻接矩阵快
使用算法时要尽量进行优化