最短路

最短路

Floyd 算法

适用于无负环的图。

思路:枚举所有点对 \((i, j)\) 以及中转点 \(k\) ,再对邻接矩阵进行松弛操作。

时间复杂度 $ O(n^{3}) $ ,可以一次求出任意两点最短路。

inline void Floyd() {
    for (int k = 1; k <= n; ++k) // 枚举中转点
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                a[i][j] = min(a[i][j], a[i][k] + a[k][j]);
}

bitset 优化传递闭包

用 Floyd 转递闭包时可以用 bitset 优化,时间复杂度 \(O(\dfrac{n^{3}}{\omega})\)​ 。

for (int k = 1; k <= n; ++k)
    for (int i = 1; i <= n; ++i)
        if (f[i][k])
            f[i] = f[i] | f[k];

应用

P1119 灾后重建

给出一张无向图,第 \(i\) 个点在 \(t_i\) 时刻被修复,若 \(t_i = 0\)\(i\) 未损坏。

\(q\) 次询问,每次询问 \(t\) 时刻 \(x\)\(y\) 的最短路,保证给出的 \(t\) 不降。

\(n \leq 200\)

用 Floyd 求最短路,按修复时间枚举中转点松弛即可。

P6175 无向图的最小环问题

给一个正权无向图,找一个最小权值和的环。

枚举中转点 \(k\) 时,我们已经得到了前 \(k-1\) 个点的最短路径。\(x \rightsquigarrow y\)\(y \to k\)\(k \to x\) 共同构成了环,所以连接起来就得到了一个经过 \(x , y , k\) 的最小环。

Bellman–Ford 算法

首先介绍一下松弛操作:\(dis_v \leftarrow \min(dis_v, dis_u + w(u, v))\)

该算法不断尝试对图上每一条边进行松弛。由于每次松弛成功都会使得最短路长度增加 \(1\) ,所以循环 \(n - 1\) 次即可求出最短路。

时间复杂度 \(O(nm)\)

但还有一种情况,如果从 \(S\) 出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 \(n - 1\) 轮。因此若第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发能够抵达一个负环,但不能说明图中不存在负环。

SPFA 算法

经过队列优化的 Bellman-Ford 算法。

Bellman-Ford 算法中很多点是不用松弛的,只有上一次被松弛的结点所连接的边才有可能引起下一次松弛操作。

于是用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。

若要判负环,则记录一下每个点的松弛次数(即入队次数)即可。

SPFA 算法在随机图上时间复杂度为 \(O(km)\) ,但是可以被卡到 \(O(nm)\)

inline bool SPFA(int s) {
    fill(dis + 1, dis + 1 + n, inf);
    queue<int> q;
    dis[s] = 0, q.emplace(s), inque[s] = true, cnt[s] = 1;

    while (!q.empty()) {
        int u = q.front();
        q.pop(), inque[u] = false;
        
        if (cnt[u] == n - 1)
            return false;

        for (int it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;

                if (!inque[v])
                    q.emplace(v), inque[v] = true, ++cnt[v];
            }
        }
    }

    return true;
}

一般来说判负环的时候用 dfs 版的 SPFA 更快

bool SPFA(int u) {
    vis[u] = true;
    
    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = e[i].v, w = e[i].w;
        
        if (dis[v] > dis[u] + w) {
            dis[v] = dis[u] + w;
            
            if (vis[v] || !SPFA(v))
            	return false;
        }
    }
    
    return vis[u] = false, 1;
}

优化

比较弱的优化:

  • LLL 优化:使用双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则从队头插入。
  • SLF 优化:使用双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则从队头插入。

强一点的优化:

  • SLF 带容错:每次将入队结点距离和队首比较,如果比队首大超过一定值则插入至队尾,否则从队头插入。
  • mcfx 优化:定义区间 \([l, r]\) ,当入队点入队次数属于这个区间时从队首插入,否则从队头插入。通常取 \([2, \sqrt{n}]\)
  • SLF + swap:每当队列改变时,如果队首距离大于队尾,则交换首尾。
    • 这个 SLF 看起来很弱,但却通过了所有 Hack 数据,而且非常难卡。

玄学优化:

  • 随机打乱边。
  • 以一定概率从队首/队尾插入。
  • 入队次数一定周期就随机打乱队列。

Hack

原理:让点尽可能多次入队,反复更新。

普通 SPFA 很好卡:

  • 随机网格图:如果在网格图中走错了一次,就会走很多步无用步,于是就死了。
  • 一个构造过的链套菊花:如果一个点被多个边连着,那么当这些边先依次走向它的时候,这个点就会被调用很多次,然后往下走。于是对于菊花的那个点会反复入队很多次,然后往下每次都要走很多次,于是就死了。

结合两种卡法,对于普通的 SPFA 最好的卡法就是将图构造为一个网格套链套菊花。

