【学习笔记】最短路
前言:只是对一些最短路算法的实现整理。
以下内容有部分摘自OI_wiki。
Floyd 算法
求全源最短路。可以有负边权。
Floyd 算法的本质是动态规划。设 \(dis(k, i, j)\) 表示由若干个编号不超过 \(k\) 的节点中转后从 \(i\) 到 \(j\) 的最短路。
该“动规”有两个决策,一是经过编号不超过 \(k-1\) 的节点由 \(i\) 到 \(j\),二是由 \(i\) 到 \(k\),再由 \(k\) 到 \(j\)。两者取最小值,据此可写出转移方程:
\[dis(k, i, j) = \min(dis(k-1, i, j), dis(k-1, i, k)+dis(k-1,k, j)) \]最终 \(i\) 到 \(j\) 的最短路为 \(dis(n, i, j)\)。
三维数组可能会爆内存,观察到 \(dis(k, i, j)\) 只与 \(k-1\) 层有关,故可以用滚动数组的方式将第一位滚去,此时的转移方程为:
\[dis(i, j) = min(dis(i, j), dis(i, k)+dis(k, j)) \]最终 \(i\) 到 \(j\) 的最短路为 \(dis(i, j)\)。
时间复杂度为 \(O(n^3)\),空间复杂度为 \(O(n^2)\)。
实现过程:
-
初始化 \(dis\) 数组所有值为正无穷。随后令 \(dis(i, j) = M(i, j)\),\(M(i, j)\) 为邻接矩阵中的值,即 \(i\) 到 \(j\) 的边权。
-
三重循环跑一遍 Floyd。
代码主体部分实现:
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 算法也可以在前 \(k\) 个元素的两两之间的最短路确定的时候加一点 \(k+1\),以 \(O(k^2)\) 的时间复杂度更新两两节点之间的最短路。
CF295B Greg and Graph
正向思路是按照题意每次删除点 \(x_{i}\),但 Floyd 不支持删除点,并且这样做会很麻烦:每到一个点 \(x_i\),就要做一次复杂度为 \(O(n^3)\) 的 Floyd,整体复杂度为 \(O(n^4)\),也会超时。
所以我们逆向思考,看成每次添加一个点 \(x_{i}\),并记录该点出现过。在计算总和时特判这些点是否出现过,记录答案。最后倒序输出。时间复杂度为 \(O(n^3)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 505;
int dis[N][N], del[N];
ll ans[N];
bool flag[N];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin>>n;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin>>dis[i][j];
for(int i=1; i<=n; i++)
cin>>del[i];
for(int p=n; p>=1; p--){
int k = del[p];
flag[k] = 1;
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(flag[i] && flag[j]) ans[p] += dis[i][j];
}
}
}
for(int i=1; i<=n; i++)
cout<<ans[i]<<" ";
return 0;
}
传递闭包
给出若干个元素以及它们的两两关系,如果这些元素具有传递性,我们就可以推出尽可能多的元素之间的关系。
此时 \(dis(i, j)\) 数组的意义为 \((i, j)\) 是否具有关系,有关系则 \(dis(i, j) = 1\),无关系则 \(dis(i, j) = 0\)。
转移方程为:
\[dis(i, j) = dis(i, j) \vee (dis(i, k) \wedge dis(k, j)) \]具体解释为 \((i,j)\) 既可以经过不大于 \(k-1\) 的节点连通,也可以由 \(i\) 经过 \(k\) 中转到 \(j\) 来连通。
实现过程:
-
令 \(dis(i, i) = dis(i, j) = 1\),满足自己到自己可达, \((i, j)\) 之间有关系(关系是 \(i\) 到 \(j\))。\(dis\) 数组内其余均为 \(0\)。
-
三重循环跑一遍 Floyd。
代码主体部分实现:
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[i][k] & dis[k][j]);
Dijkstra 算法
求解非负权图中单源最短路最稳定的算法。
实现过程:
将结点分成两个集合:已确定最短路长度的点集(记为 \(S\) 集合)的和未确定最短路长度的点集(记为 \(T\) 集合)。一开始所有的点都属于 \(T\) 集合。
-
初始化 \(dis(s)=0\),其他点的 \(dis\) 均为 \(+\infty\)。
-
然后重复操作:
从 \(T\) 集合中,选取一个最短路长度最小的结点,移到 \(S\) 集合中。对那些刚刚被加入 \(S\) 集合的结点的所有出边执行松弛操作。直到 \(T\) 集合为空,算法结束。
有多种方法来维护操作中最短路长度最小的结点,不同的实现导致了 Dijkstra 算法时间复杂度上的差异。
这里主要讲堆优化的 Dijkstra 的实现过程 (因为只会这个):
-
初始化 \(dis(s)=0\),其他点的 \(dis\) 均为 \(+\infty\)。并且将起点 \(s\) 以及它的距离 \(0\) 入队。
-
重复以下操作直至队列(堆)为空:
-
取出堆顶元素,并出队。堆是以当前元素的 \(dis\) 尽量小为关键字。
-
如果已经访问过这个点 \(u\)(堆顶元素对应的),则无视它进入下一次循环(因为 Dijkstra 算法基于贪心的思想已经更新了经过该点的最短路)。否则标记该点 \(u\) 为已访问。
-
遍历以 \(u\) 为起点的边,记终点为 \(v\),\(w\) 为 \((u, v)\) 的边权,如果满足 \(dis(v) > dis(u) + w\),则进行松弛操作,并将该元素和其对应的 \(dis\) 入队。
-
时间复杂度为 \(O(m \log m)\)。
以下为模板题的代码实现:
#include<bits/stdc++.h>
#define INF 0x7fffffff
using namespace std;
int read(){
int f=1, k=0; char c = getchar();
while(c<'0' || c>'9'){if(c=='-') f = -1; c = getchar();}
while(c>='0' && c<='9'){k = (k<<1) + (k<<3) + (c^48); c = getchar();}
return f*k;
}
const int MAXN = 100010;
struct node{
int to, va;
bool operator <( const node &x )const{
return x.va < va;
}
};
vector<node> p[MAXN];
int dis[MAXN];
bool vis[MAXN];
int n, m, s;
priority_queue<node> q;
void dijkstra(){
for(int i=1; i<=n; i++)
dis[i] = INF;
dis[s] = 0;
q.push((node){s, 0});
while(!q.empty()){
node x = q.top(); q.pop();
if(vis[x.to]) continue;
vis[x.to] = true;
for(int i=0; i<p[x.to].size(); i++){
node now = p[x.to][i];
if(dis[x.to]+now.va < dis[now.to]){
dis[now.to] = dis[x.to]+now.va;
q.push((node){now.to, dis[now.to]});
}
}
}
}
int main(){
n = read(), m = read(), s = read();
for(int i=1, x, y, z; i<=m; i++){
x = read(), y = read(), z = read();
p[x].push_back((node){y, z});
}
dijkstra();
for(int i=1; i<=n; i++){
printf("%d ", dis[i]);
}
return 0;
}
顺带一提最短路计数。多了一个统计。
Bellman–Ford 算法
Bellman–Ford 算法是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
实现过程:
不断尝试对图上每一条边进行松弛。每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。
每次循环是 \(O(m)\) 的,在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 \(+1\),而最短路的边数最多为 \(n-1\),因此整个算法最多执行 \(n-1\) 轮松弛操作。故求其的时间复杂度为 \(O(nm)\)。
还有一种情况,如果从 \(S\) 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 \(n-1\) 轮,因此如果第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发,能够抵达一个负环。
需要注意的是,以 \(S\) 点为源点跑 Bellman–Ford 算法时,如果没有给出存在负环的结果,只能说明从 \(S\) 点出发不能抵达一个负环,而不能说明图上不存在负环。
因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点执行 Bellman–Ford 算法。
代码实现(判是否存在负环):
struct Edge {
int u, v, w;
};
vector<Edge> edge;
int dis[MAXN], u, v, w;
const int INF = 0x3f3f3f3f;
bool bellmanford(int n, int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
bool flag = false; // 判断一轮循环过程中是否发生松弛操作
for (int i = 1; i <= n; i++) {
flag = false;
for (int j = 0; j < edge.size(); j++) {
u = edge[j].u, v = edge[j].v, w = edge[j].w;
if (dis[u] == INF) continue;
// 无穷大与常数加减仍然为无穷大
// 因此最短路长度为 INF 的点引出的边不可能发生松弛操作
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
// 没有可以松弛的边时就停止算法
if (!flag)
break;
}
// 第 n 轮循环仍然可以松弛时说明 s 点可以抵达一个负环
return flag;
}
SPFA 算法
关于 SPFA,它死了。
本质上是经过队列优化过的 Bellman–Ford 算法。所以能做的事情跟它差不多。并且最坏复杂度也是 \(O(nm)\)。
优化思路:
很多时候我们并不需要那么多无用的松弛操作。
很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。
那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。
SPFA 也可以用于判断 \(S\) 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少 \(n\) 条边时,说明 \(S\) 点可以抵达一个负环。
实现过程:
-
初始化 \(dis(s)=0, vis(s)=1\),其他点的 \(dis\) 均为 \(+\infty\),\(vis\) 均为 \(0\)。并且将起点 \(s\) 入队。
-
重复以下操作直至队列为空:
-
取出队头元素 \(u\),并出队。并且标记 \(vis(u) = 0\)。
-
遍历以 \(u\) 为起点的边,记终点为 \(v\),\(w\) 为 \((u, v)\) 的边权,如果满足 \(dis(v) > dis(u) + w\),则进行松弛操作。满足可松弛操作时 \(v\) 不在队列内(\(vis[v] = 0\))则将 \(v\) 入队,并标记 \(vis[v] = 1\)。
-
想要判负环的话加个统计,看最短路经过的边数是否 \(\ge n\)。
代码实现(判负环):
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;
bool spfa(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1; // 记录最短路经过的边数
if (cnt[v] >= n) return false;
// 在不经过负环的情况下,最短路至多经过 n - 1 条边
// 因此如果经过了多于 n 条边,一定说明经过了负环
if (!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return true;
}
以下为一些关于图的应用...
无向图最小环
暴力解法:
设 \(u\) 和 \(v\) 之间有一条边长为 \(w\) 的边,\(dis(u,v)\) 表示删除 \(u\) 和 \(v\) 之间的连边之后,\(u\) 和 \(v\) 之间的最短路。
那么无向图中的最小环是 \(dis(u,v)+w\)。
注意若是在有向图中求最小环,相对应的公式要修改,最小环是 \(dis(v,u)+w\)。
总时间复杂度 \(O(n^2m)\)。
Floyd:
记原图中 \(u,v\) 之间边的边权为 \(val\left(u,v\right)\)。
我们注意到 Floyd 算法有一个性质:在最外层循环到点 \(k\) 时(尚未开始第 \(k\) 次循环),最短路数组 \(dis\) 中,\(dis_{u,v}\) 表示的是从 \(u\) 到 \(v\) 且仅经过编号在 \(\left[1, k\right)\) 区间中的点的最短路。
由最小环的定义可知其至少有三个顶点,设其中编号最大的顶点为 \(w\),环上与 \(w\) 相邻两侧的两个点为 \(u,v\),则在最外层循环枚举到 \(k=w\) 时,该环的长度即为 \(dis_{u,v}+val\left(v,w\right)+val\left(w,u\right)\)。
故在循环时对于每个 \(k\) 枚举满足 \(i<k,j<k\) 的 \((i,j)\),更新答案即可。
总时间复杂度为 \(O(n^3)\)。
int val[maxn + 1][maxn + 1]; // 原图的邻接矩阵
int dis[maxn + 1][maxn + 1]; // 最短路矩阵
int floyd(int n) {
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = val[i][j]; // 初始化最短路矩阵
int ans = INT32_MAX;
for (int k = 1; k <= n; ++k) {
for (int i = 1; i < k; ++i)
for (int j = 1; j < i; ++j)
ans = min(ans, dis[i][j] + val[i][k] + val[k][j]); // 更新答案
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 更新最短路矩阵
}
return ans;
}
Dijkstra:
枚举所有边,每一次求删除一条边之后对这条边的起点跑一次 Dijkstra,道理同上。
时间复杂度 \(O(m(n+m)\log n)\)。
差分约束
差分约束系统是一种特殊的 \(n\) 元一次不等式组,它包含 \(n\) 个变量 \(x_1,x_2,\dots,x_n\) 以及 \(m\) 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如 \(x_i-x_j\leq c_k\),其中 \(1 \leq i, j \leq n, i \neq j, 1 \leq k \leq m\) 并且 \(c_k\) 是常数。我们要解决的问题是:求一组解 \(x_1=a_1,x_2=a_2,\dots,x_n=a_n\),使得所有的约束条件得到满足,否则判断出无解。
差分约束系统中的每个约束条件 \(x_i-x_j\leq c_k\) 都可以变形成 \(x_i\leq x_j+c_k\),这与单源最短路中的三角形不等式 \(d_{v} \leq d_{u}+w_{<u,v>}\) 非常相似。因此,我们可以把每个变量 \(x_i\) 看做图中的一个结点,对于每个约束条件 \(x_i-x_j\leq c_k\),从结点 \(j\) 向结点 \(i\) 连一条长度为 \(c_k\) 的有向边。
注意到,如果 \(\{a_1,a_2,\dots,a_n\}\) 是该差分约束系统的一组解,那么对于任意的常数 \(d\),\(\{a_1+d,a_2+d,\dots,a_n+d\}\) 显然也是该差分约束系统的一组解,因为这样做差后 \(d\) 刚好被消掉。
过程:
设 \(dis[0]=0\) 并向每一个点连一条权重为 \(0\) 的边,跑单源最短路,若图中存在负环,则给定的差分约束系统无解,否则,\(x_i=dis[i]\) 为该差分约束系统的一组解。
一些转化技巧:
题意 | 转化 | 连边 |
---|---|---|
$x_a-x_b \ge c$ | $x_b-x_a \leq -c$ | $add(a, b, -c);$ |
$x_a-x_b \leq c$ | $x_a-x_b \leq c$ | $add(b, a, c);$ |
$x_a=x_b$ | $x_a-x_b\leq0, x_b-x_a\leq0$ | $add(b, a, 0), add(a, b, 0);$ |
最短路树
在单源最短路的过程中,每个点 \(v\) 都存在一些前驱 \(u\),满足 \(dis(u)+w(u,v)=dis(v)\),这里 \(w(u,v)\) 是 \(u,v\) 两点之间最短的一条边的边权。
如果我们得到一棵树,根节点为单源最短路的起点 \(s\),树上每个节点的父亲都是它的某个前驱,则起点到每个点的最短距离,就是这棵树上这个节点到根路径的长度。这样的树就被称作最短路树。
简单来说,定义构建一棵树,使得树上任意不属于根的节点 \(x\),\(dis(root, x)\) 就是原图起点 \(s\) 到 \(x\) 的最短路。
实现过程:
要得到最短路树,首先需要知道每个点的前驱。
在 Dijkstra 的过程中,我们只需要记录每个点 \(v\) 最后一次被松弛的时候是被哪个点 \(u\) 松弛的,则这个 \(u\) 就是 \(v\) 的前驱。
然后把每个节点的父亲设置成它的前驱就行了。
注意到这样做必定是没有环的。因为将节点出队顺序看作一个拓扑序,节点之间的前驱关系是符合这个拓扑序的。也就是说每个点的前驱必然比它早出队。因此即使图上有零权环也没关系。
P3556 [POI2013] MOR-Tales of seafaring
题意:给 \(n\) 个点 \(m\) 条边无向图,每次询问两个点之间是否有长度为 \(d\) 的路径(不一定是简单路径)。
因为不一定是简单路径,所以可以在一条边上反复横跳,这样只要分别记录 \(u\) 到 \(v\) 的经过奇数条路径的最短路和经过偶数条路径的最短路。查看与 \(k\) 同奇偶的那条路径的最短路是否 \(\le k\)。
如果一个点是孤立的,那么怎么走都无解!
思路一(重要):分层图。
考虑拆点。将 \((u, v)\) 的路径拆成 \((u, v+n)\) 和 \((u+n, v)\),这样当当前点为 \(\{ 1, 2, \dots, n \}\) 时,所在层数即为经过偶数条路径能到达的一层,当当前点为 \(\{ n+1, n+2, \dots, n+n \}\) 时,所在层数即为经过奇数条路径能到达的一层。
因为边权均为 \(1\),所以可以 bfs 统计最短路。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5005;
vector<int> g[N<<1];
struct node{
int id, v, w;
};
int dis[N<<1][N<<1];
void bfs(int s){
queue<int> Q;
Q.push(s);
while(!Q.empty()){
int u = Q.front(); Q.pop();
for(int v : g[u]){
if(!dis[s][v]){ // 防止一条边走过两次之后继续重复
dis[s][v] = dis[s][u]+1;
Q.push(v);
}
}
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m, qy; cin>>n>>m>>qy;
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
g[u].push_back(v+n); g[v+n].push_back(u);
g[u+n].push_back(v); g[v].push_back(u+n);
}
for(int i=1; i<=n; i++)
bfs(i);
for(int i=1; i<=qy; i++){
int u, v, d; cin>>u>>v>>d;
if(d & 1){
if(dis[u][v+n]<=d && dis[u][v+n]) cout<<"TAK\n";
else cout<<"NIE\n";
} else{
if(dis[u][v]<=d && dis[u][v]) cout<<"TAK\n";
else cout<<"NIE\n";
}
}
return 0;
}
思路二:用不同的数组存距离。
因为 \(k\) 的值非常大,\(n\) 又非常小,为了不 TLE,可以将询问离线,枚举 \(s\) 跑 SPFA,一次性将 \(s\) 有关的询问全解决掉。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5005;
vector<int> g[N];
struct node{
int id, v, w;
};
vector<node> query[N];
bool ans[1000005], vis[N];
int dis[N][2]; // 分奇偶
void spfa(int s){
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
queue<int> Q;
dis[s][0] = 0;
vis[s] = 1; Q.push(s);
while(!Q.empty()){
int u = Q.front(); Q.pop();
vis[u] = 0;
for(int v : g[u]){
if(dis[v][1] > dis[u][0]+1){
dis[v][1] = dis[u][0]+1;
if(!vis[v]) vis[v] = 1, Q.push(v);
}
if(dis[v][0] > dis[u][1]+1){
dis[v][0] = dis[u][1]+1;
if(!vis[v]) vis[v] = 1, Q.push(v);
}
}
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m, qy; cin>>n>>m>>qy;
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1; i<=qy; i++){
int u, v, d; cin>>u>>v>>d;
query[u].push_back({i, v, d});
}
for(int u=1; u<=n; u++){
if(!query[u].empty() && g[u].size()){
spfa(u);
for(auto p : query[u]){
if(p.w >= dis[p.v][p.w&1]) ans[p.id] = 1;
}
}
}
for(int i=1; i<=qy; i++)
cout<<(ans[i] ? "TAK" : "NIE")<<"\n";
return 0;
}
百度地图的实时路况
题目大意:给定一张 \(n\) 个点的无向图,点 \(i\) 到点 \(j\) 的距离为 \(G_{i,j}\),特别地,\(G_{i,j}=-1\) 表示 \(i\) 和 \(j\) 不可达。定义 \(dis(u,v,w)\) 为 \(u\) 出发到 \(v\) 且不经过 \(w\) 的最短路径。(不能到达则为 \(-1\))求:
\(\sum_{u=1}^n\sum_{v=1}^n\sum_{w=1}^ndis(u,v,w)\)。其中 \(3\leq n\leq300, -1 \leq G_{i,j}\leq10000, G_{i, i}=0\)。
思路:分治 Floyd。
每次递归时计算 \(dis(l, r)\) 表示不使用 \([l,r]\) 中的点条件下的两两之间最短路。向下递归时,将 \(\left[mid+1,r\right]\) 的点加入后递归计算 \(dis(l, mid)\),同理 \([l,mid]\) 的点加入后递归计算\(dis(mid+1, r)\)。
在\(l=r\)时统计答案即可。
由主定理 \(T(N) = Nn^2+2T(N/2)\) 可以得到时间复杂度为 \(O(n^3\log n)\)。
trick:zzy 称其为时间线段树。撤销 \(\neq\) 删除。有些题目撤销往往比删除好维护非常多。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 305;
int n, dis[N][N];
ll solve(int l, int r){
ll ans = 0;
if(l == r){
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
if(i!=l && j!=r) ans += dis[i][j];
return ans;
}
int tmp[N][N];
int mid = l + (r-l)/2;
memcpy(tmp, dis, sizeof(dis));
// 左不变右变(右边有不取的元素)
for(int k=l; k<=mid; k++){
for(int i=1; i<=n; i++){
if(i==k) continue;
for(int j=1; j<=n; j++){
if(i==j || j==k) continue;
if(dis[i][k]!=-1 && dis[k][j]!=-1){
if(dis[i][j]!=-1) dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
else dis[i][j] = dis[i][k]+dis[k][j];
}
}
}
}
ans += solve(mid+1, r);
memcpy(dis, tmp, sizeof(tmp));
// 右不变同理
for(int k=mid+1; k<=r; k++){
for(int i=1; i<=n; i++){
if(i==k) continue;
for(int j=1; j<=n; j++){
if(i==j || j==k) continue;
if(dis[i][k]!=-1 && dis[k][j]!=-1){
if(dis[i][j]!=-1) dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
else dis[i][j] = dis[i][k]+dis[k][j];
}
}
}
}
ans += solve(l, mid);
memcpy(dis, tmp, sizeof(tmp));
return ans;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin>>dis[i][j];
cout<<solve(1, n);
return 0;
}
P1811 最短路
看到题目是求带限制条件的最短路,想到 BFS。
当走到一个点的时候,记录四个值 \(pos, lst, dis, pre\),依次代表现在所处的点,走的上一个点,走过的距离,是从队列的几号元素转移来的(便于最后输出方案)。
这样,枚举下一个点 \(v\) 的时候,只用判断如下两个条件:
-
\(lst, pos, v\) 是否是一个被禁止的三元组。
-
把双向边拆成两条单向边后,判断这条边有没有被走过。因为这是 BFS,早标记的时间肯定不大于晚标记的时间。
最终利用 \(pre\) 去递归,输出答案即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define tiii tuple<int, int, int>
#define pii pair<int, int>
int n, m, k, tail, head;
map<tiii, bool> M;
vector<int> g[3005];
map<pii, int> Enum;
int Ecnt;
bool vis[40005];
struct node{
int pos, lst, dis, pre;
}Q[40005];
void print(int x){
if(Q[x].pre) print(Q[x].pre);
cout<<Q[x].pos<<" ";
}
void bfs(){
head = tail = 1;
Q[1].pos = 1;
while(head <= tail){
node u = Q[head];
for(auto v : g[u.pos]){
if(vis[Enum[{u.pos, v}]] || M[{u.lst, u.pos, v}]) continue;
if(v == n){
cout<<u.dis+1<<"\n";
print(head);
cout<<n;
exit(0);
}
vis[Enum[{u.pos, v}]] = 1;
Q[++tail] = {v, u.pos, u.dis+1, head};
}
head++;
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m>>k;
for(int i=1; i<=m; i++){
int u, v; cin>>u>>v;
g[u].push_back(v);
Enum[{u, v}] = ++Ecnt;
g[v].push_back(u);
Enum[{v, u}] = ++Ecnt;
}
for(int i=1; i<=k; i++){
int a, b, c; cin>>a>>b>>c;
M[{a, b, c}] = 1;
}
bfs();
cout<<"-1";
return 0;
}