【学习笔记】最短路

【学习笔记】最短路

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

以下内容有部分摘自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)\)

实现过程:

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

  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(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\) 来连通。

实现过程:

  1. \(dis(i, i) = dis(i, j) = 1\),满足自己到自己可达, \((i, j)\) 之间有关系(关系是 \(i\)\(j\))。\(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\) 均为 \(+\infty\)

  2. 然后重复操作:

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

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

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

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

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

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

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

    3. 遍历以 \(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 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\),而最短路的边数最多为 \(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\) 点可以抵达一个负环。

实现过程:

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

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

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

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

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

判断一整个图中是否有负环也需要建一个超级源点,最后看松弛次数是否 \(\ge 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;
}

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

无向图最小环

暴力解法:

\(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);

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\),树上每个节点的父亲都是它的某个前驱,则起点到每个点的最短距离,就是这棵树上这个节点到根路径的长度。这样的树就被称作最短路树。

简单来说,定义构建一棵树,使得树上任意不属于根的节点 \(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\) 的时候,只用判断如下两个条件:

  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 @ 2024-07-27 21:36  FlyPancake  阅读(73)  评论(0)    收藏  举报
// music