【学习笔记】最短路

【学习笔记】最短路

前言:只是对一些最短路算法的实现整理。

以下内容有部分摘自OI_wiki

Floyd 算法

求全源最短路。可以有负边权。

Floyd 算法的本质是动态规划。设 dis(k,i,j) 表示由若干个编号不超过 k 的节点中转后ij 的最短路。

该“动规”有两个决策,一是经过编号不超过 k1 的节点由 ij,二是由 ik,再由 kj。两者取最小值,据此可写出转移方程:

dis(k,i,j)=min(dis(k1,i,j),dis(k1,i,k)+dis(k1,k,j))

最终 ij 的最短路为 dis(n,i,j)

三维数组可能会爆内存,观察到 dis(k,i,j) 只与 k1 层有关,故可以用滚动数组的方式将第一位滚去,此时的转移方程为:

dis(i,j)=min(dis(i,j),dis(i,k)+dis(k,j))

最终 ij 的最短路为 dis(i,j)

时间复杂度为 O(n3),空间复杂度为 O(n2)

实现过程:

  1. 初始化 dis 数组所有值为正无穷。随后令 dis(i,j)=M(i,j)M(i,j) 为邻接矩阵中的值,即 ij 的边权。

  2. 三重循环跑一遍 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(k2) 的时间复杂度更新两两节点之间的最短路。

CF295B Greg and Graph

正向思路是按照题意每次删除点 xi,但 Floyd 不支持删除点,并且这样做会很麻烦:每到一个点 xi,就要做一次复杂度为 O(n3) 的 Floyd,整体复杂度为 O(n4),也会超时。

所以我们逆向思考,看成每次添加一个点 xi,并记录该点出现过。在计算总和时特判这些点是否出现过,记录答案。最后倒序输出。时间复杂度为 O(n3)

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)(dis(i,k)dis(k,j))

具体解释为 (ij) 既可以经过不大于 k1 的节点连通,也可以由 i 经过 k 中转到 j 来连通。

实现过程:

  1. dis(i,i)=dis(i,j)=1,满足自己到自己可达, (i,j) 之间有关系(关系是 ij)。dis 数组内其余均为 0

  2. 三重循环跑一遍 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 集合。

  1. 初始化 dis(s)=0,其他点的 dis 均为 +

  2. 然后重复操作:

    T 集合中,选取一个最短路长度最小的结点,移到 S 集合中。对那些刚刚被加入 S 集合的结点的所有出边执行松弛操作。直到 T 集合为空,算法结束。

有多种方法来维护操作中最短路长度最小的结点,不同的实现导致了 Dijkstra 算法时间复杂度上的差异。

这里主要讲堆优化的 Dijkstra 的实现过程 (因为只会这个)

  1. 初始化 dis(s)=0,其他点的 dis 均为 +。并且将起点 s 以及它的距离 0 入队。

  2. 重复以下操作直至队列(堆)为空:

    1. 取出堆顶元素,并出队。堆是以当前元素的 dis 尽量小为关键字。

    2. 如果已经访问过这个点 u(堆顶元素对应的),则无视它进入下一次循环(因为 Dijkstra 算法基于贪心的思想已经更新了经过该点的最短路)。否则标记该点 u 为已访问。

    3. 遍历以 u 为起点的边,记终点为 vw(u,v) 的边权,如果满足 dis(v)>dis(u)+w,则进行松弛操作,并将该元素和其对应的 dis 入队。

时间复杂度为 O(mlogm)

以下为模板题的代码实现:

#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 va > x.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,而最短路的边数最多为 n1,因此整个算法最多执行 n1 轮松弛操作。故求其的时间复杂度为 O(nm)

还有一种情况,如果从 S 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 n1 轮,因此如果第 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 点可以抵达一个负环。

实现过程:

  1. 初始化 dis(s)=0,vis(s)=1,其他点的 dis 均为 +vis 均为 0。并且将起点 s 入队。

  2. 重复以下操作直至队列为空:

    1. 取出队头元素 u,并出队。并且标记 vis(u)=0

    2. 遍历以 u 为起点的边,记终点为 vw(u,v) 的边权,如果满足 dis(v)>dis(u)+w,则进行松弛操作。满足可松弛操作时 v 不在队列内(vis[v]=0)则将 v 入队,并标记 vis[v]=1

想要判负环的话加个统计,看最短路经过的边数是否 n

判断一整个图中是否有负环也需要建一个超级源点,最后看松弛次数是否 n+1

代码实现(判负环):

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;
}

以下为一些关于图的应用...

无向图最小环

暴力解法:

uv 之间有一条边长为 w 的边,dis(u,v) 表示删除 uv 之间的连边之后,uv 之间的最短路。

那么无向图中的最小环是 dis(u,v)+w

注意若是在有向图中求最小环,相对应的公式要修改,最小环是 dis(v,u)+w

总时间复杂度 O(n2m)

Floyd:

记原图中 u,v 之间边的边权为 val(u,v)

我们注意到 Floyd 算法有一个性质:在最外层循环到点 k 时(尚未开始第 k 次循环),最短路数组 dis 中,disu,v 表示的是从 uv 且仅经过编号在 [1,k) 区间中的点的最短路。

由最小环的定义可知其至少有三个顶点,设其中编号最大的顶点为 w,环上与 w 相邻两侧的两个点为 u,v,则在最外层循环枚举到 k=w 时,该环的长度即为 disu,v+val(v,w)+val(w,u)

故在循环时对于每个 k 枚举满足 i<k,j<k(i,j),更新答案即可。

总时间复杂度为 O(n3)

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)logn)

差分约束

