最短路
Floyd 算法
适用于无负环的图。
思路:枚举所有点对 ( i , j ) ( i , j ) 以及中转点 k k ,再对邻接矩阵进行松弛操作。
时间复杂度 O ( n 3 ) O ( n 3 ) ,可以一次求出任意两点最短路。
view code
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 ( n 3 ω ) O ( n 3 ω ) 。
view code
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 i 个点在 t i t i 时刻被修复,若 t i = 0 t i = 0 则 i i 未损坏。
q q 次询问,每次询问 t t 时刻 x x 到 y y 的最短路,保证给出的 t t 不降。
n ≤ 200 n ≤ 200
用 Floyd 求最短路,按修复时间枚举中转点松弛即可。
P6175 无向图的最小环问题
给一个正权无向图,找一个最小权值和的环。
枚举中转点 k k 时,我们已经得到了前 k − 1 k − 1 个点的最短路径。x ⇝ y x ⇝ y 、 y → k y → k 和 k → x k → x 共同构成了环,所以连接起来就得到了一个经过 x , y , k x , y , k 的最小环。
Bellman–Ford 算法
首先介绍一下松弛操作:d i s v ← min ( d i s v , d i s u + w ( u , v ) ) d i s v ← min ( d i s v , d i s u + w ( u , v ) ) 。
该算法不断尝试对图上每一条边进行松弛。由于每次松弛成功都会使得最短路长度增加 1 1 ,所以循环 n − 1 n − 1 次即可求出最短路。
时间复杂度 O ( n m ) O ( n m ) 。
但还有一种情况,如果从 S S 出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 n − 1 n − 1 轮。因此若第 n n 轮循环时仍然存在能松弛的边,说明从 S S 点出发能够抵达一个负环,但不能说明图中不存在负环。
SPFA 算法
经过队列优化的 Bellman-Ford 算法。
Bellman-Ford 算法中很多点是不用松弛的,只有上一次被松弛的结点所连接的边才有可能引起下一次松弛操作。
于是用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。
若要判负环,则记录一下每个点的松弛次数(即入队次数)即可。
SPFA 算法在随机图上时间复杂度为 O ( k m ) O ( k m ) ,但是可以被卡到 O ( n m ) O ( n m ) 。
view code
inline bool SPFA (int s) {
memset (dis + 1 , inf, sizeof (int ) * n);
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 更快
view code
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 , true ;
}
优化
比较弱的优化:
LLL 优化:使用双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则从队头插入。
SLF 优化:使用双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则从队头插入。
强一点的优化:
SLF 带容错:每次将入队结点距离和队首比较,如果比队首大超过一定值则插入至队尾,否则从队头插入。
mcfx 优化:定义区间 [ l , r ] [ l , r ] ,当入队点入队次数属于这个区间时从队首插入,否则从队头插入。通常取 [ 2 , √ n ] [ 2 , n ] 。
SLF + swap:每当队列改变时,如果队首距离大于队尾,则交换首尾。
玄学优化:
随机打乱边。
以一定概率从队首/队尾插入。
入队次数一定周期就随机打乱队列。
Dijkstra 算法
将结点分成两个集合:已确定最短路长度的点集 S S 的和未确定最短路长度的点集 T T 。
初始时所有的点都属于 T T ,令 d i s s = 0 d i s s = 0 ,其它点的 d i s d i s 均为 + ∞ + ∞ 。
重复操作直到 T T 为空:从 T T 中选一个 d i s d i s 最小的点移到 S S 中,并用该点松弛其它点。
Dijkstra 算法只能解决正权图上的最短路问题问题。
具体实现:
暴力:每次暴力找到 d i s d i s 最小的点松弛其它点,时间复杂度 O ( n 2 + m ) = O ( n 2 ) O ( n 2 + m ) = O ( n 2 ) 。
优先队列:每次松弛 ( u , v ) ( u , v ) 后将 v v 插入优先队列中,每次从优先队列中选 d i s d i s 最小的点松弛其它点。由于不能在优先队列中删除元素,所以取出时要判重,时间复杂度 O ( m log n ) O ( m log n ) 。
线段树:基本不用,将上面的操作改为单点修改和全局查询最小值,时间复杂度 O ( m log n ) O ( m log n ) 。
需要权衡 O ( n 2 ) O ( n 2 ) 和 O ( m log n ) O ( m log n ) 两种实现方式的优劣,一般稠密图使用 O ( n 2 ) O ( n 2 ) ,稀疏图用 O ( m log n ) O ( m log n ) 。
O ( n 2 ) O ( n 2 ) 的实现:
view code
inline void Dijkstra (int S) {
memset (dis + 1 , inf, sizeof (int ) * n);
dis[S] = 0 ;
for (;;) {
int u = -1 ;
for (int i = 1 ; i <= n; ++i)
if (!vis[i] && (u == -1 || dis[i] < dis[u]))
u = i;
if (u == -1 )
break ;
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;
}
}
}
优先队列优化的 Dijkstra 的实现:
view code
inline void Dijkstra (int S) {
memset (dis + 1 , inf, sizeof (int ) * n);
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);
}
}
}
另一种写法(取消了 v i s v i s 数组):
view code
inline void Dijkstra (int S) {
memset (dis + 1 , inf, sizeof (int ) * n);
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);
}
}
}
应用
P5304 [GXOI/GZOI2019] 旅行者
给定一张有向图和 k k 个关键点,求关键点两两之间最短路的最小值。
k ≤ n ≤ 10 5 k ≤ n ≤ 10 5 ,m ≤ 5 × 10 5 m ≤ 5 × 10 5
考虑两个关键点 x , y x , y 之间的最短路,先不考虑 x , y x , y 直接连边的情况。记路径上非端点的一个点为 z z ,则路径形如 x → z → y x → z → y 。
于是考虑对正图和反图各跑一遍以关键点为源点的最短路,则对于一个非关键点 z z ,关键点之间经过 z z 的最短路径即为正反两次 d i s z d i s z 的和,对所有 z z 的贡献取 min min 。
但是这样是假的,因为可能出现 x = y x = y 的情况。一个简单的想法是跑次短路,但是并不优美。考虑不枚举路径上的点,转而枚举路径上的边,再用两端正反图上起点不同的边更新即可。显然不会枚举到自己到自己的路径,然后可以证明一定会枚举到最短路答案,因此正确性有保证。
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
const ll inf = 0x3f3f3f3f3f3f3f3f ;
const int N = 1e5 + 7 ;
struct Graph {
vector <pair <int , int > > e[N];
inline void clear (int n) {
for (int i = 1 ; i <= n; ++i)
e[i].clear();
}
inline void insert (int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G, rG;
vector <int > kp;
ll dis1[N], dis2[N];
int st1[N], st2[N];
int n, m, k;
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 (Graph &G, ll *dis, int *st) {
memset (dis + 1 , inf, sizeof (ll) * n);
memset (st + 1 , 0 , sizeof (int ) * n);
priority_queue <pair <ll, int > > q;
for (int it : kp)
dis[it] = 0 , st[it] = it, q.emplace(-dis[it], it);
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, st[v] = st[u], q.emplace(-dis[v], v);
}
}
}
signed main () {
int T = read();
while (T--) {
n = read(), m = read();
kp.resize(read()), G.clear(n), rG.clear(n);
for (int i = 1 ; i <= m; ++i) {
int u = read(), v = read(), w = read();
if (u != v)
G.insert(u, v, w), rG.insert(v, u, w);
}
for (int &it : kp)
it = read();
Dijkstra(G, dis1, st1), Dijkstra(rG, dis2, st2);
ll ans = inf;
for (int u = 1 ; u <= n; ++u)
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (st1[u] && st2[v] && st1[u] != st2[v])
ans = min(ans, dis1[u] + w + dis2[v]);
}
printf ("%lld\n" , ans);
}
return 0 ;
}
给定一张图,每条边 s , t s , t 有两个权值 w , v w , v ,分别表示 ( s , t , w ) ( s , t , w ) 与 ( t , s , v ) ( t , s , v ) 的有向边,求边权和最小的包含 1 1 的简单环。
考虑一个简单环 S → x → y → S S → x → y → S ,且 S → x S → x 与 y → S y → S 不交。为了使答案最优,S → x S → x 与 y → S y → S 应当在满足无交集的情况下长度最短。先求出 S → x S → x 的最短路 d ( x ) d ( x ) 以及走的第一个点 p ( x ) p ( x ) ,再在反图上跑一边相同的流程,记为 d ( y ) d ( y ) 与 r p ( y ) r p ( y ) 。接下来枚举所有有向边 ( u , v , w ) ( u , v , w ) :
若 u = S u = S :不操作,因为这种情况会被其他情况覆盖。
若 v = S v = S :
若 p ( u ) ≠ u p ( u ) ≠ u :则用 d ( u ) + w d ( u ) + w 更新答案。
若 p ( u ) = u p ( u ) = u :则 S → u S → u 走最短路径不合法,要经过其他的边,这种情况会被其他情况覆盖。
否则:
若 p ( u ) ≠ r p ( v ) p ( u ) ≠ r p ( v ) :则用 d ( u ) + w + r d ( v ) d ( u ) + w + r d ( v ) 更新答案。
若 p ( u ) = r p ( v ) p ( u ) = r p ( v ) :则 S → u S → u 和 v → S v → S 走最短路径有重叠,也要走其他的路径,这种情况也会被其他情况覆盖。
上面的更新方式可以覆盖所有的简单环情况,所以不会漏过然后一个可能的答案。同时对于经过某条边且可以更新的简单环情况,其长度是最优的。时间复杂度 O ( m log m ) O ( m log m ) 。
view code
#include <bits/stdc++.h>
using namespace std ;
const int inf = 0x3f3f3f3f ;
const int N = 1e4 + 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, rG;
int d[N], p[N], rd[N], rp[N];
int n, m, S;
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 (Graph &G, int *dis, int *pre, int S) {
memset (dis + 1 , inf, sizeof (int ) * n);
priority_queue <pair <int , int > > q;
dis[S] = 0 , pre[S] = S, 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 (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, pre[v] = (u == S ? v : pre[u]), q.emplace(-dis[v], v);
}
}
}
signed main () {
n = read(), m = read(), S = 1 ;
for (int i = 1 ; i <= m; ++i) {
int u = read(), v = read(), w1 = read(), w2 = read();
G.insert(u, v, w1), rG.insert(v, u, w1);
G.insert(v, u, w2), rG.insert(u, v, w2);
}
Dijkstra(G, d, p, S), Dijkstra(rG, rd, rp, S);
int ans = inf;
for (int u = 1 ; u <= n; ++u) {
if (u == S)
continue ;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v == S) {
if (p[u] != u)
ans = min(ans, d[u] + w);
} else {
if (p[u] != rp[v])
ans = min(ans, d[u] + w + rd[v]);
}
}
}
printf ("%d" , ans);
return 0 ;
}
Johnson 全源最短路
如果没有负权边,那直接跑 n n 次 Dijkstra 即可。下面考虑怎么处理负权边。
建一个超级源点,所有点与其连一条边权为 0 0 的边。先用 SPFA 求每个点与超级源点的最短路径长度 h i h i 。
将每条边 u → v u → v 的边权增加 h u − h v h u − h v ,最后统计 i → j i → j 的最短路时减去 h i − h j h i − h j 即可,于是就能直接跑 n n 次 Dijkstra 了。
时间复杂度 O ( k m + n m log m ) O ( k m + n m log m ) 。
P5905 【模板】Johnson 全源最短路
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
const int inf = 0x3f3f3f3f ;
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 () {
memset (h + 1 , inf, sizeof (int ) * n);
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) {
memset (dis + 1 , inf, sizeof (int ) * n);
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] == inf ? 1e9 : dis[i][j]);
printf ("%lld\n" , res);
}
return 0 ;
}
BFS 相关
在一些特殊的图上,可以用 BFS 求解最短路做到 O ( n + m ) O ( n + m ) 的时间复杂度。
无权图上的最短路直接用 BFS 求解即可。
01BFS:若边权仅有 0 0 和 1 1 ,考虑有 deque
维护 BFS ,若走的边权为 0 0 则从队首入队,若走的边权为 1 1 则从队尾入队。
应用
CF173B Chamber of Secrets
一个 n × m n × m 的图,现在有一束激光从左上角往右边射出,每遇到 #
,你可以选择光线往四个方向射出,或者什么都不做。
问最少需要多少个 #
往四个方向射出才能使光线在第 n n 行往右边射出。
n , m ≤ 1000 n , m ≤ 1000
将柱子改为 #
后,一条光线经过的时候实际效果是该行该列都会有光线。于是视该操作代价为 1 1 跑 BFS 即可。
view code
#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 → v u → v ,答案即为:
min ( d i s 1 , u + w ( u , v ) + d i s v , n ) min ( d i s 1 , u + w ( u , v ) + d i s v , n )
d i s 1 , u , d i s v , n d i s 1 , u , d i s v , n 建立正反图跑两次 Dijkstra 即可求得。
求严格次短路时,我们不必记录最短路的路径,只需枚举每条边,若路径长度严格小于最短路时更新答案即可。
另一种方式是对于每个点都记录一下最短路与次短路,只要被更新就去松弛别的点。
P2865 [USACO06NOV] Roadblocks G
注意本题求的是严格次短路,下面给出第二种方法的实现。
view code
#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 集合位置
注意本题选取的第二短路径不会重复经过同一条路,所以只能把最短路上的边依次删去然后跑最短路。
view code
#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,只需将 d i s v = d i s u + w d i s v = d i s u + w 的边连边即可。
P2149 [SDOI2009] Elaxia的路线
给出一张无向图和两对点,求图中两对点间最短路的最长公共路径,注意同一条边走的方向不同也算公共路径。
n ≤ 1.5 × 10 3 n ≤ 1.5 × 10 3 ,m ≤ 3 × 10 5 m ≤ 3 × 10 5
一个显然的事实是最长公共路径一定是连续的一段。
考虑建立 s 1 → t 1 s 1 → t 1 的最短路图,注意此时仅保留 d i s s → u + w ( u , v ) + d i s v → t 1 d i s s → u + w ( u , v ) + d i s v → t 1 的边即可。
然后在最短路图上拓扑排序,正反各跑一次即可。一个简单的方法是记 f u , g u f u , g u 表示以 u u 为结尾/开始的最长公共路径长度,转移时只要考虑 u → v u → v 这条边能不能在 s 2 → t 2 s 2 → t 2 的最短路上即可。
view code
#include <bits/stdc++.h>
using namespace std ;
const int inf = 0x3f3f3f3f ;
const int N = 1.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[4 ][N], indeg[N], f[N], g[N];
bool vis[N], flag[N];
int n, m, s1, t1, s2, t2;
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, int *dis) {
memset (dis + 1 , inf, sizeof (int ) * n);
memset (vis + 1 , false , sizeof (bool ) * n);
priority_queue <pair <int , int > > q;
dis[S] = 0 , q.emplace(-dis[S], 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);
}
}
}
signed main () {
n = read(), m = read(), s1 = read(), t1 = read(), s2 = read(), t2 = 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(s1, dis[0 ]), Dijkstra(t1, dis[1 ]), Dijkstra(s2, dis[2 ]), Dijkstra(t2, dis[3 ]);
for (int u = 1 ; u <= n; ++u)
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[0 ][u] + w + dis[1 ][v] == dis[0 ][t1])
flag[v] = true , ++indeg[v];
}
queue <int > q;
q.emplace(s1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!flag[v])
continue ;
if (dis[2 ][u] + w + dis[3 ][v] == dis[2 ][t2])
f[v] = max(f[v], f[u] + w);
if (dis[3 ][u] + w + dis[2 ][v] == dis[2 ][t2])
g[v] = max(g[v], g[u] + w);
--indeg[v];
if (!indeg[v])
q.emplace(v);
}
}
printf ("%d" , max(*max_element(f + 1 , f + n + 1 ), *max_element(g + 1 , g + n + 1 )));
return 0 ;
}
「ROI 2017 Day 1」前往大都会
某国有 n n 座城市与 m m 条单向铁路线,构成一张连通图。第 i i 条单向铁路线由 v i , 1 , v i , 2 , ⋯ , v i , s i + 1 v i , 1 , v i , 2 , ⋯ , v i , s i + 1 城市组成,城市 v i , j v i , j 通过该线路到城市 v i , j + 1 v i , j + 1 花费的时间为 t i , j t i , j 。
求 1 1 到 n n 花费时间最少的情况下,经过任意两个相邻城市所花费时间的平方和的最大值。
n , m ≤ 10 6 n , m ≤ 10 6
首先求出最短路图,那么只要在最短路图上找到平方和最大的路径。
这里的最短路图是 DAG, 于是可以按拓扑序设计 DP 。
设 d p x d p x 表示以 x x 为终点的最大权值,枚举上一个换乘点,有:
f x = max y { f y + ( d x − d y ) 2 } = d 2 x + max y { d 2 y − 2 d x d y + f y } f x = max y { f y + ( d x − d y ) 2 } = d x 2 + max y { d y 2 − 2 d x d y + f y }
斜率优化即可,复杂度瓶颈为最短路。
view code
#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
给出一张无向图,给定源点,求边权和最小的 SPT。
n , m ≤ 3 × 10 5 n , m ≤ 3 × 10 5
要求边权和最小。可以考虑贪心,在松弛时若遇到松弛前后边权相等时取边权较小者即可。
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
const ll inf = 1e18 ;
const int N = 3e5 + 7 ;
struct Graph {
struct Edge {
int nxt, v, w;
bool tag;
} 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, false }, head[u] = tot;
}
} G;
ll dis[N];
int pre[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 <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, pre[v] = i, q.emplace(-dis[v], v);
else if (dis[v] == dis[u] + w && w < G.e[pre[v]].w)
pre[v] = i;
}
}
}
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(read());
for (int i = 1 ; i <= n; ++i)
G.e[pre[i]].tag = true ;
ll ans = 0 ;
for (int i = 1 ; i <= m; ++i)
if (G.e[i << 1 ].tag || G.e[i << 1 | 1 ].tag)
ans += G.e[i << 1 ].w;
printf ("%lld\n" , ans);
for (int i = 1 ; i <= m; ++i)
if (G.e[i << 1 ].tag || G.e[i << 1 | 1 ].tag)
printf ("%d " , i);
return 0 ;
}
应用
CF1005F Berland and the Shortest Paths
给出一张无向无边权简单连通图,求 SPT 方案数与方案(若超过 k k 种则只取 k k 种即可)。
对每个点维护可能成为前驱节点的集合,总方案数就是所有集合大小的乘积,求解方案直接暴力从每个集合中选一个元素组合即可。
view code
#include <bits/stdc++.h>
using namespace std ;
const int N = 2e5 + 7 ;
struct Graph {
struct Edge {
int nxt, v;
} e[N << 1 ];
int head[N];
int tot = 1 ;
inline void insert (int u, int v) {
e[++tot] = (Edge) {head[u], v}, head[u] = tot;
}
} G;
vector <int > pre[N];
int dis[N];
bool vis[N];
int n, m, k, ans = 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 bfs (int S) {
memset (dis + 1 , -1 , sizeof (int ) * n);
fill(dis + 1 , dis + 1 + n, -1 );
queue <int > q;
dis[S] = 0 , q.emplace(S);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (dis[v] == -1 )
dis[v] = dis[u] + 1 , pre[v].emplace_back(i >> 1 ), q.emplace(v);
else if (dis[v] == dis[u] + 1 )
pre[v].emplace_back(i >> 1 );
}
}
}
void dfs (int u) {
if (u > n) {
for (int i = 1 ; i <= m; ++i)
putchar (vis[i] ? '1' : '0' );
puts ("" );
--ans;
if (!ans)
exit (0 );
return ;
}
for (int it : pre[u])
vis[it] = true , dfs(u + 1 ), vis[it] = false ;
return ;
}
signed main () {
n = read(), m = read(), k = read();
for (int i = 1 ; i <= m; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
bfs(1 );
for (int i = 2 ; i <= n; ++i)
if (ans * pre[i].size() > k) {
ans = k;
break ;
} else
ans *= pre[i].size();
printf ("%d\n" , ans);
dfs(2 );
return 0 ;
}
P6880 [JOI 2020 Final] オリンピックバス
给定一个有向图,每条边从 u i u i 指向 v i v i ,经过这条边的代价为 c i c i ,翻转这条边的代价为 d i d i 。
可以翻转一条边(或不翻转),求 1 → n → 1 1 → n → 1 的最小代价和。
n ≤ 200 n ≤ 200 ,m ≤ 5 × 10 4 m ≤ 5 × 10 4
考虑求翻转一条边 ( u , v ) ( u , v ) 后 1 → n 1 → n 的最短路,n → 1 n → 1 是类似的。
一个显然的暴力是暴力每次都跑一次 Dijkstra,时间复杂度 O ( m n 2 ) O ( m n 2 ) 。
注意到只有 SPT 上的边才可能影响最短路,其余情况只要用强制经过这条边的最短路与原最短路取较小者即可,所以只要枚举 SPT 上的边即可,时间复杂度 O ( n 3 ) O ( n 3 ) 。
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
const int inf = 0x3f3f3f3f ;
const int N = 2e2 + 7 , M = 5e4 + 7 ;
struct Edge {
int u, v, w1, w2;
} edge[M];
pair <int , int > e[N][N];
ll dis[5 ][N];
int pre[5 ][N];
bool vis[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, ll *dis, int *pre) {
fill(dis + 1 , dis + n + 1 , 1e18 );
memset (pre + 1 , -1 , sizeof (int ) * n);
memset (vis + 1 , false , sizeof (bool ) * n);
dis[S] = 0 ;
for (;;) {
int u = -1 ;
for (int i = 1 ; i <= n; ++i)
if (!vis[i] && (u == -1 || dis[i] < dis[u]))
u = i;
if (u == -1 )
break ;
vis[u] = true ;
for (int v = 1 ; v <= n; ++v)
if (dis[v] > dis[u] + e[u][v].first)
dis[v] = dis[u] + e[u][v].first, pre[v] = u;
}
}
signed main () {
n = read(), m = read();
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= n; ++j)
e[i][j] = make_pair (inf, inf);
for (int i = 1 ; i <= m; ++i) {
int u = read(), v = read(), w1 = read(), w2 = read();
edge[i] = (Edge) {u, v, w1, w2};
if (w1 <= e[u][v].first)
e[u][v].second = e[u][v].first, e[u][v].first = w1;
else if (w1 <= e[u][v].second)
e[u][v].second = w1;
}
Dijkstra(1 , dis[0 ], pre[0 ]), Dijkstra(n, dis[1 ], pre[1 ]);
for (int i = 1 ; i <= n; ++i)
for (int j = i + 1 ; j <= n; ++j)
swap(e[i][j], e[j][i]);
Dijkstra(1 , dis[2 ], pre[2 ]), Dijkstra(n, dis[3 ], pre[3 ]);
for (int i = 1 ; i <= n; ++i)
for (int j = i + 1 ; j <= n; ++j)
swap(e[i][j], e[j][i]);
ll ans = dis[0 ][n] + dis[1 ][1 ];
for (int i = 1 ; i <= m; ++i) {
int u = edge[i].u, v = edge[i].v, w1 = edge[i].w1, w2 = edge[i].w2;
auto preedge = e[u][v], preedge2 = e[v][u];
e[u][v].first = e[u][v].second, e[v][u].first = min(e[v][u].first, w1);
ll res = w2;
if (pre[0 ][v] != u || dis[0 ][u] + w1 != dis[0 ][v])
res += min(dis[0 ][n], dis[0 ][v] + w1 + dis[3 ][u]);
else
Dijkstra(1 , dis[4 ], pre[4 ]), res += dis[4 ][n];
if (pre[1 ][v] != u || dis[1 ][u] + w1 != dis[1 ][v])
res += min(dis[1 ][1 ], dis[1 ][v] + w1 + dis[2 ][u]);
else
Dijkstra(n, dis[4 ], pre[4 ]), res += dis[4 ][1 ];
ans = min(ans, res);
e[u][v] = preedge, e[v][u] = preedge2;
}
printf ("%lld" , ans >= inf ? -1 : ans);
return 0 ;
}
差分约束系统
差分约束系统是一种特殊的 n n 元一次不等式组。每个不等式都形如 x i − x j ≤ c k x i − x j ≤ c k ,其中 c k c k 为常数且 i ≠ j i ≠ j 。需要求出一组整数解。
将每个不等式都转化为 x i ≤ x j + c k x i ≤ x j + c k ,这与三角形不等式 d i s v ≤ d i s u + w d i s v ≤ d i s u + w 十分相似。那么对于一组不等式 x v − x u ≤ w x v − x u ≤ w ,建边 ( u , v , w ) ( u , v , w ) 。
从超级源点向每个点连一条边权为 0 0 的边,若建图后图中有负环则方程组无解,否则 x i = d i s i x i = d i s i 就是方程组的一组解。
若 { x 1 , x 2 , ⋯ , x n } { x 1 , x 2 , ⋯ , x n } 是方程的一组解,则 { x 1 + d , x 2 + d , ⋯ , x n + d } { x 1 + d , x 2 + d , ⋯ , x n + d } 也是方程的一组解。
tricks:
x i − x j < c k x i − x j < c k 可以转化为 x i − x j ≤ c k − 1 x i − x j ≤ c k − 1 。
x i = x j x i = x j 可以转化为 x i − x j ≤ 0 x i − x j ≤ 0 且 x j − x i ≤ 0 x j − x i ≤ 0 。
差分约束系统的一个性质:如果跑的是最短路,则固定一个值时,其余的值都会取到最大值。
若 v 0 → v u v 0 → v u 经过的路径为:
v i 1 − v 0 ≤ l 0 v i 2 − v i 1 ≤ l 1 ⋯ v u − v i k ≤ l k v i 1 − v 0 ≤ l 0 v i 2 − v i 1 ≤ l 1 ⋯ v u − v i k ≤ l k
则加起来得到 v u − v 0 ≤ l 0 + l 1 + ⋯ + l k = d i s t ( 0 , u ) v u − v 0 ≤ l 0 + l 1 + ⋯ + l k = d i s t ( 0 , u ) 。
模板:P3275 [SCOI2011] 糖果
应用
P2474 [SCOI2008] 天平
有 n n 个砝码,分别重 x 1 ∼ n x 1 ∼ 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
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 ≤ 50 n ≤ 50
建立差分约束系统后,先跑一边最短路和最长路,求出 i , j i , j 之间的质量差最小值 m n i , j m n i , j 和最大值 m x i , j m x i , j 。
转化一下三种情况:
x a − x c > x b − x d x a − x c > x b − x d ,即 m n a , c > m x b , d m n a , c > m x b , d 。
x a − x c = x b − x d x a − x c = x b − x d ,即 m n a , c = m x a , c = m n b , d = m x b , d m n a , c = m x a , c = m n b , d = m x b , d 。
x a − x c < x b − x d x a − x c < x b − x d ,即 m n a , c < m x b , d m n a , c < m x b , d 。
分别统计方案数即可。
[AGC056C] 01 Balanced
构造长度为 n n 的字典序最小的 01 01 字符串,满足 m m 组子串 [ l i , r i ] [ l i , r i ] 含相同数量的 0 0 和 1 1 。
n ≤ 10 6 , m ≤ 2 × 10 5 n ≤ 10 6 , m ≤ 2 × 10 5 ,保证 r − l + 1 r − l + 1 是偶数
将 0 0 当作 1 1 ,1 1 当作 − 1 − 1 。因为要让答案的字典序最小,即 s i s i 尽可能大,即需要求出字典序最大的一组解,于是可以用差分约束系统求解。
相邻两个位置的限制从 | s i − s i − 1 | = 1 | s i − s i − 1 | = 1 弱化为 | s i − s i − 1 | ≤ 1 | s i − s i − 1 | ≤ 1 。
不可能存在 s i − 1 = s i s i − 1 = s i :若存在可以构造出 ± 1 ± 1 交错的 s s 使得字典序更大。
对于一组限制,转化为 s l − 1 = s r s l − 1 = s r 。
于是 01bfs 即可求解。
view code
#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 ;
}
[AGC036D] Negative Cycle
有一张 n n 个点的有向图,形如:
有 n − 1 n − 1 条形如 i → i + 1 i → i + 1 边权为 0 0 的边,不可删去。
对于每一对 i < j i < j ,存在边权为 − 1 − 1 的边。
对于每一对 i > j i > j ,存在边权为 1 1 的边。
删去边 ( i , j ) ( i , j ) 花费 a i , j a i , j 的代价,求图中不存在负环的最小删边代价。
n ≤ 500 n ≤ 500
考虑差分约束系统,要求图上没有负环,等价于存在一组差分约束的合法解,那么可以把图上的边都写成不等式。
设差分约束系统的合法解为 x 1 ∼ n x 1 ∼ n ,记 x x 的差分数组为 c i = x i − x i + 1 c i = x i − x i + 1 ,则:
对于边 i → i + 1 i → i + 1 ,其等价于 x i − x i + 1 = c i ≥ 0 x i − x i + 1 = c i ≥ 0 。
对于边 i → j i → j ( i < j i < j ),其等价于 x i − x j = ∑ j − 1 k = i c k ≥ 1 x i − x j = ∑ k = i j − 1 c k ≥ 1 ,即 [ i , j − 1 ] [ i , j − 1 ] 的区间和非 0 0 。
对于边 i → j i → j ( i > j i > j ),其等价于 x j − x i = ∑ i − 1 k = j c k ≤ 1 x j − x i = ∑ k = j i − 1 c k ≤ 1 。
可以发现,若 c i ≥ 2 c i ≥ 2 ,则可以调整为 c i = 1 c i = 1 ,答案不会变劣,因此仅需考虑 c i ∈ { 0 , 1 } c i ∈ { 0 , 1 } 的情况。
设 f i , j f i , j 表示考虑到 i i ,c i = 1 c i = 1 ,上一个 c c 为 1 1 的位置为 j j 的最小代价,考虑如何从 f j , k f j , k 转移到 f i , j f i , j :
左右端点都在 [ j + 1 , i ] [ j + 1 , i ] 中的第二类边需要删去。
左端点 ∈ [ k + 1 , j ] ∈ [ k + 1 , j ] ,右端点 ∈ [ i + 1 , n ] ∈ [ i + 1 , n ] 的第三类边需要删去。
不难用二维前缀和统计,时间复杂度 O ( n 2 ) O ( n 2 ) 。
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
const int N = 5e2 + 7 ;
ll f[N][N], s2[N][N], s3[N][N];
int a[N][N];
int n;
signed main () {
scanf ("%d" , &n);
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= n; ++j) {
if (i < j)
scanf ("%lld" , s2[i] + (j - 1 ));
else if (i > j)
scanf ("%lld" , s3[j] + (i - 1 ));
}
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= n; ++j) {
s2[i][j] += s2[i - 1 ][j] + s2[i][j - 1 ] - s2[i - 1 ][j - 1 ];
s3[i][j] += s3[i - 1 ][j] + s3[i][j - 1 ] - s3[i - 1 ][j - 1 ];
}
ll ans = 1e18 ;
auto calc = [](int k, int j, int i) {
return s2[i - 1 ][i - 1 ] - s2[j][i - 1 ] - s2[i - 1 ][j] + s2[j][j] +
s3[j][n] - s3[k][n] - s3[j][i - 1 ] + s3[k][i - 1 ];
};
for (int i = 1 ; i < n; ++i) {
f[i][0 ] = calc(0 , 0 , i);
for (int j = 1 ; j < i; ++j) {
f[i][j] = 1e18 ;
for (int k = 0 ; k < j; ++k)
f[i][j] = min(f[i][j], f[j][k] + calc(k, j, i));
}
for (int j = 0 ; j < i; ++j)
ans = min(ans, f[i][j] + calc(j, i, n));
= }
printf ("%lld" , ans);
return 0 ;
}
同余最短路
同余最短路利用同余来构造一些状态,并将其看作单源最短路中的点。
P3403 跳楼机
给出 x , y , z , h x , y , z , h ,求有多少 k ∈ [ 1 , h ] k ∈ [ 1 , h ] 满足 a x + b y + c z = k a x + b y + c z = k 。
x , y , z ≤ 10 5 , h ≤ 2 63 − 1 x , y , z ≤ 10 5 , h ≤ 2 63 − 1
不妨设 x < y < z x < y < z 。
令 d i d i 表示仅通过 b y + c z b y + c z 后能得到的模 x x 下与 i i 同余的最小数,用来计算该同余类满足条件的数个数。可以建边:( i , ( i + y ) mod x , y ) , ( i , ( i + z ) mod x , z ) ( i , ( i + y ) mod x , y ) , ( i , ( i + z ) mod x , z ) ,于是跑一次最短路即可求出 d i d i 。
令 1 1 作为源点,此时 d i s 1 = 1 d i s 1 = 1 最小,即可得到最小的一组解,类比差分约束即可得到所有解。
答案即为:
x − 1 ∑ i = 0 ( h − d i x + 1 ) ∑ i = 0 x − 1 ( h − d i x + 1 )
view code
#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 。求一个 K K 的正整数倍 S S ,使得 S S 的数位累加和最小。
2 ≤ K ≤ 10 5 2 ≤ K ≤ 10 5 。
注意到一个数都可以通过 + 1 + 1 和 × 10 × 10 得到。+ 1 + 1 时数位累加和增加,× 10 × 10 时不变。
因为不需要求出具体数值,输出数位累加和即可,所以我们在 mod k mod k 意义下利用同余最短路配合 01BFS 计算即可。
view code
#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 q 次询问,每次询问给出 t , x t , x ,求若将 t t 这条边的长度修改为 x x 时 1 1 到 n n 的最短路长度。
n , m , q ≤ 2 × 10 5 n , m , q ≤ 2 × 10 5
首先,若这条边不在最短路上,则答案要么为原来的最短路,要么为经过这条边的最短路,即:
a n s = min { d i s 1 , n , d i s 1 , u + x + d i s v , n , d i s 1 , v + k + d i s u , n } a n s = min { d i s 1 , n , d i s 1 , u + x + d i s v , n , d i s 1 , v + k + d i s u , n }
否则又分两种情况。若走这条边,答案为 d i s 1 , u + w ( u , v ) + d i s v , n d i s 1 , u + w ( u , v ) + d i s v , n 。
若不走这条边,设删掉这条边后找出的最短路为 E E ,共有 k k 条边分别为 e 1 ∼ k e 1 ∼ k 。
结论:删掉任意一条边后,一定存在一条 1 1 到 n n 的最短路有一个前缀(可能为空)和 E E 重合,有一个后缀(也可能为空)和 E E 重合,中间的部分都不在 E E 上。
若有两段不在 E E 上,因为只删掉了一条边,所以将其中一段换为 E E 上的一段一定不劣。
设:
l x l x 表示最小的 i i 使得在某条 1 → x 1 → x 的最短路上 e i e i 是第一条 E E 上的不在其中的边。
r x r x 表示最大的 i i 使得在某条 x → n x → n 的最短路上 e i e i 是最后一条 E E 上的不在其中的边。
考虑求 l x , r x l x , r x 。首先以 1 1 和 n n 为源点分别求一遍最短路,找出一条最短路 E E 。对于 E E 上的第 i i 个点 x x ,初始化 l x = i , r x = i − 1 l x = i , r x = i − 1 。
以 l l 为例,r r 同理。若边 ( u , v ) ( u , v ) 满足 d 1 , u + w u , v = d 1 , v d 1 , u + w u , v = d 1 , v ,则 l v = min ( l v , l u ) l v = min ( l v , l u ) 。按照 d i s 1 , i d i s 1 , i 排序后则可以线性更新。注意此时需要满足 1 → x 1 → x 和 E E 只有一个前缀重合,所以不能用 E E 上的边更新。
记 a i a i 为删掉 e i e i 之后的答案。求出 l , r l , r 后枚举不在 E E 上的边 ( u , v ) ( u , v ) ,用 d 1 , u + w u , v + d v , n d 1 , u + w u , v + d v , n 更新 [ a l u , a r v ] [ a l u , a r v ] ,用 d 1 , v + w u , v + d u , n d 1 , v + w u , v + d u , n 更新 [ a l v , a r u ] [ a l v , a r u ] 。需要支持区间取 min min ,单点查询。因为查询都在修改之后,用 multiset
做一遍扫描线即可。
时间复杂度 O ( m log n + q ) O ( m log n + q ) 。
view code
#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 ;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步