单源最短路进阶 - “旧王已死,新王当立!”
目录:
从一道题目出发 —— Luogu 4779 - 【模板】单源最短路径(标准版)
题目链接:https://www.luogu.org/problemnew/show/P4779
题目背景
2018 年 7 月 19 日,某位同学在 NOI Day 1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。
然后呢?
100→60;
Ag→Cu;
最终,他因此没能与理想的大学达成契约。
小 F 衷心祝愿大家不再重蹈覆辙。
题目描述
给定一个 N 个点,M 条有向边的带非负权图,请你计算从 S 出发,到每个点的距离。
数据保证你能从 S 出发到任意点。
输入格式:
第一行为三个正整数 N,M,S。 第二行起 M 行,每行三个非负整数 ui,vi,wi,表示从 ui 到 vi 有一条权值为 wi 的边。
输出格式:
输出一行 N 个空格分隔的非负整数,表示 S 到每个点的距离。
4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4
题解:
这是一道最短路的模板题,但是它卡SPFA,还卡某些优化不好的SPFA,所以本题我们要使用SLF+swap优化的SPFA。
(顺便再用堆优化dijkstra)
首先回顾一下Bellman-Ford算法:
①初始化,所有点的 dist[i] = INF,出发点 s 的 dist[s] = 0;
②对于每条边 edge(u,v),若 dist[u] != INF,且 dist[v] > dist[u] + edge(u,v).w,则松弛 dist[v] = dist[u] + edge(u,v).w
③循环步骤② $\left| V \right| - 1$ 次,或者知道某一次步骤②中没有边可以松弛,则转步骤④
④若存在一条边 edge(u,v),满足 dist[u] != INF,且dist[v] > dist[u] + edge(u,v).w,则图中存在负环。
我们知道,Bellman-Ford算法的时间复杂度是 $O\left( {\left| V \right|\left| E \right|} \right)$,而我们可以使用队列对其进行优化,那就是大名鼎鼎的SPFA算法,
所以说,SPFA就是队列优化的Bellman-Ford算法。
不妨回顾一下SPFA算法:
①初始化,所有点的 dist[i] = INF,源点 s 的 dist[s] = 0;构建队列,源点 s 入队,并标记该点已在队列中。
②队头出队,标记该点已不在队列中(若图存在负权边,则可以对该点出队次数检查,若出队次数大于 n,则存在负环,算法结束),
遍历该点出发的所有边,假设当前遍历到某条边为 edge(u,v),若 dist[v] > dist[u] + edge(u,v).w,则松弛dist[v] = dist[u] + edge(u,v).w,
检查节点 v 是否在队列中,若不在则入队,标记节点 v 已在队列中。
④重复执行步骤②直到队列为空。
普通SPFA的TLE代码:
#include<bits/stdc++.h> using namespace std; const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; //邻接表存图 struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } //SPFA单源最短路 int dist[maxn]; bool vis[maxn]; void spfa() { for(int i=1;i<=n;i++) dist[i]=INF,vis[i]=0; dist[s]=0; queue<int> Q; Q.push(s); vis[s]=1; while(!Q.empty()) { int u=Q.front();Q.pop(); vis[u]=0; for(int i=0;i<G[u].size();i++) { Edge &e=E[G[u][i]]; int v=e.v; if(dist[v]>dist[u]+e.w) { dist[v]=dist[u]+e.w; if(!vis[v]) { Q.push(v); vis[v]=1; } } } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } spfa(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" ")); }
但是,对于这个“队列优化”,有必要清楚的一点是:
SPFA的时间复杂度,其实和Bellman-Ford是一样的,都是$O\left( {\left| V \right|\left| E \right|} \right)$,
只是SPFA在部分图中跑的比较快,给人以 $O\left( {k\left| E \right|} \right)$ 的感觉(其中 $k$ 为所有点入队次数的平均,部分图的 $k$ 值很小),
但是,现在很多的题目,都是会卡掉SPFA的。所以,现在对于没有负权边的图,单源最短路请优先考虑堆优化Dij
当然啦,SPFA被卡了我还是想用SPFA怎么办?根据知乎上@fstqwq对于“如何看待SPFA算法已死这种说法?”的回答表明,
在不断的构造图卡SPFA和不断地优化SPFA过数据的斗争中,LLL优化、SLF优化、SLF带容错等一系列优化都被卡掉了,
所以……
而到目前(2018.9.4)为止,暂时有位神仙想出了一种SLF+swap优化的SPFA,暂时还很难卡掉,是不是SPFA还能苟住一波呢?心向往之情不自禁地就想了解一下:
首先是单纯的 SLF优化:Small Label First策略,设要入队的节点是 j,而队首元素为 i,若dist[j] < dist[i] 则将 j 插入队首,否则插入队尾。
再然后是 SLF+swap优化:每当队列改变时,如果队首节点 i 的 dist[i] 大于队尾节点 j 的 dist[j],则交换首尾节点。
SLF+swap优化的AC代码:
#include<bits/stdc++.h> using namespace std;
const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; //邻接表存图 struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } //数组模拟队列 const int Qsize=2e5+10; int head,tail; int Q[Qsize]; //SPFA单源最短路 int dist[maxn]; bool vis[maxn]; void spfa() { for(int i=1;i<=n;i++) dist[i]=INF,vis[i]=0; dist[s]=0; head=tail=0; Q[tail++]=s; vis[s]=1; while(head<tail) { int u=Q[head++]; vis[u]=0; if(head<tail-1 && dist[Q[head]]>dist[Q[tail-1]]) swap(Q[head],Q[tail-1]); for(int i=0;i<G[u].size();i++) { Edge &e=E[G[u][i]]; int v=e.v; if(dist[v]>dist[u]+e.w) { dist[v]=dist[u]+e.w; if(!vis[v]) { Q[tail++]=v; vis[v]=1; if(head<tail-1 && dist[Q[head]]>dist[Q[tail-1]]) swap(Q[head],Q[tail-1]); } } } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } spfa(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" ")); }
第二天凌晨(2018.9.5 - 0:18)更新,上面的代码WA了,添加的第六组数据,直接把这种优化的SPFA给叉掉了,神奇!
所以!再强调一遍!
所以!还是Dijkstra大法好!
接下来回到Dijkstra时间!之前说了,没有负权边的图,推荐使用堆优化的Dijkstra,时间复杂度有保证!
首先依然是回顾一下Dijkstra算法:
①构建两个存储节点的集合:
集合S:存储的是已经确定正确计算出dist[]的节点,刚开始为空;
集合Q:$V - S$,刚开始时就等于集合V。
构建标记数组vis[],标记为1代表该点在集合S中,标记为0就在集合Q中。
②初始化,所有点的 dist[i] = INF,源点 s 的 dist[s] = 0;所有点的标记全部置零。
②重复 $\left| V \right|$ 次如下步骤:
1.寻找集合Q里dist[]最小的那个节点 u,标记 vis[u] = 1(放入集合S中)
2.遍历节点 u 出发的所有边,假设当前遍历到某条边为 edge(u,v),若节点 v 在集合Q中(vis[v] = 0),则尝试松弛dist[v] = min( dist[v] , dist[u] + edge(u,v).w )。
普通的Dijkstra算法时间复杂度 $O\left( {\left| V \right|^2 } \right)$,跟Bellman-Ford算法和普通SPFA一样过不了本题,
所以就要掏出堆优化的Dijkstra了,因为要寻找集合Q里dist[]最小的那个节点 u,不一定要遍历来寻找,可以通过堆来降低寻找的时间复杂度。
第一种,实现起来最简单的,用STL库的优先队列实现,
考虑最坏情况,所有的边都要松弛一遍,则往优先队列里push了 $O\left( {\left| E \right|} \right)$ 个元素,所以每次push和pop都要 $O\left( {\log \left| E \right|} \right)$,
同样,又因为最坏情况每条边都要松弛一次,则要进行 $O\left( {\left| E \right|} \right)$ 次push和pop。故时间复杂度 $O\left( {\left| E \right|\log \left| E \right|} \right)$,
AC代码:
#include<bits/stdc++.h> using namespace std; typedef pair<int,int> pii; //first是最短距离,second是节点编号 #define mk(x,y) make_pair(x,y) const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } int dist[maxn]; bool vis[maxn]; priority_queue< pii, vector<pii>, greater<pii> > Q; void dijkstra() { for(int i=1;i<=n;i++) dist[i]=INF, vis[i]=0; dist[s]=0, Q.push(mk(0,s)); while(!Q.empty()) { int u=Q.top().second; Q.pop(); if(vis[u]) continue; vis[u]=1; for(auto x:G[u]) { Edge &e=E[x]; int v=e.v; if(vis[v]) continue; if(dist[v]>dist[u]+e.w) dist[v]=dist[u]+e.w, Q.push(mk(dist[v],v)); } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } dijkstra(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" ")); }
接下来是手写二叉堆优化Dijkstra算法,由于控制堆内元素个数 $O\left( {\left| V \right|} \right)$,所以每次push和pop时间复杂度是 $O\left( {\log \left| V \right|} \right)$,
同时,每个点都出堆(或者说,出集合Q)一次,则进行了 $O\left( {\left| V \right|} \right)$ 次pop操作,
又考虑最坏情况每条边都进行了松弛,则进行了 $O\left( {\left| E \right|} \right)$ 次入堆push或者堆内某个点上移up操作,
因此总时间复杂度 $O\left( {\left( {\left| V \right| + \left| E \right|} \right)\log \left| V \right|} \right)$。
AC代码:
#include<bits/stdc++.h> using namespace std; const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; int dist[maxn]; bool vis[maxn]; struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } struct Heap { int sz; int heap[4*maxn],pos[maxn]; void up(int now) { while(now>1) { int par=now>>1; if(dist[heap[now]]<dist[heap[par]]) //子节点小于父节点,不满足小顶堆性质 { swap(heap[par],heap[now]); swap(pos[heap[par]],pos[heap[now]]); now=par; } else break; } } void push(int x) //插入权值为x的节点 { heap[++sz]=x; pos[x]=sz; up(sz); } inline int top(){return heap[1];} void down(int now) { while((now<<1)<=sz) { int nxt=now<<1; if(nxt+1<=sz && dist[heap[nxt+1]]<dist[heap[nxt]]) nxt++; //取左右子节点中较小的 if(dist[heap[now]]>dist[heap[nxt]]) //子节点小于父节点,不满足小顶堆性质 { swap(heap[now],heap[nxt]); swap(pos[heap[now]],pos[heap[nxt]]); now=nxt; } else break; } } void pop() //移除堆顶 { heap[1]=heap[sz--]; pos[heap[1]]=1; down(1); } void del(int p) //删除存储在数组下标为p位置的节点 { heap[p]=heap[sz--]; pos[heap[p]]=p; up(p), down(p); } inline void clr() { sz=0; memset(pos,0,sizeof(pos)); } }h; void dijkstra() { for(int i=1;i<=n;i++) dist[i]=INF, vis[i]=0; dist[s]=0; h.clr(); h.push(s); while(h.sz) { int u=h.top(); h.pop(); if(vis[u]) continue; vis[u]=1; for(int i=0;i<G[u].size();i++) { Edge &e=E[G[u][i]]; int v=e.v; if(!vis[v] && dist[v]>dist[u]+e.w) { dist[v]=dist[u]+e.w; if(h.pos[v]) h.up(h.pos[v]); else h.push(v); } } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } dijkstra(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" ")); }
这个代码还没有AC,只拿了80分,但是今晚(2018.9.4 - 23:38)把上面AC过的代码交了一发,以及本题题解里面的代码交了一些,都只有80分,最后一个测试点没能通过,原因是因为Too long on line 1,比较奇怪,猜测可能是数据问题。
第二天凌晨(2018.9.5 - 0:15)更新,上面的代码AC了,添加的第六组数据有点问题,已经被@fstqwq巨巨修好了。
当然,最后还有一种比较神奇的优化Dijkstra方式,就是线段树优化(线段树天下第一!),
其实它优化Dijkstra的原理和优先队列和二叉堆都是差不多的,优化的重点无非是在集合Q找dist[]最小的那个点,
所以首先不妨把1~n个点全部扔进去建线段树,维护两个值:区间最小值minval 和 最小值在哪个位置minpos,现在这棵线段树就是我们的初始的集合Q了!
要在集合Q里找dist[]最小的节点 u,简单啊 节点u 不就是 node[root].minpos 嘛!
很好,那接下来怎么把这个节点从集合Q里踢出去呢,删除节点不现实,把它更新成INF不就好了,这样以后就不会再找到这个点了,
如果还能再找到这个点……说明整棵线段树里所有元素的值都变成INF了,那不就代表集合Q是空的了嘛,所以循环结束~
时间复杂度:建树 $O\left( {\left| V \right|} \right)$,线段树单点修改 $O\left( {\log \left| V \right|} \right)$,
每个点出集合Q一次即 $O\left( {\left| V \right|} \right)$ 次线段树单点修改,每条边全部松弛一次即 $O\left( {\left| E \right|} \right)$ 次线段树单点修改,
因此总的时间复杂度 $O\left( {\left( {\left| V \right| + \left| E \right|} \right)\log \left| V \right|} \right)$。
#include<bits/stdc++.h> using namespace std; const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; int dist[maxn]; bool vis[maxn]; struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } /********************************* Segment Tree - st *********************************/ struct Node{ int l,r; int minval,minpos; }node[4*maxn]; int nodeidx[maxn]; void pushup(int root) { if(node[root<<1].minval<=node[root<<1|1].minval) { node[root].minval=node[root<<1].minval; node[root].minpos=node[root<<1].minpos; } else { node[root].minval=node[root<<1|1].minval; node[root].minpos=node[root<<1|1].minpos; } } void build(int root,int l,int r) { if(l>r) return; node[root].l=l; node[root].r=r; if(l==r) { node[root].minval=((l==s)?0:INF); node[root].minpos=l; nodeidx[l]=root; } else { int mid=l+(r-l)/2; build(root*2,l,mid); build(root*2+1,mid+1,r); pushup(root); } } void update(int root,int pos,int val) { if(node[root].l==node[root].r) { node[root].minval=val; return; } int mid=node[root].l+(node[root].r-node[root].l)/2; if(pos<=mid) update(root*2,pos,val); if(pos>mid) update(root*2+1,pos,val); pushup(root); } /********************************* Segment Tree - ed *********************************/ void dijkstra() { for(int i=1;i<=n;i++) dist[i]=((i==s)?0:INF),vis[i]=0; build(1,1,n); while(node[1].minval<INF) { int u=node[1].minpos; if(vis[u]) continue; vis[u]=1; update(1,u,INF); for(int i=0;i<G[u].size();i++) { Edge &e=E[G[u][i]]; int v=e.v; if(vis[v]) continue; if(dist[v]>dist[u]+e.w) { dist[v]=dist[u]+e.w; update(1,v,dist[u]+e.w); } } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } dijkstra(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"\n":" ")); }