目录
引入
我们在信息学奥赛中,时常会遇到让我们求图上最短路的问题,之前我们可以敲dfs或bfs,但是这两个一个时间复杂度太大了,一个有限制条件,所以说我们还不如重新研究一个算法,所以这就有了最短路算法。
但是这里有个点值得注意,最短路算法不仅仅可以用于解决最短路,还可以用于解决一些不等式(组)求解的问题,也就是我们经常说的差分约束。
友情提示:在学习这篇之前,请先学习图论基础章
单源最短路
如果说,我们只想求一个点到另一个点或者说一个点到其他所有点的最短路的话,这也就是在求单源最短路,一般情况下,单源最短路会用两种算法解决,一个是Djikstra,另一个是SPFA,这两个算法都可以在较快的时间复杂度内解决,但是都有一些细微的差别,下面我们会具体来讲。
Dijkstra算法
我们还是以一道例题入手。
例题1------最短路
题目描述
给一张有 \(n\) 个点 \(m\) 条边的带权有向图,求从 \(1\) 到 \(n\) 的最短路长度
输入格式
第一行有两个正整数 \(n\) , \(m\) \((1\le n,m \le 2\times 10^5)\)
接下来有 \(m\) 行,每行有3个整数 \(u,v,w\, (1\le u,v \le n,w\le 1000)\) 表示一条从 \(u\) 到 \(v\) 的权值为 \(w\) 的边。
输出格式
输出一个整数,表示最短路的长度,如果不存在,则输出 \(No \,\ solotion\)
样例输入
5 10
3 2 8
5 4 7
1 4 8
2 5 3
3 4 8
3 5 2
4 3 6
4 1 1
5 3 6
5 1 9
样例输出
16
分析
如果说我们拿到这道题会怎么做?硬跑dfs?这样的时间复杂度是一定不行的。所以说我们这里就要采用Dijkstra算法。
Dijkstra算法的过程
我们将所有的点分成两个集合,一个是已经求到最小值的(我们记为 \(V\) ),另一个是还没有求到最小值,也就是说还没有确定答案的(我们记为 \(E\)),我们再记 \(dis\) 为答案数组。
首先初始话 \(dis\) 数组为正无穷。然后 \(dis[st]\) 为0 (\(st\) 为起始点)
我们从 \(E\) 中取出一个离起点最近的点 (记为 \(u\)),将他放入 \(V\),然后遍历 \(u\) 连接到的点 (记为 \(v\)),根据 \(u\) 的答案来更新 \(v\) 的答案(这里的 \(v\) 是没有在集合 \(V\) 中的,这个操作就是被我们叫做的"松弛操作 "),并将更新过的答案放入 \(E\) 中。重复循环这个过程,直到 \(E\) 为空集,此时 \(n\) 的答案就是我们想要的。
这样听起来是不是有点抽象,我们举个例子。
记 \(mp[x][y]\) 为从 \(x\) 到 \(y\) 的路径长度。
我们先初始化所有的 \(dis\) 数组为正无穷。\(dis[1]\) 为0。
从1开始,先把1放入集合 \(V\) ,当前可以去的点有2,3,5;
\(\because\,dis[2]>dis[1]+mp[1][2]\) \(\therefore dis[2]=dis[1]+mp[1][2]=3\)同理 \(dis[3]=dis[1]+mp[1][3]=3,dis[5]=dis[1]+mp[1][5]=6\)
此时将上述两个放入集合 \(E\) 中。\(E=\{2,3,5\}\)
从集合 \(E\) 中拿出 2,先把2放入集合 \(V\) ,从2开始,可以去的点有4,5;
\(\because\,dis[4]>dis[2]+mp[2][4]\) \(\therefore dis[4]=dis[2]+mp[2][4]=5\)
\(\because\, dis[5]<dis[2]+mp[2][5]\) \(\therefore \,dis[5]=dis[5]\)将上述的两个放入集合 \(E\) 中,新的5替代原本的5。\(E=\{3,4,5\}\)
以下开始简写
取出 3,放入集合 \(V\),从3开始,没有可以去的点,所以跳过。
此时集合 \(E=\{4,5\}\)
取出4,放入集合 \(V\),从4开始可以去5和6。
\(dis[5]=\min(dis[5],dis[4]+mp[4][5])=6\)
\(dis[6]=\min(dis[6],dis[4]+mp[4][6])=6\)将5替换,将6放入集合 \(E\)。此时集合 \(E=\{5,6\}\)
取出5,放入集合 \(V\) ,从5开始可以去3,5,但是3已经在集合 \(V\) 中了,所以不能去3,只能去6。
\(dis[6]=\min(dis[6],dis[5]+mp[5][6])=6\)将6替换,此时集合 \(E=\{6\}\)
取出6,放入集合 \(V\),从6开始没有可以去的点,所以跳过,此时集合 \(E\) 为空,结束算法,答案为 \(dis[6]\)。
现在应该看懂了吧,没懂的话可以自己再手动模拟一下。
Dijkstra算法的时间复杂度和代码
如果说我们不使用一些特殊的数据结构来优化的话,时间复杂度是 \(\cal O(n^2)\) 的。因为我们每次都要花费 \(O(n)\) 的时间来寻找集合 \(E\) 中的最小值,再加上外面的一层 \(O(n)\),时间复杂度就变成 \(O(n^2)\) 的了。
示范代码
cpp
const int INF=1e4+10;
struct Node{
int v, w;
};
vector<Node> mp[INF];
int dis[INF],used[INF];
int n;
void dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
for (int i=1;i<=n;i++){
int u=0,minn=INT_MAX;
for (int j=1;j<=n;j++){
if (!used[j]&&dis[j]<minn)u=j,minn=dis[j];
used[j]=1;
}
int len=mp[u].size();
for (int i=0;i<len;i++){
int v=mp[u][i].v,w=mp[u][i].w;
if (dis[v]>dis[u]+w)dis[v]=dis[u]+w;
}
}
}
但是这样的时间复杂度还是有一点高,所以说我们可以想想用一下数据结构来优化,比如说优先队列。
我们会发现每次都要找的最小值其实可以塞进一个优先队列中,让它以 \(O(\log n)\) 的时间复杂度处理出来,这样我们的时间复杂度就可以降低了。但是如果这样的话,我们几乎是要把每条边都要放进去,也就是说放的这个动作,我们至少会操纵 \(m\) 次,同时还有 \(n\) 个点,那么我们的时间复杂度就是 \(O((n+m)\log n)\) 的。
因为时间复杂度是 \(O((n+m)\log n)\) ,所以说我们就要小心加谨慎了,因为如果是稠密图的话(也就是 \(m\approx n^2\))的时候时间复杂度反而比不优化更劣,所以说我们要小心加谨慎。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=2e5+10,MAXN=1e18;
struct Node{
long long p,num;
bool operator <(const Node &b)const{
return num>b.num;
}
};
priority_queue<Node> q;
long long n,m,st,dis[INF],used[INF];
vector<Node> mp[INF];
void dijkstra(int x){
dis[x]=0,q.push({x,0});
while (!q.empty()){
int u=q.top().p;
q.pop();
if (used[u]!=0)continue;
used[u]=1;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].p,w=mp[u][i].num;
if (dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({v,dis[v]});
}
}
}
}
void fi(){
for (int i=0;i<=n;i++)mp[i].clear();
for (int i=0;i<=n;i++)dis[i]=MAXN;
for (int i=0;i<=n;i++)used[i]=0;
}
int main(){
int T;
cin>>T;
while (T--){
cin>>n>>m>>st;
fi();
for (int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});
}
dijkstra(st);
for (int i=1;i<=n;i++){
if (dis[i]>=MAXN)cout<<2147383647<<" ";
else cout<<dis[i]<<" ";
}
cout<<endl;
}
return 0;
}
Dijkstra的最短路计数
如果说我们现在有一个图,它不仅让你找出最短路,还要让你找到最短路有几条你会怎么做呢?
其实这道题就是一本通上的一道题:一本通------最短路计数
我们可以仔细思考一下,对于一条最短路,他的突破点在哪里?是不是就是那个松弛操作的方程,是不是最短路也只会在那个地方进行更改?那么我们的关键就在这里了。
因为松弛操作可能会有三种可能,一种是新的答案小于旧的答案,一种是新的答案等于旧的答案,还有一种是新的答案大于旧的答案。对于这三种而言,能够对最短路造成影响的只有前两个,因为第三个压根不会更新,那么我们现在就着重来看这两种情况。
对于第一种情况,很显然,之前所求的最短路是全部都要作废的,因为我的答案更新了,但是这个更新了的答案等于1吗?显然不是,我们在更新当前答案的时候,前一个答案应该是已经得到了的(即在更新 \(dis[v]\) 的时候,\(dis[u]\) 的答案应该是确定的),基于此,前一个答案的方案数也应该是知道的,所以说当前值的方案数就应该等于前一个值的方案数(即 \(ans[v]=ans[u]\))
对于第二种情况,可以发现,当前的最短路是有新的方案加入的,也就是说,当前的方案数应该还要加上前一个答案的方案数的,这样才可以更新最短路的方案数(即 \(ans[v]+=ans[u]\))
基于这样的分析,我就可以很轻松的求得最短路的方案数了。
cpp
#include<bits/stdc++.h>
using namespace std;
struct Node{
int p,num;
bool operator <(const Node &a)const{
return num>a.num;
}
};
const int INF=1e5+10;
vector<int> mp[INF];
priority_queue<Node> q;
int dis[INF],ans[INF],used[INF];
void dijkstra(int x){
dis[x]=0,ans[x]=1,q.push({x,dis[x]});
while (!q.empty()){
int u=q.top().p;q.pop();
if (used[u])continue;
used[u]=1;
int len=mp[u].size();
for (int i=0;i<len;i++){
int v=mp[u][i];
if (dis[v]>dis[u]+1){//情况1
dis[v]=dis[u]+1,ans[v]=ans[u];
q.push({v,dis[v]});
}else if (dis[v]==dis[u]+1){//情况2
ans[v]+=ans[u];
ans[v]%=100003;
}
}
}
}
int n,m;
void fi(){
for (int i=0;i<=n;i++){
dis[i]=1e8;
}
}
int main(){
cin>>n>>m;
fi();
for (int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
mp[u].push_back(v);
mp[v].push_back(u);
}
dijkstra(1);
for (int i=1;i<=n;i++){
cout<<ans[i]<<endl;
}
return 0;
}
但是Dijkstra算法是有缺陷的,他不能跑有负权的图,换句话说,我们的Dijkstra算法在利用贪心算法的时候,遇到负权就会出事,比如说下面的这个例子。
我们从1出发可以到2,3,根据优先队列的原理,我们会先到3号点去, 此时3号点的答案就已经确定了,为-1,这个答案明显是错的。我们的正确答案应该是从一旁绕一下,经过2号点,再到3号点,答案应该是-99。所以说Dijkstra算法是不可以用于有负权的值的 。
那么我们此时就要采用另外一个算法了,下面请出------SPFA。
SPFA算法
首先我们要知道SPFA算法是从Bellman-Ford算法优化而来的,但是由于没有优化过的Bellman-Ford算法跑的真的太慢了,所以说我们这里也就不讲了。
在我们OI圈,有一句话叫做 "SPFA 已死!",为什么呢?因为SPFA刚刚出来的时候,跑的非常快,以至于他的创始人就误以为这个算法是接近 \(O(n)\) 的时间复杂度,但是非常可惜,SPFA算法在遇到菊花图的时候会被卡成 \(O(nm)\) 的时间复杂度,也就是说,如果说SPFA是标算的一部分的话,题目中应该给出 \(O(nm)\) 能过的范围。基于此,我们应该抱有这样的心态------我写的就是Bellman-Ford,只不过他在有些图上跑的飞快。嗯,确凿。
SPFA算法的过程
和Dijkstra算法一样,我们这里还是要进行松弛操作(\(dis[v]=\max(dis[v],dis[u]+w)\),而没有经过优化的Bellman-Ford算法就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,他就结束了。
很简单的就会发现,每一次循环是 \(O(m)\) 的,然而在最短路存在的前提下,每次松弛操作至少会增加一种最短路的可能,而总共会有 \(n-1\) 种可能,所以说最多会进行 \(n-1\) 次循环,所以时间复杂度就是 \(O(nm)\) 的。
但是呢,我们可能不需要有这么多次的松弛操作,也就是说Bellman-Ford所作的很多的松弛操作是无用的(捂脸)。很显然,只有上一次被松弛的结点所连接的边,才有可能引起下一次的松弛操作。我们可以简单的证明一下:
设图 \(G = (V, E)\),其中 \(V\) 是顶点集,\(E\) 是边集。对于图中的每条边 \((u, v) \in E\),都有一个权值 \(w(u, v)\)。设 \(d[v]\) 表示从源点 \(s\) 到顶点 \(v\) 的当前最短路径估计值。
其中,对于边 \((u, v)\),松弛操作的伪代码如下:
cppif d[v] > d[u] + w(u, v) d[v] = d[u] + w(u, v)
它指的 "是如果通过顶点 \(u\) 到达顶点 \(v\) 的路径长度比当前 \(d[v]\) 更短,那么更新 \(d[v]\)"。
在SPFA开始跑的时候,源点 \(s\) 的距离 \(d[s] = 0\),其他顶点 \(v \in V(without \,s)\) 的距离 \(d[v] = +\infty\)。此时,只有源点 \(s\) 会被放入队列(在 SPFA 算法中),因为只有 \(s\) 的距离发生了改变(从 \(+\infty\) 变为 0),即 \(s\) 是第一次被松弛的点。
我们可以考虑当前队列中的一个顶点 \(u\)(即上一轮被松弛的顶点),对其所有邻接顶点 \(v\) 进行松弛操作。如果边 \((u, v)\) 满足松弛条件,即 \(d[v] > d[u] + w(u, v)\),那么 \(d[v]\) 会被更新,反之不会。
而对于一个没有在这一轮被松弛的顶点 \(x\),它的距离估计值 \(d[x]\) 也就不会发生变化。所以对于与 \(x\) 相连的边 \((x, y)\),由于 \(d[x]\) 没有改变,那么 \(d[y]\) 也不可能因为边 \((x, y)\) 而发生改变,因为 \(d[y]\) 的更新依赖于 \(d[x]\) 的值(根据松弛操作的定义 \(d[y] = d[x] + w(x, y)\))。
也就是说,只有那些上一轮被松弛的顶点 \(u\) 所连接的边 \((u, v)\) 才有可能引起下一次的松弛操作,因为只有这些顶点的距离估计值发生了变化,才有可能影响到它们邻接顶点的距离估计值。故得证。
基于这个结论,我们就可以很显然可以通过记录那些点被松弛过了,从而来减少无用的松弛操作,而这个记录的过程,我们就可以使用队列来优化。
但是请注意,我们要保证在队列中,一个元素只出现了一次,换句话说,如果当前的松弛操作成功了,但是松弛的这个点如果是在队列中的,我们是不能再把这个点放进去的。因为这样才可以保证时间复杂度的相对减少,同时这样也是正确的,因为我们往队列里塞的只是编号,没有像Dijkstra一样,把长度还放了进去。因此,再外面进行了更改以后,这个效果也会同步传递过去,因为两个元素所对应的值是相同的。
SPFA算法的时间复杂度和代码
SPFA在稀疏图上的时间复杂度为 \(O(km)\),其中 \(k\) 是一个很小的常数,一般情况下 \(k<2\),但是SPFA在稠密图,或者说菊花图上的时候会被卡成原型,也就是 \(O(nm)\) ,所以说要小心加谨慎。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=5e3+10,MAXN=1e18;
struct Node{
int p,num;
};
long long dis[INF],sum[INF];
int n,m,s,t,used[INF];
vector<Node> mp[INF];
void spfa(int x){
queue<int> q;
dis[x]=0,q.push(x),used[x]=1;
while (!q.empty()){
int u=q.front();q.pop();
used[u]=0;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].p,w=mp[u][i].num;
if (dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if (!used[v]){
q.push(v);used[v]=1;
}
}
}
}
}
void fi(){
for (int i=0;i<=n;i++)dis[i]=MAXN;
}
int main(){
ios::sync_with_stdio();
cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
fi();
for (int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});//有向图
}
spfa(s);
cout<<dis[t];
return 0;
}
但是因为这个求的是最短路,所以说如果存在负环的话,就会死循环,然后就gameover了,所以说我们还要想想怎么判断负环。
SPFA判断负环
因为对于一个有 \(n\) 个点的图来说,最短路至多经过 \(n-1\) 个点,换句话说,一个点至多被其余的 \(n-1\) 个点给松弛 \(n-1\) 遍,因此我们可以检测一下每个点被松弛了的次数,如果说大于等于了 \(n\) 次,就说明一定有负环,反之,如果说SPFA正常的跑完了,就说明是没有负环的。判断负环也是SPFA唯一还比较有用的技巧。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=5e3+10,MAXN=1e18;
struct Node{
int p,num;
};
long long dis[INF],sum[INF];
int n,m,s,t,used[INF];
vector<Node> mp[INF];
void spfa(int x){
queue<int> q;
dis[x]=0,q.push(x),used[x]=1;
while (!q.empty()){
int u=q.front();q.pop();
used[u]=0;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].p,w=mp[u][i].num;
if (dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if (!used[v]){
if (++sum[v]>=n){//如果有负环
cout<<"No";
exit(0);
}
q.push(v);used[v]=1;
}
}
}
}
}
void fi(){
for (int i=0;i<=n;i++)dis[i]=MAXN;
}
int main(){
ios::sync_with_stdio();
cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
fi();
for (int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});
}
spfa(s);
if (dis[t]>=1e18)cout<<"No";//如果到达不了
else cout<<dis[t];
return 0;
}
单源最短路的例题
P2951 [USACO09OPEN------洛谷] Hide and Seek S
P2951 [USACO09OPEN] Hide and Seek S
题目描述
Bessie is playing hide and seek (a game in which a number of players hide and a single player (the seeker) attempts to find them after which various penalties and rewards are assessed; much fun usually ensues).
She is trying to figure out in which of N (2 <= N <= 20,000) barns conveniently numbered 1..N she should hide. She knows that FJ (the seeker) starts out in barn 1. All the barns are connected by M (1 <= M <= 50,000) bidirectional paths with endpoints A_i and B_i (1 <= A_i <= N; 1 <= B_i <= N; A_i != B_i); it is possible to reach any barn from any other through the paths.
Bessie decides that it will be safest to hide in the barn that has the greatest distance from barn 1 (the distance between two barns is the smallest number of paths that one must traverse to get from one to the other). Help Bessie figure out the best barn in which to hide.
奶牛贝西和农夫约翰(FJ)玩捉迷藏,现在有N个谷仓,FJ开始在第一个谷仓,贝西为了不让FJ找到她,当然要藏在距离第一个谷仓最远的那个谷仓了。现在告诉你N个谷仓,和M个两两谷仓间的"无向边"。每两个仓谷间当然会有最短路径,现在要求距离第一个谷仓(FJ那里)最远的谷仓是哪个(所谓最远就是距离第一个谷仓最大的最短路径)?如有多个则输出编号最小的。以及求这最远距离是多少,和有几个这样的谷仓距离第一个谷仓那么远。
输入格式
* Line 1: Two space-separated integers: N and M
* Lines 2..M+1: Line i+1 contains the endpoints for path i: A_i and B_i
第一行:两个整数N,M;
第2-M+1行:每行两个整数,表示端点A_i 和 B_i 间有一条无向边。
输出格式
* Line 1: On a single line, print three space-separated integers: the index of the barn farthest from barn 1 (if there are multiple such barns, print the smallest such index), the smallest number of paths needed to reach this barn from barn 1, and the number of barns with this number of paths.
仅一行,三个整数,两两中间空格隔开。表示:距离第一个谷仓最远的谷仓编号(如有多个则输出编号最小的。),以及最远的距离,和有几个谷仓距离第一个谷仓那么远。
输入输出样例 #1
输入 #1
6 7
3 6
4 3
3 2
1 3
1 2
2 4
5 2
输出 #1
4 2 3
分析
这题太简单了,我们在进行状态转移的时候,求一下最大值就可以了,没有什么难度。
cpp
#include<bits/stdc++.h>
using namespace std;
const int INF=2e4+10;
int cnt,maxn=INT_MIN,ans=INT_MAX;
struct Node{
int p,num;
bool operator <(const Node &a)const{
return num>a.num;
}
};
int dis[INF],used[INF];
int n,m;
vector<int> mp[INF];
priority_queue<Node> q;
void dijkstra(int x){
dis[x]=0;q.push({x,0});
while (!q.empty()){
int u=q.top().p;q.pop();
if (used[u]==1)continue;
used[u]=1;
int len=mp[u].size();
for (int i=0;i<len;i++){
int v=mp[u][i];
if (dis[v]>dis[u]+1){
dis[v]=dis[u]+1;
if (maxn<dis[v])maxn=dis[v],cnt=1,ans=v;
else if (maxn==dis[v])cnt++,ans=min(ans,v);
q.push({v,dis[v]});
}
}
}
}
void fi(){
for (int i=0;i<=n;i++)dis[i]=INT_MAX;
}
int main(){
cin>>n>>m;
fi();
for (int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
mp[u].push_back(v);
mp[v].push_back(u);
}
dijkstra(1);
cout<<ans<<" "<<maxn<<" "<<cnt;
return 0;
}
P1576 最小花费
题目描述
在 \(n\) 个人中,某些人的银行账号之间可以互相转账。这些人之间转账的手续费各不相同。给定这些人之间转账时需要从转账金额里扣除百分之几的手续费,请问 \(A\) 最少需要多少钱使得转账后 \(B\) 收到 \(100\) 元。
输入格式
第一行输入两个正整数 \(n,m\),分别表示总人数和可以互相转账的人的对数。
以下 \(m\) 行每行输入三个正整数 \(x,y,z\),表示标号为 \(x\) 的人和标号为 \(y\) 的人之间互相转账需要扣除 \(z\%\) 的手续费 \((z<100)\)。
最后一行输入两个正整数 \(A,B\)。数据保证 \(A\) 与 \(B\) 之间可以直接或间接地转账。
输出格式
输出 \(A\) 使得 \(B\) 到账 \(100\) 元最少需要的总费用。精确到小数点后 \(8\) 位。
输入输出样例 #1
输入 #1
3 3
1 2 1
2 3 2
1 3 3
1 3
输出 #1
103.07153164
说明/提示
\(1\le n \le 2000,m\le 100000\)。
分析
我们这道题可以反着来跑,我们从终点的答案为100,然后起点跑,这样就可以的到最小的答案了。
cpp
#include<bits/stdc++.h>
using namespace std;
const int INF=2e5+10,MAXN=1e8;
struct Node{
int p;
double num;
bool operator <(const Node &a)const{
return num>a.num;
}
};
vector<Node> mp[INF];
double dis[INF];
int n,m,used[INF];
priority_queue<Node> q;
void dijkstra(int x){
dis[x]=100.0,q.push({x,0});
while (!q.empty()){
int u=q.top().p;q.pop();
if (used[u]==1)continue;
used[u]=1;
int len=mp[u].size();
for (int i=0;i<len;i++){
int v=mp[u][i].p,w=mp[u][i].num;
if (dis[v]>dis[u]/(1.0-(w*0.01))){
dis[v]=dis[u]/(1.0-(w*0.01));
q.push({v,dis[v]});
}
}
}
}
void fi(){
for (int i=0;i<=n;i++)dis[i]=MAXN;
}
int main(){
cin>>n>>m;
fi();
for (int i=1;i<=m;i++){
int x,y;double z;
cin>>x>>y>>z;
mp[x].push_back({y,z});
mp[y].push_back({x,z});
}
int st,ed;
cin>>st>>ed;
dijkstra(ed);
printf("%.8lf",dis[st]);
return 0;
}
P3385 【模板】负环
题目描述
给定一个 \(n\) 个点的有向图,请求出图中是否存在从顶点 \(1\) 出发能到达的负环。
负环的定义是:一条边权之和为负数的回路。
输入格式
本题单测试点有多组测试数据。
输入的第一行是一个整数 \(T\),表示测试数据的组数。对于每组数据的格式如下:
第一行有两个整数,分别表示图的点数 \(n\) 和接下来给出边信息的条数 \(m\)。
接下来 \(m\) 行,每行三个整数 \(u, v, w\)。
- 若 \(w \geq 0\),则表示存在一条从 \(u\) 至 \(v\) 边权为 \(w\) 的边,还存在一条从 \(v\) 至 \(u\) 边权为 \(w\) 的边。
- 若 \(w < 0\),则只表示存在一条从 \(u\) 至 \(v\) 边权为 \(w\) 的边。
输出格式
对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES
,否则输出 NO
。
输入输出样例 #1
输入 #1
2
3 4
1 2 2
1 3 4
2 3 1
3 1 -3
3 3
1 2 3
2 3 4
3 1 -8
输出 #1
NO
YES
说明/提示
数据规模与约定
对于全部的测试点,保证:
- \(1 \leq n \leq 2 \times 10^3\),\(1 \leq m \leq 3 \times 10^3\)。
- \(1 \leq u, v \leq n\),\(-10^4 \leq w \leq 10^4\)。
- \(1 \leq T \leq 10\)。
分析
这道题其实就是非常裸的负环的模版,只不过这里要注意一下数组的清空和邻接表的清空,要不然就寄完了。
cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e8,INF=1e5;
struct Node{
int p,num;
};
int n,m;
int sum[INF],dis[INF],used[INF];
vector<Node> mp[INF];
void spfa(int x){
queue<int> q;
dis[x]=0,q.push(x),used[x]=1;
while (!q.empty()){
int u=q.front();q.pop();
used[u]=0;
int len=mp[u].size();
for (int i=0;i<len;i++){
int v=mp[u][i].p,w=mp[u][i].num;
if (dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if (!used[v]){
if (++sum[v]>=n){
cout<<"YES"<<endl;
return;
}
q.push(v);
used[v]=1;
}
}
}
}
cout<<"NO"<<endl;
return;
}
void fi(){
for (int i=0;i<=n;i++)used[i]=0;
for (int i=0;i<=n;i++)dis[i]=MAXN;
for (int i=0;i<=n;i++)sum[i]=0;
for (int i=0;i<=n;i++)mp[i].clear();
}
int main(){
ios::sync_with_stdio();
cin.tie(),cout.tie();
int T;
cin>>T;
while (T--){
cin>>n>>m;
fi();
for (int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});
if (w>=0)mp[v].push_back({u,w});
}
spfa(1);
}
return 0;
}
全源最短路
如果说我们想要求任意两点之间的最短路,那么就等于我们要求的是全源最短路,对于全源最短路,我们经常会有两种算法可以解决,一种是Floyd,另一种是Johnson,这两个算法和上面的两个算法不同的是,这两个算法可以使用在任意图上,不必在意是否有负权或负环。
Floyd算法
可能你在自学的时候,会发现一些博客或者书中讲的是这样的:
每次枚举一个中间的转折点,然后再枚举另外两个点,然后答案就是两个点经过中转点转移走后的答案的最小值,也就是说枚举中转点 \(k\) 和另外两个点 \(i\),\(j\),答案就是 \(dis[i][j]=\min(dis[i][k]+dis[k][j],dis[i][j])\)
非常尴尬的说,这样的理解是完全错误 的,我们可以注意到,动态规划是要从已知量到未知量,但是这里的 \(dis[i][k]\) 和 \(dis[k][j]\) 是两个不确定的答案,这两个不一定是正确答案,甚至于说后来可能还会被更新,这就一定是不正确的。
咱就说,有没有一种可能,Floyd算法原本是三维的?
Floyd算法的过程
因为是dp,我们可以将d的状态定义成经过的点的编号不超过 \(k\),也就是说,我们的dp数组应该长成这样
\[dp[k][i][j]//从i到j的路径上的编号不超过k的最小值 \]
如果是这样理解的话,我们转移的时候就有一下两种情况
换句话说就是,在所有点的编号都是小于等于 \(k\) 的前提下时,从 \(i\) 到 \(j\) 的最短路经不经过 \(k\) 这个点。
基于此,我们就可以非常轻松的得到以下两种状态转移的方式。
\[dp[k][i][j]=dp[k-1][i][j] //如果当前这条路径上的点都小于k \\ \, \\dp[k][i][j]=dp[k-1][i][k]+dp[k-1][k][j] //因为有点k,所以以点k为分界点,将左右两边的最优解加起来 \]
注意:这里的把看作为转换点和网络上的是不一样的,这里是有限制的。
因为这两种方式都是可以的,所以我们把这两个状态转移方程合并一下就可以得到以下内容。
\[dp[i][j][k]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]) \]
此时我们的边界条件是 \(dp[0][i][j]=mp[i][j]\)(不存在的边存为无无穷大)
但是这样的空间是三维的,忒多了,指定要爆,所以说我们要优化一下。
可以发现,我们只做了一次dp,并且k的值是递增的,也就是说,每一次的答案只和前一次的答案有关系,所以说我们就可以把第三维给滚动掉,这样我们只需要开一个二维数组就行了。我们也可以顺手证明一下:
对于给定的 \(k\),当更新 \(dp[k][i][j]\) 时,他所涉及的元素总是来自 \(dp[k-1]\) 这个长数组的第 \(k\) 行和第 \(k\) 列。然后我们可以发现,对于给定的 \(k\),当更新 \(dp[k][k][j]\) 或 \(dp[k][i][k]\),总是不会发生数值更新,因为按照公式 \(dp[k][k][j] = min(dp[k-1][k][j], dp[k-1][k][k]+dp[k-1][k][j])\),\(dp[k-1][k][k]\) 为 0,因此这个值总是 \(dp[k-1][k][j]\),对于 \(dp[k][i][k]\) 的证明类似。
因此,如果省略第一维,在给定的 \(k\) 下,每个元素的更新中使用到的元素都没有在这次迭代中更新,因此第一维的省略并不会影响结果。
Floyd算法的时间复杂度和代码
很显然可以发现,这个是非常铁的三层循环,并且常数非常小,所以说时间复杂度是铁打的 \(\cal O(n^3)\)
cpp
for (int k=1;k<=n;k++){
for (int i=1;i<=n;i++){
for (int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
Floyd寻找最小环
如果我们要在一个图中,找到一个至少有3个节点的环,那么我们可以怎么办呢?其实我们是可以暴力跑Dijkstra的,但是呢时间复杂度太高了,没有必要,所以说我们就来研究一下其他的方法。
考虑Floyd算法的过程,因为 \(dis[i][j]\) 在外层循环的时候,是存储的"经过编号不超过 \(k-1\) 的结点,从 \(i\) 到 \(j\) 的最短路的长度,因此我们就可以想到 \(\min(dis[i][j]+a[j][k]+a[k][i])(1 \le i <j <k)\) 就是满足以下两个条件的最小环的长度:
- 由编号不超过k的节点构成
- 经过了节点k
注:上述的 \(i,j\) 是枚举了与 \(k\) 相邻的两个点
这里有一个易错点,为什么是 \(dis[i][j]+a[j][k]+a[k][i]\)而不是\(dis[i][j]+dis[j][k]+dis[k][i]\)?
客观的说,我刚开始也没搞懂,其实这里画个图就行了。
因为我们开的是滚动数组,所以我们需要在进行Floyd的同时,计算最小环,也就是说,我们要把计算最小的代码放在计算Floyd的前面,因为我需要用的是所经过的节点的编号不超过k-1的值,如果放在后面的话,就变成计算节点编号不超过k的值了。
这个是在我们对Floyd这个算法真正的理解透了之后才会有的想法,否则在其他的博客上看的话,可能这个就是一个新的算法了,但其实这个最小环只是Floyd的一个应用。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=1e13;
long long a[310][310],dis[310][310];
long long n,m,ans=INF;
int main(){
cin>>n>>m;
memset(dis,0x3f3f,sizeof(dis));
memset(a,0x3f3f,sizeof(a));
for (int i=0;i<=n+5;i++){
a[i][i]=0;
dis[i][i]=0;
}
for (int i=1;i<=m;i++){
long long x,y,z;
cin>>x>>y>>z;
a[x][y]=a[y][x]=min(a[x][y],z);
dis[x][y]=dis[y][x]=a[x][y];
}
for (int k=1;k<=n;k++){
//计算最小环
for (int i=1;i<k;i++){//因为是计算编号数小于等于k-1的结点,所以是i<k
for (int j=i+1;j<k;j++){
ans=min(dis[i][j]+a[i][k]+a[k][j],ans);
}
}
//计算Floyd
for (int i=1;i<=n;i++){
for (int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
if (ans==INF)cout<<"No solution.";
else cout<<ans;
return 0;
}
Floyd输出找到的最小环
接着上面的讲,如果说,我现在知道了一个最小环的长度,但是我要求出来这个最小环的具体是由哪些点构成的,这个又该怎么做呢?
不难发现,一个最小环的更新只有可能在上述的找最小环的时候,答案更新了,这个时候才会有最小环的变动,所以说我们应该从这里下手。
因此我们可以疯狂记录所有边的中转点 \(k\) ,通过这个来恢复出整条路径,感受一下,就和之前的二分一样,一半一半的,慢慢恢复。比如说 \(dis[i][j]\) 的中转点是 \(a\),那么整条路径就会被弄成三段,分别是 \(dis[i][a]+a+dis[a][j]\),那么我们现在再去找 \(dis[i][a]\) 的中转点,然后再找 \(dis[a][j]\) 的中转点,这样一直递归下去,那么最终就一定可以找到整条路径。
这里我们就可以用动态数组,和dfs来完成这个操作。
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e15;
const int INF=1010;
ll dis[INF][INF],a[INF][INF],c[INF][INF];
vector<int> path;
void dfs(int i,int j){
if (c[i][j]==0)return;
dfs(i,c[i][j]);
path.push_back(c[i][j]);
dfs(c[i][j],j);
return;
}
void print(){
int len=path.size();
for (int i=0;i<len-1;i++){
cout<<path[i]<<" ";
}
cout<<path[len-1];
}
int main(){
ll minn=MAXN;
for (int i=0;i<=1005;i++){
for (int j=0;j<=1005;j++){
dis[i][j]=MAXN;
}
}
for (int i=0;i<=1006;i++){
for (int j=0;j<=1005;j++){
a[i][j]=MAXN;
}
}
int n,m;
cin>>n>>m;
for (int i=0;i<=n+5;i++)dis[i][i]=0,a[i][i]=0;
for (int i=1;i<=m;i++){
ll u,v,d;
cin>>u>>v>>d;
a[u][v]=a[v][u]=dis[u][v]=dis[v][u]=min(dis[u][v],d);
}
for (int s=1;s<=n;s++){
for (int i=1;i<s;i++){
for (int j=i+1;j<s;j++){
if (minn>dis[i][j]+a[i][s]+a[s][j]){
minn=dis[i][j]+a[i][s]+a[s][j];
path.clear();
path.push_back(i);
dfs(i,j);
path.push_back(j);
path.push_back(s);
}
}
}
for (int i=1;i<=n;i++){
for (int j=1;j<=n;j++){
if (dis[i][j]>dis[i][s]+dis[s][j]){
dis[i][j]=dis[i][s]+dis[s][j];
c[i][j]=s;
}
}
}
}
if (minn==MAXN)cout<<"No solution.";
else print();
return 0;
}
Johnson算法
对于在稀疏图上,我们如果采用Floyd算法的话,时间复杂度可能过高,那么我们现在就可以想想其他的方法了。我们可以跑 \(n\) 边SPFA算法,但是如果遇到菊花图,就会被卡成 \(\cal O(n^2m)\),毕竟我们总不能去赌对吧。这时,我们可能会想到,用优先队列优化了的Dijkstra是不是更快呢?但是我们会发现,Dijkstra他是不能跑负权的,这条路就有可能行不通。真的吗?
如果说我们对每条边都加上一个权值 \(w\),使他们全部都变成正数,然后跑最短路,看经过了几条边 \(k\) ,我们就减去 \(kw\),那么剩下的是不是就是答案了?但是这样显然也是错误的。下面就是一个例子:

如果说在这个图上,我们对所有的点加上一个权值5,那么上图就会变成这样:
此时,新图中的从1到 2 的最短路应该是 7+2 等于9,那么按照上述的方法,这里的最短路就应该是 \(9-2\times5=-1\),但是真正的答案是这个吗?显然不是,正确的最短路应该是 \(-2\)。看来这条路是彻底走不通了。
真的吗?在我们都垂头丧气,准备回家继续使用Floyd的时候,有个人不服输,那就是 Donald B. Johnson,他在经过不懈的研究下发明了 "re-weight" 方法,让Djikstra可以跑起来,并且答案是正确的!所以这才有了现在的 Johnson算法。
Johnson算法的过程
其实这个算法的过程,关键就是在于他的那个 "re-weight"操作,实在是太逆天了。
我们新建一个节点(在这里我们就设它的编号为 0)。从这个点向其他所有点连一条边权为 0 的边。
接下来我们先用 SPFA 算法求出从 0 号点到其他所有点的最短路,记为 \(h_i\)。
假如存在一条从 \(u\) 点到 \(v\) 点,且边权为 \(w\) 的边的话,我们就将该边的边权重新设置为 \(w+h_u-h_v\)(这个就是他所谓的"re-weight"操作)。接下来以每个点为起点,跑 \(n\) 轮 Dijkstra 算法即可求出任意两点间的最短路了。
但是这样写为什么是对的?我们得想办法证明一下:
首先我们要明确我们要证明的是一种重新标注边权的方式为什么是正确的,而这背后其实是 Johnson 算法正确性的证明。
在正式开始证明之前,我们先引入一个物理概念 ------ 势能,像我们熟知的重力势能(比如高处的物体具有的能量)和电势能(电荷在电场中具有的能量)都属于势能。势能有两个很重要的特点:
- 势能的变化量只和起点和终点的位置有关,举个例子,你从一楼走到三楼,不管是坐电梯上去,还是走楼梯上去,重力势能的变化量是一样的,和你走的具体路径没关系。
- 势能的具体数值大小往往取决于我们设置的零势能点,就好像你在测量高度的时候,把海平面当作高度为 0 的点,或者把你家的地板当作高度为 0 的点,这会影响物体高度的具体数值。但是不管你把零势能点设在哪里,任意两点之间的势能差值是固定不变的。
好,我们先扯回来。在重新标记边权后的图里,从 \(s\) 点走到 \(t\) 点有一条路径,我们假设它是 \(s\) 点先走到 \(p_1\) 点,再从 \(p_1\) 点走到 \(p_2\) 点,一直这样走下去,最后从 \(p_k\) 点走到 \(t\) 点。那这条路径的长度可以写成下面这样:
\[(w(s,p_1)+h_s - h_{p_1})+(w(p_1,p_2)+h_{p_1}-h_{p_2})+ \cdots +(w(p_k,t)+h_{p_k}-h_t) \]
这里的 \(w(x,y)\) 表示从 \(x\) 点到 \(y\) 点的边的原来的权重,\(h_x\) 我们就把它叫做 \(x\) 点的势能。
我们把上面这个式子化简一下,会发现:
\[w(s,p_1)+w(p_1,p_2)+ \cdots +w(p_k,t)+h_s - h_t \]
你看,不管我们从 \(s\) 点到 \(t\) 点走的是哪一条路径,\(h_s - h_t\) 这个值是不会变的,这就和我们刚才说的势能的性质一样,不管走哪条路,起点和终点的势能差是固定的。
现在新图里从 \(s\) 点到 \(t\) 点的最短路长度表达式分成了两部分,前面那些边的权重相加就是原来图里从 \(s\) 点到 t 点的最短路长度,后面那部分 \(h_s - h_t\) 就是 \(s\) 点和 \(t\) 点之间的势能差。因为两点间的势能差是个定值,所以原来图里 \(s\) 点到 \(t\) 点的最短路和新图里 \(s\) 点到 \(t\) 点的最短路是对应的,因为他只取决于前面的一串的大小,和后面的定值没有关系。
到这里,我们只证明了一半的正确性,也就是重新标注边权之后,图里的最短路径还是原来的最短路径。但是我们还得证明新图里所有边的边权都是非负数才行,因为只有在边权都是非负的图上,Dijkstra 算法(一种找最短路的算法)才能保证得出正确的结果。
那怎么证明新图里边权非负呢?我们借助一个数学知识 ------ 三角形不等式。在图里任意一条边 \((u,v)\) 上的两个点 \(u\) 和 \(v\) ,它们满足 \(h_v \leq h_u + w(u,v)\) 。这条边重新标记后的边权 \(w'(u,v) = w(u,v) + h_u - h_v\) ,根据前面的不等式,我们可以知道 \(w'(u,v) \geq 0\) ,也就是说新图里所有边的边权都是非负的。这样,我们就完整地证明了 Johnson 算法的正确性,也就是这种重新标注边权的方式是正确的啦。
那么我们现在就可以非常愉快的跑Dijkstra了!
Johnson算法的时间复杂度和代码
不难发现这个算法的时间复杂度的瓶颈不是前面的SPFA算法,而是后面跑的 \(n\) 次Dijkstra,那么总的时间复杂度就是 \(\cal O(nm\log n)\),类似于Dijksra,如果说这个图是稠密图,那么时间复杂度反而不优于Floyd,所以说也要小心的使用。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=3e3+10,MAXN=1e9;
struct Node{
long long v,w;
bool operator <(const Node &a)const{
return w>a.w;
}
};
vector<Node> mp[INF];
long long h[INF],cnt[INF],used[INF],dis[INF];
int n,m;
void fi(long long t[],long long p){//手写版的memset
for (int i=0;i<=n;i++)t[i]=p;
}
bool spfa(int x){
fi(h,MAXN);
queue<long long> q;
h[x]=0,cnt[x]=1,q.push(x);
while (!q.empty()){
long long u=q.front();q.pop();
used[u]=0;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].v,w=mp[u][i].w;
if (h[v]>h[u]+w){
h[v]=h[u]+w;
if (!used[v]){
if (++cnt[v]>n)return false;
q.push(v);
used[v]=1;
}
}
}
}
return true;
}
void dijkstra(int x){
priority_queue<Node> q;
dis[x]=0;q.push({x,0});
while (!q.empty()){
long long u=q.top().v;q.pop();
if (used[u])continue;
used[u]=1;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].v,w=mp[u][i].w;
if (dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({v,dis[v]});
}
}
}
}
int main(){
cin>>n>>m;
for (int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});//正常建边
}
for (int i=1;i<=n;i++){//建立原点
mp[0].push_back({i,0});
}
if (!spfa(0)){//如果存在负环
cout<<-1;
return 0;
}
for (int i=1;i<=n;i++){
int len=mp[i].size();
for (int j=0;j<len;j++)mp[i][j].w+=h[i]-h[mp[i][j].v];//"re-weight"操作
}
for (int i=1;i<=n;i++){
fi(used,0);
fi(dis,MAXN);
dijkstra(i);//以每个点为起点,跑最短路
for (int j=1;j<=n;j++){
if (dis[j]>=MAXN)cout<<"No way to go there";
else cout<<dis[j]+h[j]-h[i];//还原成真实的最短路的长度
}
}
return 0;
}
全源最短路例题
P1690 贪婪的Copy
题目描述
Copy 从卢牛那里听说在一片叫 yz 的神的领域埋藏着不少宝藏,于是 Copy 来到了这个被划分为 \(n\) 个区域的神地。卢牛告诉了Copy这里共有 \(n\) 个宝藏,分别放在第 \(P_i\) 个 \((1\le P_i\le N)\) 区域。Copy还得知了每个区域之间的距离。现在 Copy 从 \(1\) 号区域出发,要获得所有的宝藏并到 \(n\) 号区域离开。Copy 很懒,只好来找你为他寻找一条合适的线路,使得他走过的距离最短。
输入格式
第一行一个正整数 \(N(1\le N\le 100)\)
接下来一个 \(N\times N\) 的矩阵,第 \(i+1\) 行第 \(j\) 列的数字表示区域 \(i,j\) 之间的距离。每个距离用空格隔开,距离保证 \(i\to j\le 1000\)。请注意的 \(i \to j\) 距离并不一定等于 \(j \to i\) 的距离。
第 \(N+2\) 行一个整数 \(P(0\le P\le 10)\)。
第 \(N+3\) 行共 \(P\) 个用空格隔开的整数,表示有宝藏的区域编号。
输出格式
一个整数,为 Copy 获得全部宝藏需要的最短距离。数据保证答案小于等于 maxlongint。
输入输出样例 #1
输入 #1
2
0 4
5 0
2
1 2
输出 #1
4
输入输出样例 #2
输入 #2
3
0 2 6
1 0 4
7 10 0
1
2
输出 #2
6
说明/提示
- 对 \(30\%\) 的数据,\(1\le n\le 15\),其余如题所述。
- 对 \(100\%\) 的数据,全部数据范围如题所述。
分析
这道非常简单,可以先用Floyd预处理出来所有的最短路,然后用dfs去找就可以了。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=1e18;
long long dis[110][110],tre[20];
int used[110],n,p;
long long ans=INT_MAX;
void dfs(int x,int num,long long tot){
if (num==p+1){
ans=min(ans,tot+dis[x][n]);
return;
}
for (int i=1;i<=p;i++){
if (used[i]==0){
used[i]=1;
dfs(tre[i],num+1,tot+dis[x][tre[i]]);
used[i]=0;
}
}
return;
}
int main(){
for (int i=0;i<=100;i++){
for (int j=0;j<=100;j++){
if (i!=j)dis[i][j]=INF;
}
}
cin>>n;
for (int i=1;i<=n;i++){
for (int j=1;j<=n;j++){
cin>>dis[i][j];
}
}
cin>>p;
for (int i=1;i<=p;i++){
cin>>tre[i];
}
for (int k=1;k<=n;k++){
for (int i=1;i<=n;i++){
for (int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
dfs(1,1,0);
cout<<ans;
return 0;
}
P10927 Sightseeing trip
题目描述
在桑给巴尔岛的阿德尔顿镇,有一家旅行社。除了许多其他景点外,这家旅行社决定为其客户提供该镇的观光旅游。为了从这个项目中获得尽可能多的收益,旅行社做出了一个精明的决定:需要找到一条始于同一地点并结束于同一地点的最短路线。你的任务是编写一个程序来找到这样的一条路线。
在该镇有 \(N\) 个编号为 1 到 \(N\) 的交叉点,以及 \(M\) 条编号为 1 到 \(M\) 的双向道路。两个交叉点可以通过多条道路连接,但没有道路连接同一个交叉点。每条观光路线是由道路编号 \(y_1\), ..., \(y_k\) 组成的序列,其中 \(k > 2\)。道路 \(y_i (1 \le i \le k-1)\) 连接交叉点 \(x_i\) 和 \(x_{i+1}\),道路 \(y_k\) 连接交叉点 \(x_k\) 和 \(x_1\)。所有的交叉点编号 \(x_1\), ..., \(x_k\) 应该是不同的。观光路线的长度是该路线所有道路长度的总和,即 \(L(y_1)+L(y_2)+...+L(y_k)\),其中 \(L(y_i)\) 是道路 \(y_i (1 \le i \le k)\) 的长度。你的程序需要找到这样一条观光路线,使其长度最小,或者指明不可能找到这样的路线,因为该镇中没有任何观光路线。
输入格式
输入第一行包含两个正整数:交叉点的数量 \(N \le 100\) 和道路的数量 \(M \le 10000\)。接下来的 \(M\) 行每行描述一条道路。每行包含三个正整数:第一个交叉点的编号,第二个交叉点的编号,以及道路的长度(小于 500 的正整数)。
输出格式
输出只有一行。如果没有任何观光路线,则输出字符串 "No solution.";否则,输出最短观光路线中所有交叉点的编号,按通过的顺序排列(即从定义中的 \(x_1\) 到 \(x_k\)),编号之间用单个空格分隔,行末不应有多余空格或者换行符。如果有多个长度相同的观光路线,可以输出其中任意一条。
输入输出样例 #1
输入 #1
5 7
1 4 1
1 3 300
3 1 10
1 2 16
2 3 100
2 5 15
5 3 20
输出 #1
1 3 5 2
输入输出样例 #2
输入 #2
4 3
1 2 10
1 3 20
1 4 30
输出 #2
No solution.
分析
这道题就是裸的找最小环的问题,所以说不太难。就不放代码了。
P5905 【模板】全源最短路(Johnson)
题目描述
给定一个包含 \(n\) 个结点和 \(m\) 条带权边的有向图,求所有点对间的最短路径长度,一条路径的长度定义为这条路径上所有边的权值和。
注意:
-
边权可能 为负,且图中可能存在重边和自环;
-
部分数据卡 \(n\) 轮 SPFA 算法。
输入格式
第 \(1\) 行:\(2\) 个整数 \(n,m\),表示给定有向图的结点数量和有向边数量。
接下来 \(m\) 行:每行 \(3\) 个整数 \(u,v,w\),表示有一条权值为 \(w\) 的有向边从编号为 \(u\) 的结点连向编号为 \(v\) 的结点。
输出格式
若图中存在负环,输出仅一行 \(-1\)。
若图中不存在负环:
输出 \(n\) 行:令 \(dis_{i,j}\) 为从 \(i\) 到 \(j\) 的最短路,在第 \(i\) 行输出 \(\sum\limits_{j=1}^n j\times dis_{i,j}\),注意这个结果可能超过 int 存储范围。
如果不存在从 \(i\) 到 \(j\) 的路径,则 \(dis_{i,j}=10^9\);如果 \(i=j\),则 \(dis_{i,j}=0\)。
输入输出样例 #1
输入 #1
5 7
1 2 4
1 4 10
2 3 7
4 5 3
4 2 -2
3 4 -3
5 3 4
输出 #1
128
1000000072
999999978
1000000026
1000000014
输入输出样例 #2
输入 #2
5 5
1 2 4
3 4 9
3 4 -3
4 5 3
5 3 -2
输出 #2
-1
说明/提示
【样例解释】
左图为样例 \(1\) 给出的有向图,最短路构成的答案矩阵为:
0 4 11 8 11
1000000000 0 7 4 7
1000000000 -5 0 -3 0
1000000000 -2 5 0 3
1000000000 -1 4 1 0
右图为样例 \(2\) 给出的有向图,红色标注的边构成了负环,注意给出的图不一定连通。

【数据范围】
对于 \(100\%\) 的数据,\(1\leq n\leq 3\times 10^3,\ \ 1\leq m\leq 6\times 10^3,\ \ 1\leq u,v\leq n,\ \ -3\times 10^5\leq w\leq 3\times 10^5\)。
对于 \(20\%\) 的数据,\(1\leq n\leq 100\),不存在负环(可用于验证 Floyd 正确性)
对于另外 \(20\%\) 的数据,\(w\ge 0\)(可用于验证 Dijkstra 正确性)
upd. 添加一组 Hack 数据:针对 SPFA 的 SLF 优化
分析
这道题看着模板都知道是Johnson算法,那么我们只需要注意一下按照题目中所说的改一下输入和输出的方式就可以了,也没有那么的难。
cpp
#include<bits/stdc++.h>
using namespace std;
const long long INF=3e3+10,MAXN=1e9;
struct Node{
long long v,w;
bool operator <(const Node &a)const{
return w>a.w;
}
};
vector<Node> mp[INF];
long long h[INF],cnt[INF],used[INF],dis[INF];
int n,m;
void fi(long long t[],long long p){
for (int i=0;i<=n;i++)t[i]=p;
}
bool spfa(int x){
fi(h,MAXN);
queue<long long> q;
h[x]=0,cnt[x]=1,q.push(x);
while (!q.empty()){
long long u=q.front();q.pop();
used[u]=0;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].v,w=mp[u][i].w;
if (h[v]>h[u]+w){
h[v]=h[u]+w;
if (!used[v]){
if (++cnt[v]>n)return false;
q.push(v);
used[v]=1;
}
}
}
}
return true;
}
void dijkstra(int x){
priority_queue<Node> q;
dis[x]=0;q.push({x,0});
while (!q.empty()){
long long u=q.top().v;q.pop();
if (used[u])continue;
used[u]=1;
int len=mp[u].size();
for (int i=0;i<len;i++){
long long v=mp[u][i].v,w=mp[u][i].w;
if (dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({v,dis[v]});
}
}
}
}
int main(){
cin>>n>>m;
for (int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
mp[u].push_back({v,w});//正常建边
}
for (int i=1;i<=n;i++){//建立原点
mp[0].push_back({i,0});
}
if (!spfa(0)){//如果存在负环
cout<<-1;
return 0;
}
for (int i=1;i<=n;i++){
int len=mp[i].size();
for (int j=0;j<len;j++)mp[i][j].w+=h[i]-h[mp[i][j].v];//"re-weight"操作
}
for (int i=1;i<=n;i++){
fi(used,0);
fi(dis,MAXN);
dijkstra(i);//以每个点为起点,跑最短路
long long ans=0;
for (int j=1;j<=n;j++){
if (dis[j]>=MAXN)ans+=j*MAXN;
else ans+=j*(dis[j]+h[j]-h[i]);
}
cout<<ans<<endl;
}
return 0;
}
这就是所有的最短路的知识了,还是码了将近3w个字,喜欢的话,可以评论一下,蟹蟹~~