最短路问题
写在最前面 因为最近学到了数据结构最短路 因此整理出笔记 以供自己
未来复习
问题抽象: 求任意两个不同顶点之间的所有路径中, 边的权值之和最小的那一条路径。这条路径称为最短路径。起点是源点 最后一个点称为终点。
一套问题分类 [在这里不分图是否有向]
- 单源最短路: 从固定的一个顶点出发到达其他所有顶点的最短
- 无权图
- 带权图
- 多源最短路:求任意两个顶点之间的最短路
稠密图用邻接矩阵 稀疏图用邻接表
可以参考此博客 彻底搞懂最短路.
无权图的单源最短路 [路径结点个数最少]
流程: 按照非递减的顺序 找到各个顶点的最短路------->BFS
时间复杂度: O(v+e)
函数部分代码:
using namespace std;
const int N=100010;
int h[N], ne[N], e[N], idx; //数组实现链表
void add(int a, int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx ++;
}
int path[N]; //记录路径上的结点
int dist[N]; //记录路径的长度
queue<int> q; //队列存储已经遍历过的顶点
void Unweighted(int n){ //从n顶点出发
//先初始化
memset(path, -1, sizeof path);
memset(dist, -1, sizeof dist);
memset(h, -1, sizeof h) ;
//预处理
dist[n]=0; //源点到自己是0
q.push(n); //n遍历过了 进队
while(q.size()){ //队列不空时
int t=q.front();
q.pop();
for(int i=h[t];i!=-1;i=ne[i]){ //遍历与i这个顶点相连的顶点
int j=e[i];
if(dist[j]==-1){ //如果还没有被访问过
dist[j]=dist[i]+1;
path[j]=i; //标记是从i来到j
q.push(j);
}
}
}
}
有权图的最短路 [路径权重最小]
负环: 这个环的权重是负数 可以无限循环得到负无穷
dijstar算法: 无法解决有负边的情况
两种方法实现找到最小距离:
- 直接扫描 O(V^2+E) 稠密图
- 使用一个最小堆 O(ElogV) 稀疏图
稠密图: dijstar1 计算1到n的最短路长度
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510;
//初始化:起点到起点距离是0 其余为正无穷
//st保存以确定的最短距离 t保存剩下的点的距离
//每次找到t中最小值加入s 并用t去更新每个点到起点的距离
int g[N][N]; //保存邻接矩阵
int dist[N]; //保存每个点到1的距离
bool st[N]; //判断点是否已经确定最短距离
int n, m;
int dijstar(){
//先初始化dist数组
memset(dist, 0x3f, sizeof dist);
dist[1]=0; //1到自己是0;
//外循环n-1次 可以每次都找到一个点的最短距离
for(int i=0;i<n;i++){
int t=0;
//找到所有还未确定最短距离中最短的那个距离对应的点j
for(int j=1;j<=n;j++){
//条件是 还没确定最短距离 并且比dist[t]短
if(!st[j]&&(t==0||dist[t]>dist[j]))
t=j;
}
//找到t后 相应的改变st[t];
st[t]=true;
//再由这个最短距离 去更新每个点到原点的最短距离
for(int j=1;j<=n;j++) {
dist[j]=min(dist[j], dist[t]+g[t][j]);
}
}
//表明不通
if(dist[n]==0x3f3f3f) return -1;
return dist[n];
}
int main(){
//n个点 m条边
scanf("%d%d", &n, &m);
//因为存在 重边 所以在 输入邻接矩阵前将每个边无穷化 方便取最小值
memset(g, 0x3f, sizeof g);
while(m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b]=min(g[a][b], c);
}
int t=dijstar() ;
printf("%d", t);
return 0;
}
堆优化版(稀疏图):
1.使用小根堆 存边权
2.每次从这个点只需要遍历与它相邻的边–>使用单链表
3.二话不说 代码如下
1.创建变量和头文件
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> pii;
const int N=1000;
//构建邻接表
int n, m;
//邻接表表头h, e为每一个结点保存的号码
//w为此结点存的号码与对应的表头之间的距离就是边权
int h[N], idx, e[N], w[N], ne[N];
bool st[N]; //代表是否已经确定了
int d[N]; //表距离
2.默写单链表
void add(int a, int b, int x){
e[idx]=b; w[idx]=x;
ne[idx]=h[a], h[a]=idx ++;
}
3.假设dijstar算法已经实现 写main函数读入所有数据
int main(){
//一定要记得表头初始化
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
while(m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t=dijstar();
printf("%d\n", t);
return 0;
}
4.重要的dijstar算法
小根堆的定义方法:priority_queue<pii, vector<pii>, greater<pii>>
int dijstar(){
//先将距离取无穷
memset(d, 0x3f, sizeof d);
d[1]=0;
//定义一个小根堆
priority_queue<pii, vector<pii>, greater<pii>> heap;
heap.push({0, 1}); //以距离排序 所以距离在第一位 点坐标在第二位
while(heap.size()){
pii t=heap.top(); //=取最小的那个距离
heap.pop(); //删掉
//num是号码 dis是距离
int num=t.second, dis=t.first;
//如果这个边已经走过了 就直接下一步 以达到去重的效果
//同是 不会判断冗余:已经判断过的
if(st[num]) continue;
st[num]=true;
//遍历这个num所链接的边
for(int i=h[num];i!=-1;i=ne[i]){
//得到此节点号码
int j=e[i];
//判断一下 是原来的距离小 还是从起点经过num再到 j的距离小
if(d[j]>dis+w[i]){
d[j]=dis+w[i];
heap.push({d[j], j});
}
}
}
if(d[n]==0x3f3f3f3f) return -1;
return d[n];
}
5.完整代码
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> pii;
const int N=1e6;
int h[N], e[N], w[N], ne[N], idx;
int d[N];
bool st[N];
int n, m;
void add(int a, int b, int x){
e[idx]=b, w[idx]=x;
ne[idx]=h[a], h[a]=idx++;
}
int dijstar(){
memset(d, 0x3f, sizeof d);
d[1]=0;
priority_queue<pii, vector<pii>, greater<pii>> q;
q.push({0, 1});
while(q.size()){
pii k=q.top();
q.pop();
int num=k.second, dis=k.first;
if(st[num]) continue;
st[num]=true;
for(int i=h[num];i!=-1;i=ne[i]){
int j=e[i];
if(d[j]>dis+w[i]){
d[j]=dis+w[i];
q.push({d[j], j});
}
}
}
if(d[n]==0x3f3f3f3f) return -1;
return d[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while(m--){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
int t=dijstar();
printf("%d", t);
return 0;
}
spfa算法 可以解决带负权边的图
(最短路推荐算法)
若是一个没有负权稠密图 用dijstar堆优化
SPFA:
是对bell慢的一个优化 基于每一次迭代 每个边未必会变小。源点s到达其他的点的最短路径中的第一条边,必定是源点s与s的邻接点相连的边,因此,第一次松弛,我们只需要将这些边松弛一下即可。第二条边必定是第一次松弛的时候的邻接点与这些邻接点的邻接点相连的边(这句话是SPFA算法的关键,请读者自行体会),因此我们可以这样进行优化:设置一个队列,初始的时候将源点s放入,然后s出队,松弛s与其邻接点相连的边,将松弛成功的点放入队列中,然后再次取出队列中的点,松弛该点与该点的邻接点相连的边,如果松弛成功,看这个邻接点是否在队列中,没有则进入,有则不管。
!为什么在队列中存在就不继续放入队列
这里要说明一下,如果发现某点u的邻接点v已经在队列中,那么将点v再次放到队列中是没有意义的。因为即时你不放入队列中,点v的邻接点相连的边也会被松弛,只有松弛成功的边相连的邻接点,且这个点没有在队列中,这时候稍后对其进行松弛才有意义。因为该点已经更新,需要重新松弛。
算法实现
1.先把起点放入队列(队列(存所有距离变小的结点 这样经过此节点往后的结点都有变小的可能))
2.取出队头t
3.遍历从队头t出发的边(邻接表) 再跟新距离
4.更新成功再放入队
5.直到队列为空
步骤
1.写变量
const int N=1e6;
//链表必备
int h[N], w[N], e[N], ne[N], idx;
int d[N]; //距离
bool st[N]; //是否在队列中
int n, m;
queue<int> q; //队列
2.默写链表
void add(int a, int b, int x){
e[idx]=b, w[idx]=x;
ne[idx]=h[a], h[a]=idx++;
}
3.写main函数 输入数据
int main(){
cin>>n>>m;
//一定要初始化链表
memset(h, -1, sizeof h);
while(m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t=spfa();
if(t==-1) puts("impossible");
else cout<<t<<endl;
return 0;
}
4.spfa函数
进队 出队及时改变st的bool值
int spfa(){
//距离初始化
memset(d, 0x3f, sizeof d);
//把1放入队列 并标记
d[1]=0;
q.push(1);
st[1]=true;
while(q.size()){
int k=q.front();
q.pop();
//队头从队列中出来 就标记不在
st[k]=false;
//遍历队头的临边
for(int i=h[k];i!=-1;i=ne[i]){
int j=e[i];
//只有跟新距离的才有可能放入队列
if(d[j]>d[k]+w[i]){
d[j]=d[k]+w[i];
//如果不在队中 就入队
if(!st[j]) {
q.push(j);
st[j]=true;
}
}
}
}
if(d[n]==0x3f3f3f3f) return -1;
return d[n];
}
5.全部代码
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e6;
//链表必备
int h[N], w[N], e[N], ne[N], idx;
int d[N]; //距离
bool st[N]; //是否在队列中
int n, m;
queue<int> q; //队列
void add(int a, int b, int x){
e[idx]=b, w[idx]=x;
ne[idx]=h[a], h[a]=idx++;
}
int spfa(){
//距离初始化
memset(d, 0x3f, sizeof d);
//把1放入队列 并标记
d[1]=0;
q.push(1);
st[1]=true;
while(q.size()){
int k=q.front();
q.pop();
//队头从队列中出来 就标记不在
st[k]=false;
//遍历队头的临边
for(int i=h[k];i!=-1;i=ne[i]){
int j=e[i];
//只有跟新距离的才有可能放入队列
if(d[j]>d[k]+w[i]){
d[j]=d[k]+w[i];
//如果不在队中 就入队
if(!st[j]) {
q.push(j);
st[j]=true;
}
}
}
}
if(d[n]==0x3f3f3f3f) return -1;
return d[n];
}
int main(){
cin>>n>>m;
memset(h, -1, sizeof h);
while(m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t=spfa();
if(t==-1) puts("impossible");
else cout<<t<<endl;
return 0;
}
多源最短路算法
floyd算法 O(v^3) 三重循环
基于dp
但有一种通俗的理解:
我们假设dist(ab)为节点a到节点b的最短路径的距离,对于每一个节点K,我们检查dist(aK) + dist(Kb) < dist(ab)是否成立,如果成立,证明从a到K再到b的路径比a直接到b的路径短,我们便设置 dist(ab) = dist(aK) + dist(Kb),这样一来,当我们遍历完所有节点K,dist(ab)中记录的便是a到b的最短路径的距离。
!注意 k一定是最外层循环
步骤:
1.写变量
const int N=220, inf=1e9;
int d[N][N]; //a b之间的距离
int n, m, q; //n个点 m条边 q次询问
2.main函数做输入 初始化
int main(){
scanf("%d%d%d", &n, &m, &q);
//初始化 自己到自己距离是0 其余是无穷 为了保存重边中最短的那个边
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, w;
scanf("%d%d%d", &a, &b, &w);
d[a][b]=min(d[a][b], w);
}
floryd();
while(q--){
int a, b;
scanf("%d%d", &a, &b);
//因为有负权的存在 所以当距离很大的时候 就认为impossible
if(d[a][b]>inf/2) puts("impossible");
else printf("%d\n", d[a][b]);
}
return 0;
}
3.floyd函数
void floryd(){
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][k]+d[k][j], d[i][j]);
}
做题
构建main框架:
- 读入图。 分析是邻接矩阵 or 邻接表
- 分析图 。 分析用什么最短路。