差分约束系统是一种特殊的 n 元一次不等式组,它包含 n 个变量 x1,x2,,xn 以及 m 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如 xixjck,其中 1i,jn,ij,1km 并且 ck常数。我们要解决的问题是:求一组解 x1=a1,x2=a2,,xn=an,使得所有的约束条件得到满足,否则判断出无解。

差分约束系统中的每个约束条件 xixjck 都可以变形成 xixj+ck,这与单源最短路中的三角形不等式 dvdu+w<u,v> 非常相似。因此,我们可以把每个变量 xi 看做图中的一个结点,对于每个约束条件 xixjck,从结点 j 向结点 i 连一条长度为 ck 的有向边。

注意到,如果 {a1,a2,,an} 是该差分约束系统的一组解,那么对于任意的常数 d{a1+d,a2+d,,an+d} 显然也是该差分约束系统的一组解,因为这样做差后 d 刚好被消掉。

过程:

dis[0]=0 并向每一个点连一条权重为 0 的边,跑单源最短路,若图中存在负环,则给定的差分约束系统无解,否则,xi=dis[i] 为该差分约束系统的一组解。

一些转化技巧:

题意 转化 连边
xaxbc xbxac add(a, b, -c);
xaxbc xaxbc add(b, a, c);
xa=xb xaxb0,xbxa0 add(b, a, 0), add(a, b, 0);

P1993 小 K 的农场

比较全面的模板题。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5005;

struct node{
    int v, w;
};
vector<node> g[N];
bool inque[N];
int cnt[N], dis[N];
int n, m;

bool spfa(int s){
    memset(dis, 0x3f, sizeof(dis));
    queue<int> Q;
    Q.push(s); inque[s] = true;
    dis[s] = 0;
    while(!Q.empty()){
        int u = Q.front();
        Q.pop(); inque[u] = false;
        for(auto [v, w] : g[u]){
            if(dis[v] > dis[u]+w){
                dis[v] = dis[u]+w;
                cnt[v] = cnt[u]+1;
                if(cnt[v]>=n+1) return false;
                if(!inque[v])
                    Q.push(v), inque[v] = true;
            }
        }
    }
    return true;
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin>>n>>m;
    while(m--){
        int op; cin>>op;
        if(op==1){
            // b<=a-c   dis[v]<dis[u]+{u->v}
            int a, b, c; cin>>a>>b>>c;
            g[a].push_back({b, -c});
        } else if(op==2){
            // a<=b+c
            int a, b, c; cin>>a>>b>>c;
            g[b].push_back({a, c});
        } else{
            int a, b; cin>>a>>b;
            // a=b a>=b b>=a
            g[b].push_back({a, 0});
            g[a].push_back({b, 0}); 
        }
    }
    for(int i=1; i<=n; i++){
        g[0].push_back({i, 0});
    }
    cout<<(spfa(0) ? "Yes" : "No");
    return 0;
}

最短路树

在单源最短路的过程中,每个点 v 都存在一些前驱 u,满足 dis(u)+w(u,v)=dis(v),这里 w(u,v)u,v 两点之间最短的一条边的边权。

如果我们得到一棵树,根节点为单源最短路的起点 s,树上每个节点的父亲都是它的某个前驱,则起点到每个点的最短距离,就是这棵树上这个节点到根路径的长度。这样的树就被称作最短路树。

简单来说,定义构建一棵树,使得树上任意不属于根的节点 xdis(root,x) 就是原图起点 sx 的最短路。

实现过程:

要得到最短路树,首先需要知道每个点的前驱。
在 Dijkstra 的过程中,我们只需要记录每个点 v 最后一次被松弛的时候是被哪个点 u 松弛的,则这个 u 就是 v 的前驱。
然后把每个节点的父亲设置成它的前驱就行了。

注意到这样做必定是没有环的。因为将节点出队顺序看作一个拓扑序,节点之间的前驱关系是符合这个拓扑序的。也就是说每个点的前驱必然比它早出队。因此即使图上有零权环也没关系。


P3556 [POI2013] MOR-Tales of seafaring

题意:给 n 个点 m 条边无向图,每次询问两个点之间是否有长度为 d 的路径(不一定是简单路径)。

因为不一定是简单路径,所以可以在一条边上反复横跳,这样只要分别记录 uv 的经过奇数条路径的最短路和经过偶数条路径的最短路。查看与 k 同奇偶的那条路径的最短路是否 k

如果一个点是孤立的,那么怎么走都无解!

思路一(重要):分层图

考虑拆点。将 (u,v) 的路径拆成 (u,v+n)(u+n,v),这样当当前点为 {1,2,,n} 时,所在层数即为经过偶数条路径能到达的一层,当当前点为 {n+1,n+2,,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 的距离为 Gi,j,特别地,Gi,j=1 表示 ij 不可达。定义 dis(u,v,w)u 出发到 v 且不经过 w 的最短路径。(不能到达则为 1)求:

u=1nv=1nw=1ndis(u,v,w)。其中 3n300,1Gi,j10000,Gi,i=0

思路:分治 Floyd。

每次递归时计算 dis(l,r) 表示不使用 [l,r] 中的点条件下的两两之间最短路。向下递归时,将 [mid+1,r] 的点加入后递归计算 dis(l,mid),同理 [l,mid] 的点加入后递归计算dis(mid+1,r)

l=r时统计答案即可。

由主定理 T(N)=Nn2+2T(N/2) 可以得到时间复杂度为 O(n3logn)

trick:zzy 称其为时间线段树。撤销 删除。有些题目撤销往往比删除好维护非常多。

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 的时候,只用判断如下两个条件:

  1. lst,pos,v 是否是一个被禁止的三元组。

  2. 把双向边拆成两条单向边后,判断这条边有没有被走过。因为这是 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;
}
posted @   FlyPancake  阅读(43)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
// music
点击右上角即可分享
微信分享提示