下面是我看acwing及一些博客之后乱七八糟的整理(主要参考yls)
是写给自己看的笔记 想要系统学习这一块的还是建议移步acwing
最短路问题存在着很多种情况 每一种情况都一定有它最好的选择 图里就归纳得很清楚了
这里值得注意的是 虽然是叫朴素Dijkstra算法 但是不是就比堆优化版的不好 只是分别适用于不同的情况而已
目录
图的存储
稀疏图用邻接表 稠密图用邻接矩阵
边数默认为m 顶点数默认为n
当m~n^2时 属于稠密图
当m~n时 属于稀疏图
朴素Dijkstra算法
//是用自己的话总结的 到时候看看数据结构再改一下
(一个for循环里面包含着一个双重循环)
1.先做好初始化 dis[ ]数组及g[ ][ ] 都初始化为正无穷 源点距离为0
2.做n次迭代 每次都找到没有确定为最短路并且dis[ ]最小的点 t 再用 t 更新其他点的距离
代码模板:Dijkstra求最短路
#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
bool st[N];//标记有没有是否已经确定是最短路
int g[N][N];//因为题里的信息很明显是稠密图 所以用邻接矩阵
int dis[N];//记录各点到源点的距离
int m,n;
int dijkstra()
{
memset(dis,0x3f,sizeof dis);//先把距离初始化为无穷大
dis[1]=0;//起点距离自己的距离为0
for(int i=0;i<n;++i)//迭代n次 每次确定一个点为最短路
{
int t=-1;//t存储当前路最短的点
for(int j=1;j<=n;++j)//这个循环是遍历n个点 找到当前最短距离
{
if(!st[j]&&(t==-1||dis[j]<dis[t]))//如果发现更短的路径 就进行更新
t=j;
}
st[t]=true;//该点最短距离确定
for(int j=1;j<=n;++j)//用当前距离最小的点t更新其他点到起点的距离
dis[j]=min(dis[j],dis[t]+g[t][j]);
}
if(dis[n]==0x3f3f3f3f)//如果到达不了n号节点
return -1;
else
return dis[n];
}
int main()
{
using namespace std;
int a,b,c;
memset(g,0x3f,sizeof g);
cin>>n>>m;
while(m--)
{
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);//因为可能存在重边 所以要判断一下 取最小
}
cout<<dijkstra()<<endl;
}
注意:
1.memset 按字节赋值 memset 0x3f 就等价与赋值为0x3f3f3f3f 所以memset用0x3f 在最后判断 dis[ ]的时候要判断是否==0x3f3f3f3f(一开始因为这里找了好久的错误)
2.这里是一个for循环里有两个for循环 注意花括号的位置 写的时候不要搞混了
堆优化版的Dijkstra算法
由于朴素Dijkstra算法每次都要循环一遍 找dis[ ]最小的值 只是需要找最小的值 没有必要每次都遍历一遍dis数组 在一组数中很快地找到最小值 就能想到小根堆 可以用STL中的优先队列 也可以自己写一个堆 但比较麻烦
先学下优先队列:
它的定义:priority_queue<Type, Container, Functional>
有些抽象了 不过目前我直接会用就行
升序队列:priority_queue <int,vector<int>,greater<int> > q;
降序队列:priority_queue <int,vector<int>,less<int> >q;
不过有些参数可以直接省略 比如STL默认第二个参数为vector 第三个默认为大根堆
priority_queue<int> q;//基础类型默认为大根堆 即降序队列
如果是pair 先比较第一个元素 第一个相等再比较第二个
它的其他函数与队列基本操作相同:
- top 访问队头元素
- empty 队列是否为空
- size 返回队列内元素个数
- push 插入元素到队尾 (并排序)
- emplace 原地构造一个元素并插入队列
- pop 弹出队头元素
- swap 交换内容
优先队列在这个算法里,存储的是各点编号及他们到源点的距离,因为这两个肯定是要捆绑在一起,所以要用到pair,又因为我们每次是要到源点距离最小的那个点 (pair优先比较第一个值),所以我们要把距离放在第一个
值得注意的是 在这里每次更新值之后,每次更新的值都会被放入队列,原先的值不能删掉(自建堆能解决这个问题 不过自建堆有点麻烦)
所以我们需要利用好st[ ]数组 在一开始的时候 如果st[ ]已经标记过这个点了 把该点出队之后就可以直接continue了
大概了解这些 来看看大概的思路
1.初始化g[ ]和dis[ ] 并把第一个点入队
2.如果队非空就取队头元素(因为是小根堆 所以该元素一定是当前路径最短的点)如果该点已经确认为最短路,就continue 看下一个 如果没有 st[ ]置为true 然后用该点去更新其他与它相邻的所有点 并把这些点也入队
3.最后如果dis[n]没有被更新过 仍然为正无穷 说明源点到不了n点 否则就返回dis[n]
模板题: Dijkstra求最短路 II
代码:
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int N=1e6;
int n,m;
bool st[N];
int h[N],w[N],e[N],ne[N],idx;
int dis[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra()
{
priority_queue<PII,vector<PII>,greater<PII>>heap;//定义优先队列 小根堆
memset(dis,0x3f,sizeof dis);//初始化所有点到源点的距离为正无穷
dis[1]=0;//记得每次更新 既要修改数组dis 又要入队
heap.push({0,1});//插入距离和顶点编号(这里不是结点编号)
while(!heap.empty())
{
PII t=heap.top();//取队头元素(即最小值)
heap.pop();
int distance=t.first;//源点到ver的距离
int ver=t.second;//顶点
if(st[ver])//如果已经标记为最短路了
continue;//下一个循环
st[ver]=true;
if(ver==n)//如果源点到n已经是最短距离 直接break就好了
break;
for(int i=h[ver];i!=-1;i=ne[i])//用当前最短距离的点ver更新所指向的结点距离
{
int j=e[i];
dis[j]=min(dis[j],dis[ver]+w[i]);//每次修改都要进行两个操作
heap.push({dis[j],j});
}
}
if(dis[n]==0x3f3f3f3f)
return -1;
return dis[n];
}
int main()
{
int a,b,c;
memset(h,-1,sizeof(h));
cin>>n>>m;
while(m--)
{
cin>>a>>b>>c;
add(a,b,c);
}
cout<<dijkstra()<<endl;
}
bellman_ford算法
此算法适用于单源最短路中存在负权边并且限制要走k条路的情况
其实这个代码十分简洁
大概流程:(一个双重for循环)
1.先初始化dist数组为正无穷 然后做k次循环(因为限制了只能走k条路)
2.每次外层循环内都要遍历每一条边 更新值 在此之前要备份一下dis[ ]数组到backup数组[ ]里
有点说不清 先放下代码:有边数限制的最短路
#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
const int M=10010;
int n,m,k;
int backup[M];//备份上一次的数据以防串联
int dis[M];
struct Edge
{
int a,b,w;
}edge[M];//把每条边都保存下来
int Bellman_ford()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=1;i<=k;++i)//最多走k条边
{
memcpy(backup,dis,sizeof dis);//备份上一次的数据
for(int j=0;j<m;++j)
{
int a=edge[j].a;
int b=edge[j].b;
int w=edge[j].w;
dis[b]=min(dis[b],backup[a]+w);//这个叫做松弛操作
//避免刚更新完a又有用新的数据立马去更新b 这样就相当于走了两条边了
}
}
if(dis[n]>0x3f3f3f3f/2)
return -1;
else
return dis[n];
}
int main()
{
cin>>n>>m>>k;
int a,b,c;
for(int i=0;i<m;++i)
{
cin>>a>>b>>c;
edge[i]={a,b,c};
}
int ans=Bellman_ford();
if(ans==-1)
puts("impossible");
else
cout<<ans<<endl;
}
Q1:什么是串联?
A1:就是在一个外层for循环内 更新一个距离之后又用这个刚刚更新的值再去更新一个点 这样就相当于走了两条边
Q2:backup[ ]数组的作用?
A2:为了防止串联的发生 这样就可以用原本的上一次的数据去更新了
Q3:为什么子函数里最后要判断dis[n]>0x3f3f3f3f/2 不是直接等于0x3f3f3f3f?
A3:因为0x3f3f3f3f并不是真的无穷大 可能存在负权边的情况 更新值的时候有可能得到比如10^9-10这样的情况 但是其实它在k条路之内并没有和源点连通
Q4:关于边的存储
A4:用y总的话来说 “这个算法就比较牛逼了 随便存 随便存 只要每次循环都能遍历到所有边就可以了“
SPFA算法
上面的Bellman_ford算法每次都会遍历所有的边 但是很多边遍历了其实没什么意义 只有当一个点的前驱结点更新了 该节点才会得到更新 所以我们可以创一个队列存储每一次距离被更新的结点 出队的时候就更新该结点的所有后驱结点
这个算法与Dijkstra算法很像 但是还是注意区分开 这里的st[ ]数组标记的是该点有没有在队列中 在这里的意思是该点有没有被更新过 它的变换是可逆的 但是dijkstra算法中st[ ]是不可逆的 st标为true之后就不能再更改 它已经确认为最短路了
用spfa算法在大多数情况下其实也可以解决dijkstra算法解决的问题并且时间会比dijkstra算法快很多 但是有些出题人可能会故意卡spfa spfa的最坏情况的时间复杂度高达O(nm)
代码: spfa求最短路
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
bool st[N];
int dis[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa()
{
memset(dis,0x3f,sizeof dis);
queue<int>q;
dis[1]=0;
q.push(1);
st[1]=true;
while(!q.empty())
{
int t=q.front();
q.pop();
st[t]=false;//从队列中取出来之后该节点st被标记为false 之后该节点如果发生更新可再次入队
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dis[j]>dis[t]+w[i])
{
dis[j]=dis[t]+w[i];
if(!st[j])//当前已经入队的结点无需再次加入队 即便发生了更新也只用更新数值即可 重复添加降低效率且没有必要
{
q.push(j);
st[j]=true;
}
}
}
}
if(dis[n]==0x3f3f3f3f)
return -1;
else
return dis[n];
}
int main()
{
int a,b,c;
memset(h,-1,sizeof h);
cin>>n>>m;
while(m--)
{
cin>>a>>b>>c;
add(a,b,c);
}
int t=spfa();
if(t==-1)
puts("impossible");
else
printf("%d\n",t);
}
这个算法也能判断是否存在负权回路(Bellman_ford算法也可以 但是效率太低 一般不用它)
我们可以开一个cnt[ ]数组记录最短路经过的边的条数 如果经过边数大于等于点数n 根据抽屉原理 说明一定存在负权回路
代码:spfa判断负环
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
bool st[N];
int dis[N];
int cnt[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool spfa()
{
queue<int> q;
for(int i=1;i<=n;++i)//因为负权回路不一定是从源点出发会经过的路 所以需要每个点都要走一遍
q.push(i);
while(!q.empty())
{
int t=q.front();
q.pop();
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dis[j]>dis[t]+w[i])
{
dis[j]=dis[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>=n)
return true;
q.push(j);
}
}
}
return false;
}
int main()
{
int a,b,c;
memset(h,-1,sizeof h);
cin>>n>>m;
while(m--)
{
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa())
puts("Yes");
else
puts("No");
return 0;
}
Q1:为什么不用初始化dis[ ]?
A1:如果存在负环,不管dis[ ]初始化为多少,都会被不断更新,所以dis初值对结果没有影响
Q2:为什么去掉了st[ ]的判断
A2:因为判负环的原理是看cnt数组是否大于n,cnt更新的前提是当前点更新,而队列的存在让我不能够很快的访问同一个点,当去掉判断以后,很快我就能重复遇到相邻的点,这样能够快速增加cnt的值,所以会快很多。(——Aku)
Floyd算法
其实不是太能理解核心代码 或者说是不太理解动态规划 但是好在比较好记
大概步骤
1.初始化d[ ]数组
2.三重循环k i j 更新d[ ]
代码:Floyd求最短路
#include<iostream>
#include<cstring>
using namespace std;
const int N=210;
const int inf=0x3f3f3f3f;
int d[N][N];
int m,n;
void Floyd()
{
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);//基于动态规划
}
int main()
{
int k;
cin>>n>>m>>k;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=n;++j)
{
if(i==j)
d[i][j]=0;//因为存在自环
else
d[i][j]=inf;
}
}
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
d[a][b]=min(d[a][b],c);//因为存在重边
}
Floyd();
while(k--)
{
int a,b;
cin>>a>>b;
if(d[a][b]>inf/2)//因为存在负权边 所以这里要大于inf/2
puts("impossible");
else
cout<<d[a][b]<<endl;
}
}
至此
终于学完最短路所有模板题
没有学完 只能说看完 继续努力