B3602 [图论与代数结构 202] 最短路问题_2 题解
BlachSnake · · 题解
2021/8/29 Update:修改了题解中的两处错误,并在“链式前向星”部分加上了部分注释、增加了建边函数
1.题意
给出一张有
2.单源最短路问题
给定一个源点(此题中为
接下来先考虑怎么把这个有向图存下来。
用二维数组(设
所以就有了这个东西——
3.链式前向星
链式前向星,是一种利用单向链表存图的数据结构。
那么它长什么样呢?
其实它就是
然后把每个链表的头部用一个 head 数组存起来,如果要遍历某个点开始的所有边,直接写
for(int i=head[x];i;i=next[i])//遍历每一条边
就行了。
那么如何加边呢?
很简单,只需要在链表的头部插入,就像这样:
next[++t]=head[x];//记录后继
to[t]=y;//记录这条边指向的地方
weight[t]=z;//记录边权
head[x]=t;//更新链表头部
存图的问题是解决了,但怎么求最短路呢?
如果用 Dfs 暴搜的话,时间复杂度为
用 Floyd(其实就是 DP)来做的话,时间复杂度为
于是,就有了这么一个算法:
4.Dijkstra 求最短路
Dijkstra 单源最短路算法,是一种用贪心实现的最短路算法,只能在边权值均为正的情况下运行。至于为什么只能在边权值为正的情况下运行,等一下会提到。
这里我们用一个
先来看一下算法流程:
初始化,把
找到
这里我把
至于为什么不再更新这个点的 (这句话你不已经说过一遍了吗……)
点
点
找到
点
点
找到
找到 然而并没有更新到任何点的 。
找到 但仍然没有更新到任何点的 。
另外点
找到 结果还是没有更新到任何点的 。
所有的点都已经被打上了标记,算法结束。
为什么这个算法是正确的呢?
因为你把一个点打上标记后,如果这条路不是最短路,那就必须得从其他的地方绕过来,而这样求出来的路的长度一定比用 Dijkstra 求出来的路要长。
除非这条路上有负边权,不然你怎样也不可能让这条路变成最短路。
这就是为什么 Dijkstra 不能跑负权图的原因。(滑稽)
但是题目规定
好的,然后我们就可以开开心心地 AC 了这道题……
了吗?
当然不。
由于 Dijkstra 算法查找
那么,怎么优化这个算法呢?
5.Dijkstra算法的优化
注意每次查找最小值的时候时间复杂度都是
那么我们每次就把这些被更新的点扔进某一个神奇数据结构里,之后只要对着这个神奇数据结构问一发:此时
那么问题来了这个神奇数据结构叫什么呢?
二叉堆啊!
当把符合要求的那个点弹出堆后,就把其他更新后的点扔进堆中;
由于扔进去的点的
时间复杂度为
至于堆的写法可以参考一下P3378的题解。
6.Code
终于可以上代码了……
不过我这里自己手写了一个堆和一个 pair,实际上也可以用 STL 的 priority_queue,但我不想用……
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
using namespace std;
const int N=300003,M=5555555;
template<class T>inline void swap(T &x,T &y){T z=x;x=y,y=z;}//交换函数
namespace io{
inline int Read(){//快速读入数字
int x=0;
bool d=0;
char c=getchar();
for(;!isdigit(c);c=getchar())if(c=='-')d=1;
for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+(c^48);
if(d)x=~x+1;
return x;
}
inline void Print(ll x){//快速输出数字
if(x){
if(x<0)x=~x+1,putchar('-');
short a[22],l;
for(l=0;x;x/=10)a[++l]=x%10;
for(;l;--l)putchar(a[l]+48);
}
else putchar('0');
putchar(' ');
}
}
namespace Classes{
int nxt[N],to[N],w[N],h[N],t;//链式前向星
inline void AE(int x,int y,int z){//加边操作
nxt[++t]=h[x];
to[t]=y;
w[t]=z;
h[x]=t;
}
template<class T1,class T2>
class Pair{//自己手写的pair,也可以用STL的pair
public:
T1 x;
T2 y;
inline bool operator<(const Pair<T1,T2>b)const{return x==b.x?y<b.y:x<b.x;}//排序函数
};
template<class T1,class T2>
inline Pair<T1,T2> Make_Pair(T1 x,T2 y){//把两个元素做成一个pair
Pair<T1,T2>a;
a.x=x,a.y=y;
return a;
}
template<class T>
class Priority_Queue{//手写堆,也可以用STL的priority_ueue
private:
T q[N];
int l;
inline void Up(int x){
int y=x>>1;
while(y&&q[x]<q[y]){
swap(q[x],q[y]);
x=y,y=x>>1;
}
}
inline void Down(int x){
int y=x<<1;
while(y<=l){
if(y<l&&q[y+1]<q[y])++y;
if(q[y]<q[x])swap(q[x],q[y]),x=y,y=x<<1;
else break;
}
}
public:
Priority_Queue(){l=0;}
inline bool Empty(){return l==0;}
inline void Clear(){l=0;}
inline void Push(T x){q[++l]=x,Up(l);}
inline void Pop(){q[1]=q[l--],Down(1);}
inline T Top(){return q[1];}
};
}
using namespace io;
using namespace Classes;
ll d[N];//dis数组,注意一定要开long long
bool b[N];//打标记的数组
Priority_Queue<Pair<ll,int> >q;
inline void Dijkstra(){
memset(d,63,sizeof(d));//初始化
d[1]=0;//让源点1的dis值为0
int x,y,z;
q.Push(Make_Pair(0ll,1));//把源点插入堆中(假装你已经更新过了源点的dis值)
while(!q.Empty()){
x=q.Top().y,q.Pop();
if(b[x])continue;//如果这个点已经被弹出过就跳过
b[x]=1;//否则就给它打上标记
for(int i=h[x];i;i=nxt[i]){
y=to[i],z=w[i];
if(d[y]>d[x]+z){//如果这个点的dis值不是最优的
d[y]=d[x]+z;//更新这个点的dis值
q.Push(Make_Pair(d[y],y));//把更新后的点插入堆中
}
}
}
}
int main(){
int n=Read(),m=Read(),x,y,z;
for(int i=1;i<=m;i++)x=Read(),y=Read(),z=Read(),AE(x,y,z);
Dijkstra();
for(int i=1;i<=n;i++)Print(d[i]>=0x3f3f3f3f3f3f3f3fll?-1:d[i]);//三元运算符,如果某个点的dis值没被更新过(就是dis值还是初始化的那个值),直接输出-1
//否则输出这个点的dis值
return 0;
}
7.一些闲话
可能会有人就会问了:STL 这么好用,为什么不用 STL 呢?
因为 STL 的堆使用的数组是动态开空间的 vector,而动态开空间又很慢,因此最好能不用 STL 就不用 STL。
另外还有一个叫 SPFA 的最短路算法,时间复杂度为
那我们为什么不能用 SPFA 做这道题呢?
因为 SPFA 在某些特殊构造的数据下,时间复杂度会从
所以 NOI2018 就出了这样一个梗:/doge