在图论中有一个非常常见的问题,就是最短路径的问题。常见的最短路径算法有三种:dijkstra,floyd和SPFA。本文将带你了解前两种最短路算法,他们分别适用于不同场景下的问题。
以下笔记乃是参考西电ACM暑假培训张帆大佬的讲课以及优先队列博客
Dijkstra
首先我们必须了解这个算法的两个局限之处,才能做题
- 不能适用于负权图
- 只适用于单源最短路问题
如果权重是负数,那就直接pass这个算法,在这里原因不表。
单源最短路径就是指只有一个出发点,到其他点的最短路径问题,以下问题也在这个前提下展开。
下面我们就来说说算法流程:
- S1. 设置dis数组,
表示起点start到i的距离。dis[i]
- S2. 从点集V中弹出一个dis值最小且未以该点为起点进行松弛操作的点。
- S3. 从该点松弛与其领接的各个点更新dis数组,返回S2,循环进行。
- 通过优先队列的操作可以优化S2,之后详细说明。
如果这样说,有点抽象。那就举个例子。这个例子也是洛谷的单源最短路径的模板题,请求出1到各点的最短路?
很显然你用肉眼看,1到本身是0,1到2、3、4的最短路分别为2,4,3。那dijkstra的操作流程是什么呢?
首先我们先开一个dis数组,让数组的值足够大,
dis[i]=0x7fffffff
,从1开始出发,令
dis[1]=0
,发现与1相连的有三个点234,那我们一个个进行松弛操作,比较
if (dis[1]+w[i]<dis[i])
,w表示各边的权重,如果小于,那就让其覆盖本身的dis值,即
dis[i]=dis[1]+w[i]
,这一波更新完后,234的值分别为2,5,4。
然后,我们需要让234全部入队,并选取dis值最小的数即2继续进行松弛操作,发现连接的是3和4,继续更新,这波结束,234的值分别为2,4,3。
接着,是上一轮dis值次小的点4,进行操作,但是4没有出的边,所以不进行操作。
最后就是剩下的一个3了,3和4还有一条权边,但是4最小的dis值依旧是3。
下面我们发现算法到这就截止了,为什么呢,因为S2的一句话,未进行松弛的点,早在第一轮234就已经全部进入过队列并且已经弹出过了,所以之后他们也不会再进入队列,我们可以设置一个bool类型的
vis[i]
数组代表第i个点是否被访问过了,如果访问过了就结束此循环,或者直接不push进入队列。
这就是整个dijkstra的算法,其实很好理解,证明我们就先略过了。
优先队列priority_queue
上面那个算法有个问题,就是怎样才能保证每次弹出的都是dis最小的数呢?如果用普通队列,不能解决这个问题,如果每次都遍历一遍来找,那复杂度直接O(VE),百分百会被T掉。
所以我们这里采用优先队列priority_queue对S2进行堆优化,就变成O((V+E)logV)了,所以做题的时候堆优化+dijkstra都是一起出现的,模板也是一起写的。
优先队列:按照一定次序的队列结构,升序或降序,从小到大排列,push和pop都遵循其规律。
优先队列的详细用法请参照这篇博客,本文仅描述dijkstra模板下的用法。
首先对于图论中的
每条点,我们都需建立一个
node
结构体,包含两个内容
num
和
dis
。分别表示该点的序号以及dis值。
struct node{
int num,dis;
};
priority_queue <node> q;
然而这样建立优先队列是会报错的,因为优先队列无法对结构体进行优先级比较。所以我们就想办法告诉程序,我们的队列是对结构体的哪儿个值进行排序,其实做法有很多,我习惯的方式是采取重载的形式。
struct node{
int num,dis;
bool operator < (const node &x)const{
return x.dis < dis; // dis小的结构体在队列中的优先级高
}
};
priority_queue <node> q;
请重载<号,别重载>会报错的。这里如果不理解的就死记硬背把,讲重载的话又要涉及很多知识。
【模板】Dijkstra算法实现
以洛谷模板题为例,P4779 【模板】单源最短路径(标准版)
这道题目的样例就是我上面所举的例子。上面我们解决了算法思路,以及用优先队列的方式对步骤2弹出dis最小的点进行了堆优化,下面我们还需要解决程序细节的问题。
先是存边的问题,这道题不存在重边等为难我们的限制,所以直接构造结构体
edge
。
struct edge{
int to,cost; // to代表下一个点,cost代表该边的权重
};
vector <edge> e[maxn];
之后就是存边,然后进行Dijkstra算法,我直接上代码了,我的代码还是新手向的,不会设计复杂的操作。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const int maxm=2e5+5;
struct edge{
int to,cost;
};
struct node{
int num,dis;
bool operator < (const node &x)const{
return x.dis < dis;
}
};
vector<edge> e[maxm];
int dis[maxn];
bool vis[maxn]; // 布尔型vis数组
int n,m,s,cnt=0;
priority_queue <node> q;
void add_edge(int u,int v,int w) // 存边
{
e[u].push_back((edge){v,w});
}
void dijkstra()
{
dis[s]=0;
q.push((node){s,0});
while (!q.empty())
{
node tmp=q.top();
q.pop();
int x=tmp.num;
if (vis[x]) continue;
vis[x]=1;
for (edge k:e[x]) // 遍历边信息,请注意洛谷请选用c++11,否则编译错误
{
if (dis[x] + k.cost < dis[k.to])
{
dis[k.to]=dis[x]+k.cost;
if (!vis[k.to]) q.push((node){k.to,dis[k.to]});
}
}
}
}
int main()
{
cin>>n>>m>>s;
int i;
for (i=1;i<=n;i++) dis[i]=0x7fffffff;
for (i=0;i<m;i++)
{
int u,v,w;
cin>>u>>v>>w;
add_edge(u,v,w);
}
dijkstra();
for (i=1;i<=n;i++) cout<<dis[i]<<" ";
return 0;
}
P1608 路径统计
题目地址:P1608 路径统计
这题基本也是个Dijkstra的模板题,大方向都没有任何变化。我们首先先考虑一个比较简单的问题,就是题目中的重边该怎么去除,重边我们当然选择最短的那条边,其实也不复杂。我采取的方法是邻接矩阵的办法,再把邻接矩阵转为边信息,考虑到N最大值为2000,二维矩阵肯定不会炸。(忽略奇怪的缩进
for (i=1;i<=m;i++)
{
int p,q,c,tmp;
cin>>p>>q>>c;
if (cmp[i][j]==0 || c<cmp[i][j]) cmp[i][j]=c;
}
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (cmp[i][j]) e[i].push_back((edge){j,cmp[i][j]});
这样就把重边给处理了,菜鸡的我也没想到其他方法了。。。
然后就是来处理本题的问题,最短路径非常简单,但是统计个数,还是需要思索一下的。我的办法是准备一个
ans
数组,
ans[i]
表示第i个点的最短路径的条数,好家伙!这样层层递推,就能推到第N个点了。
这样其实没啥问题,但是有可能统计第k个点,我统计了两条最短路径,突然来了一条更短的,这是我们的
ans[k]
从2又变回了1,好说!让k点的值等于最短路径上一个点的ans值就可以了。
所以核心代码如下。
if (dis[k.to]==dis[x]+k.w) ans[k.to]+=ans[x];
if (dis[k.to]>dis[x]+k.w)
{
dis[k.to]=dis[x]+k.w;
ans[k.to]=ans[x];
q.push((node){dis[k.to],k.to});
}
就是这样如果有相等的路径就相加,有小于的路径就让其ans值回去。如果最后的
dis[n]=0x7fffffff
,那说明根本没路径能到达它,无解。
最后附上ac代码。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const int maxm=maxn*(maxn-1)/2;
struct edge{
int to,w;
};
struct node{
int dis,num;
bool operator < (const node &x)const{
return x.dis<dis;
}
};
vector <edge> e[maxm];
int n,m;
int dis[maxn];
int ans[maxn];
int cmp[maxn][maxn];
bool vis[maxn];
priority_queue <node> q;
void dijkstra()
{
dis[1]=0;
ans[1]=1;
q.push((node){0,1});
while (!q.empty())
{
node tmp=q.top();
q.pop();
int x=tmp.num;
if (vis[x]) continue;
vis[x]=1;
for (edge k : e[x])
{
if (dis[k.to]==dis[x]+k.w) ans[k.to]+=ans[x];
if (dis[k.to]>dis[x]+k.w)
{
dis[k.to]=dis[x]+k.w;
ans[k.to]=ans[x];
q.push((node){dis[k.to],k.to});
}
}
}
}
int main()
{
cin>>n>>m;
int i,j;
for (i=1;i<=n;i++) dis[i]=0x7fffffff;
for (i=1;i<=m;i++)
{
int p,q,c,tmp;
cin>>p>>q>>c;
if (cmp[i][j]==0 || c<cmp[i][j]) cmp[i][j]=c;
}
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (cmp[i][j]) e[i].push_back((edge){j,cmp[i][j]});
dijkstra();
if (dis[n]!=0x7fffffff) cout<<dis[n]<<" "<<ans[n]<<endl;
else cout<<"No answer"<<endl;
return 0;
}
Floyd
著名的Floyd算法,算法的核心代码非常短,时间复杂度。采用邻接矩阵
arr[i][j]
的方式存图,三层循环不可调换。具体原理可以用dp来解释,在此不表。直接上核心代码。
for (int k=1;k<=N;k++)
for (int i=1;i<=N;i++)
for (int j=1;j<=N;j++)
arr[i][j]=min(arr[i][j],arr[i][k]+arr[k][j]);
Floyd算法还需要看看练习题:P1119 灾后重建
也可以看看我关与此题的题解,见个人博客或知乎专栏