针对一些优化的 Hack 方法:

  • LLL 优化:向 \(1\) 连一条权值巨大的边。
  • SLF 优化:链套菊花,在链上用几个并列在一起的小边权边让它多次进入菊花。
  • SLF 带容错:类似卡 SLF 的做法,注意要开打大边权总和才能有一定效果。
  • mcfx 优化:菊花图。
  • SLF + swap:与卡 SLF 类似,外挂诱导节点即可。但是卡的难度略大。

Dijkstra 算法

将结点分成两个集合:已确定最短路长度的点集 \(S\) 的和未确定最短路长度的点集 \(T\)

初始时所有的点都属于 \(T\) ,令 \(dis_s = 0\) ,其它点的 \(dis\) 均为 \(+ \infty\)

重复操作直到 \(T\) 为空:从 \(T\) 中选一个 \(dis\) 最小的点移到 \(S\) 中,并用该点松弛其它点。

Dijkstra 算法只能解决正权图上的最短路问题问题。

具体实现:

  • 暴力:每次暴力找到 \(dis\) 最小的点松弛其它点,时间复杂度 \(O(n^2 + m) = O(n^2)\)
  • 优先队列:每次松弛 \((u, v)\) 后将 \(v\) 插入优先队列中,每次从优先队列中选 \(dis\) 最小的点松弛其它点。由于不能在优先队列中删除元素,所以取出时要判重,时间复杂度 \(O(m \log n)\)
  • 线段树:基本不用,将上面的操作改为单点修改和全局查询最小值,时间复杂度 \(O(m \log n)\)

优先队列优化的 Dijkstra 的实现:

