最短路问题
-
单源最短路
从1号点到其他点的最短路
- 所有边的权值都是正数,这样的最短路
- 朴素版的Dijkstra算法 O(n^2)(稠密图尽量使用朴素版本的Dijkstra算法)
- 堆优化版的Dijkstra算法 o(mlogn)
- 存在负权值的边
- Bellman-Ford o(nm)(经过的边数小于等于K)
- SPFA 平均时间复杂度是线性的。(一般情况下)
- 所有边的权值都是正数,这样的最短路
-
多源最短路 Floyd算法 o(n^3)
-
最短路算法的考点:建图。如何把原来的问题抽象成一个最短路的问题。
朴素版的Dijkstra算法:
step1: dis[1]=0,dis[i]=无穷 S表示的是当前以及确定的最短距离的点。
step2:for i = 0~n:其中t表示不在S中的。距离最近的点。
step3:把这个t假如到s中去
step4:用t来更新其他所有的点。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int n;
int m;
const int N = 510;
int g[N][N];
int dis[N];
int s[N];
int dijkstra(){
for(int i=0;i<n;++i){
int t=-1;
for(int j=1;j<=n;++j){
if(!s[j]&&(t==-1||dis[j]<dis[t])){
t=j;
}
}
s[t]=1;
for(int j = 1;j<=n;++j){
dis[j]=min(dis[j],dis[t]+g[t][j]);
}
}
if(dis[n]==0x3f3f3f3f)return -1;
else return dis[n];
}
int main(){
memset(dis,0x3f,sizeof dis);
dis[1]=0;
memset(g,0x3f,sizeof g);
cin>>n>>m;
for(int i=0;i<m;++i){
int a;
int b;
cin>>a>>b;
int c;
cin>>c;
g[a][b]=min(g[a][b],c);
}
cout<<dijkstra()<<endl;
return 0;
}
bellman-ford算法
算法思路:
bellmanford算法主要是基于边的。对于K个顶点的图,要进行K-1次更新。这里我们如果对k赋予一个含义的话,那就是从起点到各个顶点经过最多k条边的最短路径。这种情况下就要使用到bellman-ford算法,
这里值得注意的是,如果我们赋予了k这样的含义,那么每次用来更新的数组就应该是我们保留了的上一个数组,不能直接在更新后的数组上进行操作,否则会出现串链的问题。
例题:acwing853
以下代码的输入是n,m,k
n表示顶点个数
m表示边数
k表示最多经过的边数,
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
int m;
int k;
const int N = 100010;
int dis[N];
int last[N];
struct edge{
int a;
int b;
int w;
}Edge[N];
void BellmFord(){
for(int i=0;i<k;++i){
memcpy(last,dis,sizeof dis);
for(int j=1;j<=m;++j){
edge e = Edge[j];
dis[e.b]=min(dis[e.b],last[e.a]+e.w);
}
}
}
int main(){
memset(dis,0x3f,sizeof dis);
dis[1]=0;
cin>>n>>m>>k;
for(int i=1;i<=m;++i){
int a;
int b;
int w;
cin>>a>>b>>w;
Edge[i].a=a;
Edge[i].b=b;
Edge[i].w=w;
}
BellmFord();
if(dis[n]>0x3f3f3f3f/2)cout<<"impossible"<<endl;
else cout<<dis[n]<<endl;
return 0;
}
SPFA算法
根据bellman-ford算法,会出现这样的一个问题。那就是说每次都要对所有的点进行一次更新,显然只有当前点被更行后,那他后续的节点才需要被更新,否则这样的更新是没意义的。争对这个问题,spfa算法引进了一个队列来保存当前更新过后的点。用更新过后的点来更新其后面的点。代码如下:
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
int n;
int m;
//这里的idx指的是边的编号。
int idx=1;
const int N =100010;
int dis[N];
int head[N];
int ne[N];
int e[N];
int val[N];
queue<int>q;
void add(int a,int b,int c){
e[idx]=b;
val[idx]=c;
ne[idx]=head[a];
head[a]=idx++;
}
int s[N];
int spfa(){
while(!q.empty()){
//cout<<"死循环了吗"<<endl;
int p = q.front();
q.pop();
s[p]=0;
for(int i = head[p];i!=-1;i=ne[i]){
int j = e[i];
if(dis[j]>dis[p]+val[i]){
dis[j]=dis[p]+val[i];
if(!s[j])
{
q.push(j);
s[j]=1;
}
}
}
}
return dis[n];
}
int main(){
memset(dis,0x3f,sizeof dis);
memset(head,-1,sizeof head);
// s[1]=1;
dis[1]=0;
cin>>n;
cin>>m;
for(int i=0;i<m;++i){
int a;
int b;
cin>>a>>b;
int c;
cin>>c;
add(a,b,c);
}
q.push(1);
int t = spfa();
if(t==0x3f3f3f3f){
cout<<"impossible"<<endl;
}else{
cout<<t<<endl;
}
return 0;
}
SPFA求负权回路
bellman-ford算法和spfa都可以用来求负权回路。基本思想如下:如果从A到B之间的最短距离在第k次遍历得到更新,说明有一条从A到B的最短距离只需要经过k条边。如果第n次(n为节点数) 更新后,仍然有节点被更新了,那么说明有环,且是负环。(因为n个节点最多有n-1个边,否则就有环,有环的情况下如果最短路得到了更新,那一定是有负环)
这里开辟一个数组,用于记录从起点到当前点需要经过的边数。当这个边数大于等于n的时候,就说明一定有负权回路。
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
int n;
int m;
//这里的idx指的是边的编号。
int idx=1;
const int N =300010;
int dis[N];
int head[N];
int ne[N];
int e[N];
int val[N];
int cnt[N];
queue<int>q;
void add(int a,int b,int c){
e[idx]=b;
val[idx]=c;
ne[idx]=head[a];
head[a]=idx++;
}
int s[N];
int spfa(){
while(!q.empty()){
//cout<<"死循环了吗"<<endl;
int p = q.front();
q.pop();
s[p]=0;
for(int i = head[p];i!=-1;i=ne[i]){
int j = e[i];
if(dis[j]>dis[p]+val[i]){
dis[j]=dis[p]+val[i];
cnt[j]=cnt[p]+1;
if(cnt[j]>=n){
return 1;
}
if(!s[j])
{
q.push(j);
s[j]=1;
}
}
}
}
return 0;
}
int main(){
memset(dis,0x3f,sizeof dis);
memset(head,-1,sizeof head);
dis[1]=0;
cin>>n;
cin>>m;
for(int i =1;i<=n;++i){
q.push(i);
}
for(int i=0;i<m;++i){
int a;
int b;
cin>>a>>b;
int c;
cin>>c;
add(a,b,c);
}
q.push(1);
if(spfa()){
cout<<"Yes"<<endl;
} else{
cout<<"No"<<endl;
}
return 0;
}
弗洛伊德算法求最短路径(多源最短路径)
考研党都明白的哈。k是中间节点,dij可以看成dik+dkj
这里有点基于动态规划的意思。dkij的含义为:从i出发,只经过从1~k这几个节点中的任意一个点作为中间点,到j的最短距离。
dp的讨论为:加入k和不加k的最小值。(具体等我把dp问题学完了在单独做一个笔记)
代码如下:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
int m;
int k;
const int N = 210;
int dis[N][N];
void Floyd(){
for(int k=1;k<=n;++k){
for(int i = 1;i<=n;++i){
for(int j =1;j<=n;++j){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
}
int main(){
cin>>n>>m>>k;
memset(dis,0x3f,sizeof dis);
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
if(i==j)
dis[i][j]=0;
}
}
for(int i = 0;i<m;++i){
int a;
int b;
int c;
cin>>a>>b>>c;
dis[a][b]=min(dis[a][b],c);
}
Floyd();
for(int i=0;i<k;++i){
int a;
int b;
cin>>a>>b;
if(dis[a][b]>0x3f3f3f3f/2)cout<<"impossible"<<endl;
else cout<<dis[a][b]<<endl;
}
return 0;
}