P3371 【模板】单源最短路径(弱化版) 题解
简要题意:
给定一张有向图,求从源点开始,向各点的最短路(无负权)。(所谓的“单源最短路径”)
显然,如果你第一次见这种最短路的模板,你可能会用 记忆化搜索 来解决。
但是很遗憾,记忆化搜索的 “记忆化” 在图中很难得到有效体现;还是会稳稳的 \(\texttt{TLE}\).
为了验证记忆化搜索的错误性,本人分析一下。
对于当前点向外扩展,如果已有答案比现有要优,则更新答案。
时间复杂度:\(O(m^2)\).(这个时间很玄学)
实际得分:\(40pts\).
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+1;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
int n,m,f[N]; //f[i] 是从源点开始的最短路答案
vector<pair<int,int> >G[N];
inline void dfs(int dep,int bs) {
if(f[dep]!=-1 && f[dep]<=bs) return; //有答案且不优
f[dep]=bs; //记录
for(int i=0;i<G[dep].size();i++)
dfs(G[dep][i].first,G[dep][i].second+bs); //走
}
int main(){
n=read(),m=read();
int s=read();
for(int i=1;i<=m;i++) {
int x=read(),y=read(),z=read();
G[x].push_back(make_pair(y,z));
} memset(f,-1,sizeof(f));
dfs(s,0); for(int i=1;i<=n;i++) printf("%d ",f[i]==-1?INT_MAX:f[i]);
return 0;
}
当然如果你会 Floyd 这题可以得到 70 分。。
但是,如果我们完全不了解最短路,也可以通过搜索解决本题。
既然,\(\texttt{dfs}\) 已经死了,那么是否可以考虑 \(\texttt{bfs}\) ?
是可以试试看的。比方说,从源点开始入队扩展,然后对当前点 不在队列里 的点更新答案(如果在队列里就不用更新,因为队列里的永远最优),然后出队。
那么你会说了,如果一个点入队多次,会不会有问题呢?
不会。
因为,只要它不在队列里,别的点就会更新它;很可能它刚刚更新了一个点出队,然后那个点反过来再更新它一遍,这是完全可能出现的,没有问题。
时间复杂度:\(O(n^2)\).(具体详见下文分析)
实际得分:\(100pts\).
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+1;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
int n,m,s,dis[N]; //dis[i] 存储答案
vector<pair<int,int> >G[N];
bool vis[N]; //vis[i] 表示是否在队中
inline void SPFA() {
queue<int>q; memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++) dis[i]=INT_MAX;
q.push(s); dis[s]=0; vis[s]=1;
while(!q.empty()) {
int u=q.front();
q.pop(); vis[u]=0; //出队,记得取消标记
for(int i=0;i<G[u].size();i++) {
int v=G[u][i].first,z=G[u][i].second;
if(dis[v]>dis[u]+z) { //更新答案
dis[v]=dis[u]+z;
if(!vis[v]) {vis[v]=1; q.push(v);}
}
}
}
}
int main(){
n=read(),m=read(),s=read();
for(int i=1;i<=m;i++) {
int x=read(),y=read(),z=read();
G[x].push_back(make_pair(y,z));
} SPFA();
for(int i=1;i<=n;i++) printf("%d ",dis[i]); //输出答案
return 0;
}
首先要惊喜地告诉你,这就是广为人知的 \(\text{SPFA}\) 算法,你已经学会了一个中级图论算法。
那么,为什么会达到 \(O(n^2)\) 的时间复杂度?可能你会说,这应该是 \(O(n)\) 伴着大常数而已吧?
不是这样的。\(\text{SPFA}\) 看似没有问题,但是 如果出题人构造数据可以卡到 \(O(n^2)\),只有随机数据才能侥幸通过,和另一个 \(\text{dijkstra}\) 的效率相比,不太稳定,随机图效率极高,手造图效率极低。考场上只有时间不够,骗分等才会用。
那么,这个极限数据是什么呢?
这里给出一组官方卡数据(来源于 CodeForces)
n 2*n-3 1
1 2 1000000000
1 3 1000000000
1 4 1000000000
...
1 n 1000000000
2 3 1
3 4 1
4 5 1
...
n-1 n 1
你会发现,假设你一步把 \(1\) ~ \(n\) 所有数的最短路都标了出来。
然后,你就运用 “\(k-1\) 和 \(k\) 之间的边开始不断更新”,然后呢就咕掉了。
所以出题人随随便便就可以把你卡掉(多简单?)
但是毕竟是随机数据嘛,没什么问题
但是请注意,如果你去 加强版 作死的话:
你将会得到一个 32分 TLE
所以,那道加强版需要用 \(\text{dijkstra}\) 加上堆优化啦。。