inline void Dijkstra(int S) {
    fill(dis + 1, dis + 1 + n, inf);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        int u = q.top().second;
        q.pop();

        if (vis[u])
            continue;

        vis[u] = true;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

另一种写法(取消了 \(vis\) 数组):

inline void Dijkstra(int S) {
    fill(dis + 1, dis + 1 + n, inf);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (dis[c.second] != -c.first)
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

应用

给定一张图,每条边 \(s, t\) 有两个权值 \(w, v\) ,分别表示 \((s, t, w)\)\((t, s, v)\) 的有向边,求边权和最小的包含 \(1\) 的简单环。

解法一:考虑对 \(1\) 的出点进行二进制拆分,每次按位将其拆为两个集合,将其中一个集合内的点向 \(n + 1\) 连边,并标记这条边不能走,那么一个环就被拆为 \(1\)\(n + 1\) 的一条路径,不难证明一定有一种拆分方案满足原图中和 \(1\) 相连的两条边一定在新图中分别与 \(1, n + 1\) 相连。时间复杂度 \(O(m \log m \log n)\)

解法二:考虑一个简单环 \(S \to x \to y \to S\) ,且 \(S \to x\)\(y \to S\) 不交。为了使答案最优,\(S \to x\)\(y \to S\) 应当在满足无交集的情况下长度最短。先求出 \(S \to x\) 的最短路 \(d(x)\) 以及走的第一个点 \(p(x)\) ,再在反图上跑一边相同的流程,记为 \(d(y)\)\(rp(y)\) 。接下来枚举所有有向边 \((u, v, w)\)

  • \(u = S\) :不操作,因为这种情况会被其他情况覆盖。
  • \(v = S\)
    • \(p(u) \neq u\) :则用 \(d(u) + w\) 更新答案。
    • \(p(u) = u\) :则 \(S \to u\) 走最短路径不合法,要经过其他的边,这种情况会被其他情况覆盖。
  • 否则:
    • \(p(u) \neq rp(v)\) :则用 \(d(u) + w + rd(v)\) 更新答案。
    • \(p(u) = rp(v)\) :则 \(S \to u\)\(v \to S\) 走最短路径有重叠,也要走其他的路径,这种情况也会被其他情况覆盖。

上面的更新方式可以覆盖所有的简单环情况,所以不会漏过然后一个可能的答案。同时对于经过某条边且可以更新的简单环情况,其长度是最优的。时间复杂度 \(O(m \log m)\)

Johnson 全源最短路

如果没有负权边,那直接跑 \(n\) 次 Dijkstra 即可。下面考虑怎么处理负权边。

建一个超级源点,所有点与其连一条边权为 \(0\)​ 的边。先用 SPFA 求每个点与超级源点的最短路径长度 \(h_i\)

将每条边 \(u \to v\) 的边权增加 \(h_u-h_v\) ,最后统计 \(i \to j\) 的最短路时减去 \(h_i - h_j\) 即可,于是就能直接跑 \(n\) 次 Dijkstra 了。

时间复杂度 \(O(km + nm \log m)\)

P5905 【模板】Johnson 全源最短路

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 1e9;
const int N = 3e3 + 7;

struct Graph {
	vector<pair<int, int> > e[N];
	
	inline void insert(const int u, const int v, const int w) {
		e[u].emplace_back(v, w);
	}
} G;

int dis[N][N];
int h[N], cnt[N];
bool inque[N];

int n, m;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline bool SPFA() {
	fill(h + 1, h + 1 + n, inf);
	queue<int> q;
	q.emplace(0), inque[0] = true, ++cnt[0];

	while (!q.empty()) {
		int u = q.front();
		q.pop(), inque[u] = false;

		if (cnt[u] == n - 1)
			return false;

		for (auto it : G.e[u]) {
			int v = it.first, w = it.second;

			if (h[v] > h[u] + w) {
				h[v] = h[u] + w;

				if (!inque[v])
					q.emplace(v), inque[v] = true, ++cnt[v];
			}
		}
	}

	return true;
}

inline void Dijkstra(int S, int *dis) {
	fill(dis + 1, dis + 1 + n, inf);
	priority_queue<pair<int, int> > q;
	dis[S] = 0, q.emplace(0, S);

	while (!q.empty()) {
		auto c = q.top();
		q.pop();

		if (-c.first != dis[c.second])
			continue;

		int u = c.second;

		for (auto it : G.e[u]) {
			int v = it.first, w = it.second + h[u] - h[v];

			if (dis[v] > dis[u] + w)
				dis[v] = dis[u] + w, q.emplace(-dis[v], v);
		}
	}
}

inline bool Johnson() {
	for (int i = 1; i <= n; ++i)
		G.insert(0, i, 0);

	if (!SPFA())
		return false;

	for (int i = 1; i <= n; ++i) {
		Dijkstra(i, dis[i]);

		for (int j = 1; j <= n; ++j)
			if (dis[i][j] != inf)
				dis[i][j] -= h[i] - h[j];
	}

	return true;
}

signed main() {
	n = read(), m = read();

	for (int i = 1; i <= m; ++i) {
		int u = read(), v = read(), w = read();
		G.insert(u, v, w);
	}

	if (!Johnson())
		return puts("-1"), 0;

	for (int i = 1; i <= n; ++i) {
		ll res = 0;

		for (int j = 1; j <= n; ++j)
			res += 1ll * j * dis[i][j];

		printf("%lld\n", res);
	}

	return 0;
}

BFS 相关

在一些特殊的图上,可以用 BFS 求解最短路做到 \(O(n + m)\) 的时间复杂度。

  • 无权图上的最短路直接用 BFS 求解即可。
  • 01BFS:若边权仅有 \(0\)\(1\) ,考虑有 deque 维护 BFS ,若走的边权为 \(0\) 则从队首入队,若走的边权为 \(1\) 则从队尾入队。

应用

CF173B Chamber of Secrets

一个 \(n \times m\) 的图,现在有一束激光从左上角往右边射出,每遇到 # ,你可以选择光线往四个方向射出,或者什么都不做。

问最少需要多少个 # 往四个方向射出才能使光线在第 \(n\) 行往右边射出。

\(n, m \leq 1000\)

将柱子改为 # 后,一条光线经过的时候实际效果是该行该列都会有光线。于是视该操作代价为 \(1\) 跑 BFS 即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e3 + 7;

struct Graph {
	vector<int> e[N];
	
	inline void insert(int u, int v) {
		e[u].emplace_back(v);
	}
} G;

queue<int> q;

int dis[N];
char str[N];
bool vis[N];

int n, m;

inline void bfs() {
	memset(dis + 1, inf, sizeof(int) * (n + m));
	dis[1] = 0, q.emplace(1), vis[1] = true;

	while (!q.empty()) {
		int u = q.front();
		q.pop(), vis[u] = true;

		for (int v : G.e[u])
			if (!vis[v])
				dis[v] = dis[u] + 1, q.emplace(v), vis[v] = true;
	}
}

signed main() {
	scanf("%d%d", &n, &m);

	for (int i = 1; i <= n; ++i) {
		scanf("%s", str + 1);

		for (int j = 1; j <= m; ++j)
			if (str[j] == '#')
				G.insert(i, j + n), G.insert(j + n, i);
	}

	bfs();
	printf("%d", dis[n] == inf ? -1 : dis[n]);
	return 0;
}

次短路

考虑每一条非最短路上的边 \(u \to v\) ,答案即为:

\[\min (dis_{1, u} + w(u, v) + dis_{v, n}) \]

\(dis_{1, u}, dis_{v, n}\) 建立正反图跑两次 Dijkstra 即可求得。

求严格次短路时,我们不必记录最短路的路径,只需枚举每条边,若路径长度严格小于最短路时更新答案即可。

另一种方式是对于每个点都记录一下最短路与次短路,只要被更新就去松弛别的点。

P2865 [USACO06NOV] Roadblocks G

注意本题求的是严格次短路,下面给出第二种方法的实现。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e3 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

int dis[N][2];

int n, m;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

inline void Dijkstra(int S) {
    memset(dis, inf, sizeof(dis));
    priority_queue<pair<int, int> > q;
    dis[S][0] = 0, q.emplace(-dis[S][0], S);

    while (!q.empty()) {
        int d = -q.top().first, u = q.top().second;
        q.pop();

        if (d != dis[u][0] && d != dis[u][1])
            continue;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v][0] > d + w)
                dis[v][1] = dis[v][0], dis[v][0] = d + w, q.emplace(-dis[v][0], v);
            else if (dis[v][0] < d + w && dis[v][1] > d + w)
                dis[v][1] = d + w, q.emplace(-dis[v][1], v);
        }
    }
}

