最短路
单源最短路
SPFA
\(\bigstar\texttt{important}\):加入队列的时候一定要打上已经入队标记,不然可能在同一个循环内重复入队!
队列优化 Bellman-Ford 算法 。
关于 SPFA ,他死了 。
时间复杂度 \(O(nm)\) (容易被卡,不太稳定)
如何判断负环:
用 SPFA ,设 \(cnt[i]\) 表示 \(1\) 到 \(i\) 的最短路条数。松弛一条边的时候用 \(cnt[u]+1\) 来更新 \(cnt[v]\) ,若 \(cnt[v]\ge n\) 则说明出现了负环 。
证明:\(1\) 到 \(i\) 的最短路最多只有 \(n-1\) 条,若 \(cnt\ge n\) ,则一个点必然经过了至少 \(2\) 次,则出现了负环 。
P3385 【模板】负环 核心代码:
#define Maxn 2005
#define Maxm 3005
void spfa()
{
memset(inq,false,sizeof(inq)),memset(cnt,0,sizeof(cnt)),memset(ds,inf,sizeof(ds)),ds[1]=0;
queue<int> q; q.push(1),inq[1]=true;
while(!q.empty() && !exfu) // 这里一定要判断,不然会死循环
{
int cur=q.front(); q.pop(),inq[cur]=false;
for(int i=hea[cur];i;i=nex[i]) if(ds[ver[i]]>ds[cur]+edg[i])
{
ds[ver[i]]=ds[cur]+edg[i],cnt[ver[i]]=cnt[cur]+1;
if(cnt[ver[i]]>=n) exfu=true; // 要特别注意整个图有多少个点
else if(!inq[ver[i]]) q.push(ver[i]),inq[ver[i]]=true;
}
}
}
spfa(),printf(exfu?"YES\n":"NO\n");
update - 如何判断负环 \(2.0\):
在做 P3199 [HNOI2009]最小圈 的时候,发现另一种寻找负环的方式,感觉复杂度和上面差不多,如果没有负环,每个点都会用三角不等式去更新它,最多更新它的度数次,最多遍历 \(m\) 条边,复杂度最坏情况下是 \(\mathcal{O(nm)}\) 的。但是如果找到了负环,那么非常快啊,将复杂度直接降到了 \(\mathcal{O(n)}\) 一次遍历。
具体做法是:我们对于每个点赋初始距离为 \(0\),从每一个点开始 dfs 遍历整张图,用 vis 数组记录这个点是否在这一次 dfs 的路径上。
如果我们发现能够更新一个在路径上的点,那么就说明找到了负环。
inline void Find(int x)
{
vis[x]=true;
for(int i=hea[x];i;i=nex[i])
{
if(ds[ver[i]]>ds[x]+edg[i])
{
ds[ver[i]]=ds[x]+edg[i];
if(vis[ver[i]] || !exist) { exist=false; return; }
Find(ver[i]);
}
}
vis[x]=false;
}
// exist 表示没有负环
Dijkstra
只适合处理没有负环的图 。
加上 系统堆 优化后,时间复杂度为 \(O((n+m)\log m)\) (没错,就是 \(m\) ,因为 priority_queue<> 中会有一大堆重复的元素,从而导致堆中最多可能有 \(m\) 个元素)。
但是如果只用 堆 ,复杂度仍然是 \(O((n+m)\log n)\) ,因为堆的信息会及时更新,剔除重复元素。
当然,如斐波那契堆的复杂度可以进一步优化到 \(O(n\log n+m)\)
\(\rightarrow\) 所以,每个节点最多只会增广一次。即,每个节点只会进行一次遍历儿子的操作。
P4779 【模板】单源最短路径(标准版) 核心代码 :
#define Maxn 100005
#define Maxm 500005
bool inq[Maxn];
struct Data
{
int num,val;
bool friend operator < (Data x,Data y){ return x.val>y.val; }
}cur;
priority_queue<Data> q;
void dij_spfa()
{
memset(ds,inf,sizeof(ds)),ds[s]=0;
q.push((Data){s,0});
while(!q.empty())
{
int cur=q.top().num; q.pop();
if(vis[cur]) continue;
vis[cur]=true;
for(int i=hea[cur];i;i=nex[i])
if(ds[ver[i]]>ds[cur]+edg[i])
{
ds[ver[i]]=ds[cur]+edg[i];
q.push((Data){ver[i],ds[ver[i]]});
}
}
}
dij_spfa();
全源最短路
Floyd
本质上式一个动态规划的过程,将原先三维的一个 dp 变为二维的。
记 \(dp(k,i,j)\) 表示只经过前 \(k\) 个点的情况下,\(i\) 和 \(j\) 的最短路是多少。
发现 \(k\) 递增,所以可以直接压掉。
for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
dis[i][j]=dis[j][i]=min(dis[i][j],dis[i][k]+dis[j][k]);
Floyd 还可以解决类似于寻找有向图的最小正环的问题,如CF147B Smile House。
收到上面的启发,我们设 \(dp(k,i,j)\) 表示只经过 \(k\) 条边时,\(i,j\) 之间的最大距离。
再加上一个倍增就完成啦!
Johnson
Johnson 算法通过一种方法给每条边重新标注边权,使每一条边都变为正整数,最后通过 \(n\) 轮 Dijkstra 求出全源最短路 。
-
我们新建一个虚拟节点( 在这里我们就设它的编号为 \(0\) )。从这个点向其他所有点连一条边权为 \(0\) 的边 。
-
接下来用 SPFA 算法求出从 \(0\) 号点到其他所有点的最短路,记为 \(h_i\) ( \([x\in [1,n]~|~h_i\le 0]\) )。
-
假如存在一条从 \(u\) 点到 \(v\) 点,边权为 \(w\) 的边,则我们将该边的边权重新设置为 \(w+h_u-h_v\) 。
-
接下来以每个点为起点,跑 \(n\) 轮 Dijkstra 算法即可求出任意两点间的最短路了 。
具体证明详见 \(OI~Wiki\) 最短路
时间复杂度:\(O(nm\log m)\) 。
P5905 【模板】Johnson 全源最短路 核心代码:
#define Maxn 3005
#define Maxm 6005
ll ds[Maxn];
void spfa()
{
memset(h,inf,sizeof(h)),h[0]=0;
queue<int> q; q.push(0),inq[0]=true;
while(!q.empty() && !exfu)
{
int cur=q.front(); q.pop(),inq[cur]=false;
for(int i=hea[cur];i;i=nex[i]) if(h[ver[i]]>h[cur]+edg[i])
{
h[ver[i]]=h[cur]+edg[i],cnt[ver[i]]=cnt[cur]+1;
if(cnt[ver[i]]>=n+1) exfu=true; // 特别注意这里有 n+1 个点
else if(!inq[ver[i]]) inq[ver[i]]=true,q.push(ver[i]);
}
}
}
void dij(int x)
{
for(int i=1;i<=n;i++) ds[i]=infll;
memset(inq,0,sizeof(inq)),ds[x]=0;
priority_queue<Data> q;
q.push((Data){x,0}),inq[x]=true;
Data cur;
while(!q.empty())
{
cur=q.top(),q.pop(),inq[cur.num]=false;
for(int i=hea[cur.num];i;i=nex[i]) if(ds[ver[i]]>ds[cur.num]+1ll*edg[i])
{
ds[ver[i]]=ds[cur.num]+1ll*edg[i];
if(!inq[ver[i]]) inq[ver[i]]=true,q.push((Data){ver[i],ds[ver[i]]});
}
}
}
n=rd(),m=rd();
for(int i=1,u,v,d;i<=m;i++) u=rd(),v=rd(),d=rd(),add(u,v,d);
for(int i=1;i<=n;i++) add(0,i,0);
spfa();
if(exfu) printf("-1\n");
else
{
for(int i=1;i<=tot;i++) edg[i]+=h[fro[i]]-h[ver[i]];
ll MAX=1000000000ll,ans;
for(int i=1;i<=n;i++)
{
dij(i),ans=0;
for(int j=1;j<=n;j++) ans+=1ll*j*((ds[j]==infll)?MAX:(ds[j]+h[j]-h[i]));
printf("%lld\n",ans);
}
}
例题
P2446 [SDOI2010]大陆争霸
改一改 \(\text{dij}\) 的操作,加上 \(arrive,release,damage\) 变量表示这个点到达、可以进入、摧毁的最短时间。
更新时用 \(damage=\max(arrive,release)\) 。
P5304 [GXOI/GZOI2019]旅行者
给定 \(n\) 个点 \(m\) 条边的有向图,其中有 \(k\) 个特殊点给出,求出 \(k\) 个特殊点两两之间的最短路的最小值是多少。(\(n\le 10^5,m\le 5\times 10^5\))
实在是不会了,好吧去贺题解。。
\(\bigstar\texttt{Hint-1}\):如果将所有特殊点分为 \(A,B\) 两个集合,\(s\) 向 \(A\) 中的点连边,\(B\) 中的点向 \(t\) 连边,那么这样的最短路就是 \(A\) 中的点到 \(B\) 中的点的最短路的最小值。
\(\bigstar\texttt{Hint-2}\):考虑按照进制分组。
好吧提示到这里我才会,看来还是太菜啦!
那么对于每一个二进制位,这一位为 \(1\) 的当做 \(A\),为 \(0\) 的当做 \(B\);之后为 \(0\) 的当做 \(A\),为 \(1\) 的当做 \(B\),分别做一遍最短路即可(单向边)。
由于任意两个点一定有一个二进制位是不同的,所以结论是对的。
P2685 [TJOI2012]桥
给定一张 \(n\) 个点 \(m\) 条边的无向图,设删去一条边 \(i\) 后从 \(1\) 到 \(n\) 的最短路为 \(S_i\),且所有 \(S_i\) 的最大值为 \(Max\)。要求求出 \(S_i\) 等于 \(Max\) 的边有多少条。
\(n\le 10^5;m\le 2\times 10^5\)。
A 的代码写了比较奇怪的做法,但感觉照上面那题的做法取个 \(\min\) 即可。
P3238 [HNOI2014]道路堵塞
给定一张有 \(n\) 个点 \(m\) 条边的有向图,其中钦定了一条长度为 \(L\) 的从 \(1\) 到 \(n\) 的路径,且保证这条路径是 \(1\) 的 \(n\) 的最短路之一。
现在问对于这条路径上的每一条边,如果删去这条边后 \(1\) 到 \(n\) 的最短路变为多少?如果不存在了输出 \(-1\)。
\(n,L\le 10^5,m\le 2\times 10^5\)。
想了想还是不会做,去看题解了。
\(\bigstar\texttt{Hint}\):我们发现最终的最短路(一组可行解)一定是从最短路一个端点跑出来,在跑回最短路中,且只会离开与返回一次。
考虑一个暴力:从左到右依次遍历路径上的点作为起点,求出到每个点的 \(ds_i\),表示从路径上第 \([1,p]\) 号点出发,到达 \(i\) 号点且不经过路径上的边的最短路。每次做完就用线段树更新答案。这样看起来复杂度是 \({O(L(n+m)m)}\) 的。
但是,每一次最短路的时候不必清空 ds
数组,因为如果之前已经到达了这个点,那么后面答案一定不会变优。
好像题解也是这么写的?太奇怪了,先这么做吧。md?过了?似乎是每次遍历都会使下一次扩展收到限制,所以综合复杂度不会特别大。
CF1163F Indecisive Taxi Fee
和上一题一模一样啊,只用自己求出一条最短路之后,不在最短路上的直接贡献,否则再加上删去这条边的贡献。