signed main() {
    n = read(), m = read();

    for (int i = 1; i <= m; ++i) {
        int u = read(), v = read(), w = read();
        G.insert(u, v, w), G.insert(v, u, w);
    }

    Dijkstra(1);
    printf("%d", dis[n][1]);
    return 0;
}

P1491 集合位置

注意本题选取的第二短路径不会重复经过同一条路,所以只能把最短路上的边依次删去然后跑最短路。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e2 + 7;

struct Graph {
	struct Edge {
		int nxt, v;
		double w;
	} e[N * N];
	
	int head[N];
	
	int tot;
	
	inline void insert(int u, int v, double w) {
		e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
	}
} G;

struct Point {
	int x, y;
} a[N];

pair<int, int> pre[N];

double dis[N];

int n, m;

inline double dist(Point a, Point b) {
	return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline double Dijkstra(int S, int restriction) {
	priority_queue<pair<double, int> > q;
	fill(dis + 1, dis + 1 + n, 1e9);
	dis[S] = 0, q.push(make_pair(-dis[S], S));
	
	while (!q.empty()) {
		auto c = q.top();
		q.pop();
		
		if (-c.first != dis[c.second])
			continue;
		
		int u = c.second;
		
		for (int i = G.head[u]; i; i = G.e[i].nxt) {
			if (i == restriction)
				continue;
			
			int v = G.e[i].v;
			double w = G.e[i].w;
			
			if (dis[u] + w < dis[v]) {
				dis[v] = dis[u] + w;
				
				if (restriction == -1)
					pre[v] = make_pair(u, i);
				
				q.push(make_pair(-dis[v], v));
			}
		}
	}
	
	return dis[n];
}

signed main() {
	n = read(), m = read();
	
	for (int i = 1; i <= n; ++i)
		a[i].x = read(), a[i].y = read();
	
	for (int i = 1; i <= m; ++i) {
		int u = read(), v = read();
		G.insert(u, v, dist(a[u], a[v])), G.insert(v, u, dist(a[u], a[v]));
	}
	
	Dijkstra(1, -1);
	double ans = 1e9;
	
	for (int cur = n; cur != 1; cur = pre[cur].first)
		ans = min(ans, Dijkstra(1, pre[cur].second));
	
	printf("%.2lf", ans);
	return 0;
}

最短路图

即求出所有最短路(多条也算)组成的 DAG,只需将 \(dis_v = dis_u + w\) 的边连边即可

「ROI 2017 Day 1」前往大都会

某国有 \(n\) 座城市与 \(m\) 条单向铁路线,构成一张连通图。第 \(i\) 条单向铁路线由 \(v_{i, 1}, v_{i, 2}, \cdots, v_{i, s_i + 1}\) 城市组成,城市 \(v_{i, j}\) 通过该线路到城市 \(v_{i, j + 1}\) 花费的时间为 \(t_{i, j}\)

\(1\)\(n\) 花费时间最少的情况下,经过任意两个相邻城市所花费时间的平方和的最大值。

\(n, m \leq 10^6\)

首先求出最短路图,那么只要在最短路图上找到平方和最大的路径。

这里的最短路图是 DAG, 于是可以按拓扑序设计 DP 。

\(dp_x\) 表示以 \(x\) 为终点的最大权值,枚举上一个换乘点,有:

\[f_x = \max_y \{ f_y + (d_x - d_y)^2 \} = d_x^2 + \max_y \{ d_y^2 - 2d_x d_y + f_y \} \]

斜率优化即可,复杂度瓶颈为最短路。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e6 + 7, S = 2e6 + 7;

struct Graph {
	vector<pair<int, int> > e[N];
	
	inline void insert(int u, int v, int w) {
		e[u].emplace_back(make_pair(v, w));
	}
} G, nG;

vector<pair<int, int> > belong[N];
vector<int> City[N], Time[N], ts[N], sta[S];

ll f[N];
int s[N], dis[N], id[N];

int n, m;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = c == '-';
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= c == '-';
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline void Dijkstra(int S) {
	fill(dis + 1, dis + 1 + n, inf);
	priority_queue<pair<int, int> > q;
	dis[S] = 0, q.push(make_pair(-dis[S], S));
	
	while (!q.empty()) {
		auto c = q.top();
		q.pop();
		
		if (-c.first != dis[c.second])
			continue;
		
		int u = c.second;
		
		for (auto it : G.e[u]) {
			int v = it.first, w = it.second;
			
			if (dis[v] > dis[u] + w)
				dis[v] = dis[u] + w, q.push(make_pair(-dis[v], v));
		}
	}
}

inline void prework() {
	int cnt = 0;
	
	for (int i = 1; i <= m; ++i) {
		ts[i].resize(s[i] + 1);
		
		for (int j = 0; j <= s[i]; ++j) {
			if (!j) {
				ts[i][j] = ++cnt;
				continue;
			}
			
			int u = City[i][j - 1], v = City[i][j], w = Time[i][j - 1];
			
			if (dis[u] + w > dis[v])
				ts[i][j] = ++cnt;
			else
				ts[i][j] = ts[i][j - 1];
		}
	}
}

inline bool cmp(const int &x, const int &y) {
	return dis[x] < dis[y];
}

inline ll slope(int x, int d) {
	return -2ll * dis[x] * d + 1ll * dis[x] * dis[x] + f[x];
}

inline bool check(int a, int b, int c) {
	ll ka = -2ll * dis[a], kb = -2ll * dis[b], kc = -2ll * dis[c];
	ll ta = 1ll * dis[a] * dis[a] + f[a];
	ll tb = 1ll * dis[b] * dis[b] + f[b];
	ll tc = 1ll * dis[c] * dis[c] + f[c];
	return (__int128) (tc - ta) * (ka - kb) >= (__int128) (tb - ta) * (ka - kc);
}

signed main() {
	n = read(), m = read();
	
	for (int i = 1; i <= m; ++i) {
		s[i] = read();
		int u = read();
		City[i].emplace_back(u);
		belong[u].emplace_back(make_pair(i, 0));
		
		for (int j = 1; j <= s[i]; ++j) {
			int w = read(), v = read();
			G.insert(u, v, w);
			City[i].emplace_back(v), Time[i].emplace_back(w);
			belong[v].emplace_back(make_pair(i, j));
			u = v;
		}
	}
	
	Dijkstra(1), prework();
	
	for (int i = 1; i <= n; ++i)
		id[i] = i;
	
	sort(id + 1, id + 1 + n, cmp);
	
	for (auto it : belong[1])
		sta[ts[it.first][it.second]].emplace_back(1);
	
	for (int i = 2; i <= n; ++i)
		if (dis[id[i]] < inf) {
			int x = id[i];
			
			for (auto it : belong[x]) {
				int ns = ts[it.first][it.second];
				
				if (sta[ns].empty())
					continue;
				
				while (sta[ns].size() >= 2 && slope(sta[ns][sta[ns].size() - 2], dis[x]) >= slope(sta[ns][sta[ns].size() - 1], dis[x]))
					sta[ns].pop_back();
				
				f[x] = max(f[x], slope(sta[ns][sta[ns].size() - 1], dis[x]) + 1ll * dis[x] * dis[x]);
			}
			
			for (auto it : belong[x]) {
				int ns = ts[it.first][it.second];
				
				if (!sta[ns].empty() && slope(sta[ns][sta[ns].size() - 1], dis[x]) >= slope(x, dis[x]))
					continue;
				
				while (sta[ns].size() >= 2 && check(sta[ns][sta[ns].size() - 2], sta[ns][sta[ns].size() - 1], x))
					sta[ns].pop_back();
					
				sta[ns].emplace_back(x);
			}
		}
	
	printf("%d %lld", dis[n], f[n]);
	return 0;
}

最短路径树(SPT)

即由最短路径组成的树,和最短路图的区别就是少了几条边。可以通过求解最短路时记录每个点的前驱更新节点求得。

但是很多情况下要求边权和最小。可以考虑贪心,在松弛时若遇到松弛前后边权相等时取边权较小者即可。

模板题:CF545E Paths and Trees

应用

CF1076D Edge Deletion

给定一张无向简单带权连通图, 要求删边至最多剩余 \(k\) 条边,最大化删边后满足 \(1\) 到其最短路不变的点的数量。

建出 SPT 后从 \(1\) 开始找一个大小为 \(k + 1\) 的连通块即可。

CF1005F Berland and the Shortest Paths

给出一张无向无边权简单连通图,求 SPT 方案数与方案(若超过 \(k\) 种则只取 \(k\) 种即可)。

然后对每个点维护可能成为前驱节点的集合,此时总的方案数就是所有集合大小的乘积,求解方案直接暴力从每个集合中选一个元素组合即可。

差分约束系统

差分约束系统是一种特殊的 \(n\) 元一次不等式组。每个不等式都形如 \(x_i - x_j \leq c_k\) ,其中 \(c_k\) 为常数且 \(i \not = j\) 。需要求出一组整数解。

将每个不等式都转化为 \(x_i \leq x_j + c_k\) ,这与三角形不等式 \(dis_v \leq dis_u + w\) 十分相似。那么对于一组不等式 \(x_v - x_u \leq w\) ,建边 \((u, v, w)\)

从超级源点向每个点连一条边权为 \(0\) 的边,若建图后图中有负环则方程组无解,否则 \(x_i = dis_i\) 就是方程组的一组解。

\(\{ x_1, x_2, \cdots, x_n \}\) 是方程的一组解,则 \(\{ x_1 + d, x_2 + d, \cdots, x_n + d \}\) 也是方程的一组解。

tricks:

  • \(x_i - x_j < c_k\) 可以转化为 \(x_i - x_j \leq c_k - 1\)
  • \(x_i = x_j\) 可以转化为 \(x_i - x_j \leq 0\)\(x_j - x_i \leq 0\)

差分约束系统的一个性质:如果跑的是最短路,则固定一个值时,其余的值都会取到最大值。

\(v_0 \to v_u\) 经过的路径为

\[v_{i_1} - v_0 \leq l_0 \\ v_{i_2} - v_{i_1} \leq l_1 \\ \cdots \\ v_u - v_{i_k} \leq l_k \]

则加起来得到 \(v_u - v_0 \leq l_0 + l_1 + \cdots + l_k = dist(0, u)\)

模板:P1993 小 K 的农场 P3275 [SCOI2011] 糖果

应用

P4926 [1007] 倍杀测量者

给出 \(m\) 组不等式:

\[\begin{cases} x_{a_i} \geq (k_i - t) \times x_{b_i} \\ (k_i + t) \times x_{a_i} \geq x_{b_i} \end{cases} \]

求最大使得不等式组无解的 \(t\)

\(n, m \leq 1000\)

考虑用对数将乘法化为加减:

\[\ln{x_{b_i}} - \ln{x_{a_i}} \leq \min(-\ln{(k_i - t)}, \ln{(k_i + t)}) \]

二分 \(t\) 跑 SPFA 判断有无负环即可。

P2474 [SCOI2008] 天平

\(n\) 个砝码,分别重 \(x_{1 \sim n}\) ,给出一些重量大小关系,求有多少对 \(x_a, x_b, x_c, x_d\) 一定满足:

  • \(x_a + x_b > x_c + x_d\)
  • \(x_a + x_b = x_c + x_d\)
  • \(x_a + x_b < x_c + x_d\)

分别求解三种情况的方案数。

\(n \leq 50\)

建立差分约束系统后,先跑一边最短路和最长路,求出 \(i, j\) 之间的质量差最小值 \(mn_{i, j}\) 和最大值 \(mx_{i, j}\)

转化一下三种情况:

  • \(x_a - x_c > x_b - x_d\) ,即 \(mn_{a, c} > mx_{b, d}\)
  • \(x_a - x_c = x_b - x_d\) ,即 \(mn_{a, c} = mx_{a, c} = mn_{b, d} = mx_{b,d}\)
  • \(x_a - x_c < x_b - x_d\) ,即 \(mn_{a, c} < mx_{b, d}\)

分别统计方案数即可。

[AGC056C] 01 Balanced

构造长度为 \(n\) 的字典序最小的 \(01\) 字符串,满足 \(m\) 组子串 \([l_i, r_i]\) 含相同数量的 \(0\)\(1\)

\(n \leq 10^6, m \leq 2 \times 10^5\) ,保证 \(r- l + 1\) 是偶数

\(0\) 当作 \(1\)\(1\) 当作 \(-1\) 。因为要让答案的字典序最小,即 \(s_i\) 尽可能大,即需要求出字典序最大的一组解,于是可以用差分约束系统求解。

相邻两个位置的限制从 \(|s_i - s_{i - 1}| = 1\) 弱化为 \(|s_i - s_{i - 1}| \leq 1\)

不可能存在 \(s_{i - 1} = s_i\) :若存在可以构造出 \(\pm 1\) 交错的 \(s\) 使得字典序更大。

对于一组限制,转化为 \(s_{l - 1} = s_r\)

于是 01bfs 即可求解。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;

struct Graph {
	vector<pair<int, int> > e[N];
	
	inline void insert(int u, int v, int w) {
		e[u].emplace_back(v, w);
	}
} G;

int dis[N];

int n, m;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline void bfs() {
	fill(dis, dis + 1 + n, -1);
	deque<int> q;
	dis[0] = 0, q.emplace_back(0);

	while (!q.empty()) {
		int u = q.front();
		q.pop_front();

		for (auto it : G.e[u]) {
			int v = it.first, w = it.second;

			if (dis[v] == -1) {
				dis[v] = dis[u] + w;

				if (w)
					q.emplace_back(v);
				else
					q.emplace_front(v);
			}
		}
	}
}

signed main() {
	n = read(), m = read();

	for (int i = 1; i <= n; ++i)
		G.insert(i - 1, i, 1), G.insert(i, i - 1, 1);

	for (int i = 1; i <= m; ++i) {
		int l = read(), r = read();
		G.insert(l - 1, r, 0), G.insert(r, l - 1, 0);
	}

	bfs();

	for (int i = 1; i <= n; ++i)
		putchar(dis[i] < dis[i - 1] ? '1' : '0');
	
	return 0;
}

同余最短路

同余最短路利用同余来构造一些状态,并将其看作单源最短路中的点。

P3403 跳楼机

给出 \(x, y, z, h\) ,求有多少 \(k \in [1, h]\) 满足 \(ax + by + cz = k\)

\(x, y, z \leq 10^5, h \leq 2^{63} - 1\)

不妨设 \(x < y < z\)

\(d_i\) 表示仅通过 \(by + cz\) 后能得到的模 \(x\) 下与 \(i\) 同余的最小数,用来计算该同余类满足条件的数个数。可以建边:\((i, (i + y) \bmod x, y), (i, (i + z) \bmod x, z)\) ,于是跑一次最短路即可求出 \(d_i\)

\(1\) 作为源点,此时 \(dis_1 = 1\) 最小,即可得到最小的一组解,类比差分约束即可得到所有解。

答案即为:

\[\sum_{i = 0}^{x - 1} (\dfrac{h - d_i}{x} + 1) \]

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = (1ull << 63) - 1;
const int N = 1e5 + 7;

struct Graph {
	vector<pair<int, int> > e[N];
	
	inline void insert(int u, int v, int w) {
		e[u].emplace_back(v, w);
	}
} G;

ll dis[N];

ll h, x, y, z;

inline void Dijkstra() {
	fill(dis, dis + x, inf);
	priority_queue<pair<ll, int> > q;
	dis[0] = 0, q.emplace(-dis[0], 0);

	while (!q.empty()) {
		auto c = q.top();
		q.pop();

		if (-c.first != dis[c.second])
			continue;

		int u = c.second;

		for (auto it : G.e[u]) {
			int v = it.first, w = it.second;

			if (dis[v] > dis[u] + w)
				dis[v] = dis[u] + w, q.emplace(-dis[v], v);
		}
	}
}

signed main() {
	scanf("%lld%lld%lld%lld", &h, &x, &y, &z);

	if (x == 1 || y == 1 || z == 1)
		return printf("%lld\n", h), 0;

	--h;

	for (int i = 0; i < x; ++i)
		G.insert(i, (i + y) % x, y), G.insert(i, (i + z) % x, z);

	Dijkstra();
	ll ans = 0;

	for (int i = 0; i < x; ++i)
		if (h >= dis[i])
			ans += (h - dis[i]) / x + 1;

	printf("%lld", ans);
	return 0;
}

[ABC077D/ARC077B] Small Multiple

给定一个整数 \(K\)。求一个 \(K\) 的正整数倍 \(S\),使得 \(S\) 的数位累加和最小。

\(2 \leq K \leq 10^5\)

注意到一个数都可以通过 \(+1\)\(\times 10\) 得到。\(+1\) 时数位累加和增加,\(\times 10\) 时不变。

因为不需要求出具体数值,输出数位累加和即可,所以我们在 \(\bmod k\) 意义下利用同余最短路配合 01BFS 计算即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;

deque<pair<int, int> > q;

bool vis[N];

int K, ans;

signed main() {
	scanf("%d", &K);
	q.emplace_back(1, 1), vis[1] = true;
	
	while (!q.empty()) {
		int num = q.front().first, w = q.front().second;
		q.pop_front();
		
		if (!num) {
			printf("%d", w);
			break;
		}
		
		if (!vis[num * 10 % K])
			vis[num * 10 % K] = true, q.emplace_front(num * 10 % K, w);
		
		if (!vis[num + 1])
			q.emplace_back(num + 1, w + 1);
	}
	
	return 0;
}

删边最短路

CF1163F Indecisive Taxi Fee

给出一张无向带正权图, \(q\) 次询问,每次询问给出 \(t, x\),求若将 \(t\) 这条边的长度修改为 \(x\)\(1\)\(n\) 的最短路长度。

\(n, m, q \leq 2 \times 10^5\)

首先,若这条边不在最短路上,则答案要么为原来的最短路,要么为经过这条边的最短路,即:

\[ans = \min \{ dis_{1, n}, dis_{1, u} + x + dis_{v, n}, dis_{1, v} + k + dis_{u, n} \} \]

否则又分两种情况。若走这条边,答案为 \(dis_{1, u} + w(u, v) + dis_{v, n}\)

若不走这条边,设删掉这条边后找出的最短路为 \(E\),共有 \(k\) 条边分别为 \(e_{1 \sim k}\)

结论:删掉任意一条边后,一定存在一条 \(1\)\(n\) 的最短路有一个前缀(可能为空)和 \(E\) 重合,有一个后缀(也可能为空)和 \(E\) 重合,中间的部分都不在 \(E\) 上。

若有两段不在 \(E\) 上,因为只删掉了一条边,所以将其中一段换为 \(E\) 上的一段一定不劣。

设:

  • \(l_x\) 表示最小的 \(i\) 使得在某条 \(1 \to x\) 的最短路上 \(e_i\) 是第一条 \(E\) 上的不在其中的边。
  • \(r_x\) 表示最大的 \(i\) 使得在某条 \(x\to n\) 的最短路上 \(e_i\) 是最后一条 \(E\) 上的不在其中的边。

考虑求 \(l_x, r_x\) 。首先以 \(1\)\(n\) 为源点分别求一遍最短路,找出一条最短路 \(E\) 。对于 \(E\) 上的第 \(i\) 个点 \(x\),初始化 \(l_x = i, r_x = i - 1\)

\(l\) 为例,\(r\) 同理。若边 \((u,v)\) 满足 \(d_{1,u}+w_{u,v}=d_{1,v}\),则 \(l_v=\min(l_v,l_u)\)。按照 \(dis_{1, i}\) 排序后则可以线性更新。注意此时需要满足 \(1\to x\)\(E\) 只有一个前缀重合,所以不能用 \(E\) 上的边更新。

\(a_i\) 为删掉 \(e_i\) 之后的答案。求出 \(l, r\) 后枚举不在 \(E\) 上的边 \((u,v)\),用 \(d_{1,u}+w_{u,v}+d_{v,n}\) 更新 \([a_{l_u},a_{r_v}]\),用 \(d_{1,v}+w_{u,v}+d_{u,n}\) 更新 \([a_{l_v},a_{r_u}]\)。需要支持区间取 \(\min\) ,单点查询。因为查询都在修改之后,用 multiset 做一遍扫描线即可。

时间复杂度 \(O(m \log n + q)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2e5 + 7;

struct Graph {
    struct Edge {
        int nxt, v, w;
    } e[N << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v, int w) {
        e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
    }
} G;

struct Edge {
    int u, v, w, id;
} e[N];

vector<ll> ins[N], rmv[N];

ll dis1[N], disn[N], ans[N];
int l[N], r[N];

int n, m, q, Len = 1;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

inline void Dijkstra(int S, ll *dis) {
    fill(dis + 1, dis + 1 + n, inf);
    priority_queue<pair<ll, int> > q;
    dis[S] = 0, q.emplace(-dis[S], S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

signed main() {
    n = read(), m = read(), q = read();

    for (int i = 1; i <= m; ++i) {
        e[i].u = read(), e[i].v = read(), e[i].w = read();
        G.insert(e[i].u, e[i].v, e[i].w), G.insert(e[i].v, e[i].u, e[i].w);
    }

    Dijkstra(1, dis1), Dijkstra(n, disn);
    fill(l + 1, l + 1 + n, n + 1), fill(r + 1, r + 1 + n, 0);

    for (int u = 1; u != n;) {      
        l[u] = ++Len, r[u] = Len - 1;

        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (disn[v] + w == disn[u]) {
                u = v, e[i / 2].id = Len;
                break;
            }
        }
    }

    l[n] = ++Len, r[n] = Len - 1;
    vector<int> id(n); 
    iota(id.begin(), id.end(), 1);
    sort(id.begin(), id.end(), [](const int &a, const int &b) { return dis1[a] < dis1[b]; });

    for (int u : id)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (!e[i / 2].id && dis1[u] + w == dis1[v])
                l[v] = min(l[v], l[u]);
        }

    sort(id.begin(), id.end(), [](const int &a, const int &b) { return disn[a] < disn[b]; });

    for (int u : id)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (!e[i / 2].id && disn[u] + w == disn[v])
                r[v] = max(r[v], r[u]);
        }

    for (int i = 1; i <= m; ++i) {
        if (e[i].id)
            continue;

        int u = e[i].u, v = e[i].v, w = e[i].w;

        if (l[u] <= r[v]) {
            ins[l[u]].emplace_back(dis1[u] + w + disn[v]);
            rmv[r[v]].emplace_back(dis1[u] + w + disn[v]);
        }

        if (l[v] <= r[u]) {
            ins[l[v]].emplace_back(dis1[v] + w + disn[u]);
            rmv[r[u]].emplace_back(dis1[v] + w + disn[u]);
        }
    }

    multiset<ll> st;

    for (int i = 1; i <= Len; ++i) {
        for (ll it : ins[i])
            st.insert(it);

        ans[i] = st.empty() ? inf : *st.begin();

        for (ll it : rmv[i])
            st.erase(st.find(it));
    }

    while (q--) {
        int x = read(), k = read();
        int u = e[x].u, v = e[x].v, w = e[x].w;

        if (e[x].id)
            printf("%lld\n", min(dis1[n] + k - w, ans[e[x].id]));
        else
            printf("%lld\n", min(dis1[n], min(dis1[u] + k + disn[v], dis1[v] + k + disn[u])));
    }

    return 0;
}
posted @ 2024-07-11 22:33  我是浣辰啦  阅读(20)  评论(0编辑  收藏  举报