Bellman-Ford 负环
执行 bellman-ford \text{bellman-ford} bellman-ford 算法的时候,如果迭代了 n − 1 n-1 n − 1 次还可以进行边的松弛操作
那么一定存在负圈,实际上,如果用队列优化,迭代了 n − 1 n-1 n − 1 次,也就是说对某个节点 u u u ,入队了 n n n 次
在编程实现上,需要 inq ( u ) \text{inq}(u) inq ( u ) 判断点 u u u 是否在队列中
同时需要 cnt ( u ) \text{cnt}(u) cnt ( u ) 记录 u u u 入队次数
初始化时,将距离向量 ∀ u , f ( u ) ← + ∞ , f ( s ) = 0 \forall u, f(u) \leftarrow +\infty, \quad f(s) = 0 ∀ u , f ( u ) ← + ∞ , f ( s ) = 0
由于一开始要将所有点进行松弛 ,所以 ∀ u ∈ [ 1 , n ] , Q . push ( u ) , inq ( u ) = 1 \forall u \in [1, n], \ \text{Q}.\text{push}(u), \ \text{inq}(u) = 1 ∀ u ∈ [ 1 , n ] , Q . push ( u ) , inq ( u ) = 1
Going in Cycle
对于 G ( n , m ) G(n, m) G ( n , m ) 的有向图,求平均值最小的回路
不难想到用二分求解,边的平均权值的值域为 [ l , r ] [l, r] [ l , r ] ,本例为浮点数二分
check ( m i d ) \text{check}(mid) check ( m i d ) 表示是否存在平均权值 严格小于 mid \text{mid} mid 的回路
如果存在,那么尝试 [ l , r ] ← [ l , m i d ] [l, r] \leftarrow [l, mid] [ l , r ] ← [ l , m i d ] ,否则 [ l , r ] ← [ m i d , r ] [l, r] \leftarrow [mid, r] [ l , r ] ← [ m i d , r ]
难点在于 check ( m i d ) \text{check}(mid) check ( m i d ) 的实现,如果存在平均权值严格小于 m i d mid m i d 的回路
那么有
w 1 + w 2 + ⋯ + w m < m ⋅ m i d ( w 1 − m i d ) + ( w 2 − m i d ) + ⋯ + ( w m − m i d ) < 0 \begin{gathered}
w_1 + w_2 + \cdots + w_m < m \cdot mid \\
(w_1 - mid) + (w_2 - mid) + \cdots + (w_m - mid) < 0
\end{gathered}
w 1 + w 2 + ⋯ + w m < m ⋅ m i d ( w 1 − m i d ) + ( w 2 − m i d ) + ⋯ + ( w m − m i d ) < 0
实现的时候,尝试修改边权 w i ← ( w i − m i d ) w_i \leftarrow (w_i - mid) w i ← ( w i − m i d ) ,然后判断是否存在负圈即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 const int maxn = 100 + 10, maxm = 2e5 + 10; const double eps = 1e-3; const int inf = 0x3f3f3f3f; int n, m; namespace Graph { int idx = 1; int ver[maxm], ne[maxm], h[maxn]; double e[maxm]; void initG () { idx = 1; memset(h, 0, sizeof h); } void add(int a, int b, double c) { ver[++idx] = b, e[idx] = c, ne[idx] = h[a], h[a] = idx; } } using namespace Graph; vector<double> f(maxn, (double)inf); vector<int> inq(maxn, 0), cnt(maxn, 0); bool negaCycle(int s, vector<double> &f) { queue<int> q; fill(f.begin(), f.end(), (double)inf); fill(inq.begin(), inq.end(), 0), fill(cnt.begin(), cnt.end(), 0); f[s] = 0; for (int i = 1; i <= n; i++) q.push(i), inq[i] = 1; while (q.size()) { auto x = q.front(); q.pop(); inq[x] = 0; for (int i = h[x]; i; i = ne[i]) { int y = ver[i]; if (f[y] > f[x] + e[i] + eps) { f[y] = f[x] + e[i]; if (!inq[y]) { inq[y] = 1, q.push(y); if (++cnt[y] > n) return true ; } } } } return false ; } bool check(double x) { for (int i = 2; i <= idx; i++) e[i] -= x; bool ret = negaCycle(1, f); for (int i = 2; i <= idx; i++) e[i] += x; return ret; } void init () { // } int main () { freopen("input.txt" , "r" , stdin); int cas; cin >> cas; int kase = 0; while (cas--) { printf ("Case #%d: " , ++kase); scanf("%d%d" , &n, &m); init(), initG(); double mx = 0.0; while (m--) { int a, b; double c; scanf("%d%d%lf" , &a, &b, &c); add(a, b, c), mx = max(mx, c); } if (check(mx + 1.0) == false ) { printf ("No cycle found.\n" ); continue ; } double l = 0.0, r = mx; while (r - l > eps) { double mid = (l + r) / 2.0; if (check(mid)) r = mid; else l = mid; } printf ("%.2lf\n" , l); } }
差分约束
差分约束系统,包含 n n n 个变量 { x 1 , x 2 , ⋯ , x n } \{x_1, x_2, \cdots, x_n\} { x 1 , x 2 , ⋯ , x n } 以及 m m m 个约束条件
每个约束条件为 ∀ i , j , 1 ⩽ i , j ⩽ n , ∀ k , 1 ⩽ k ⩽ m \forall i, j, \ 1\leqslant i, j \leqslant n, \ \forall k, 1\leqslant k \leqslant m ∀ i , j , 1 ⩽ i , j ⩽ n , ∀ k , 1 ⩽ k ⩽ m
x i − x j ⩽ c k x_i - x_j \leqslant c_k
x i − x j ⩽ c k
对于条件 x i − x j ⩽ c k x_i - x_j \leqslant c_k x i − x j ⩽ c k ,变形为 x i ⩽ x j + c k x_i \leqslant x_j + c_k x i ⩽ x j + c k
如果从 j → i j \to i j → i 连一条有向边,并且边的权值为 c k c_k c k ,此时如果差分约束 x i − x j ⩽ c k x_i - x_j \leqslant c_k x i − x j ⩽ c k 有解
那么边 ( j , i ) (j, i) ( j , i ) 一定是松弛的
引理 ,如果 ( x 1 , x 2 , ⋯ x n ) (x_1, x_2, \cdots x_n) ( x 1 , x 2 , ⋯ x n ) 为差分约束系统的一个可行解,那么 ( x 1 + d , x 2 + d , ⋯ , x n + d ) (x_1+d, x_2+d, \cdots, x_n+d) ( x 1 + d , x 2 + d , ⋯ , x n + d )
也是差分约束系统的一个解
如果差分约束系统有解,那么令 d = − ∞ d = -\infty d = − ∞ ,此时 x i + d < 0 x_i + d < 0 x i + d < 0 ,即 x i ⩽ 0 x_i \leqslant 0 x i ⩽ 0 也是一个可行解
于是,如果差分约束系统有可行解,可以令 x 0 = 0 x_0 = 0 x 0 = 0 ,这样对于 ∀ i , x i − x 0 ⩽ 0 \forall i, \ x_i - x_0 \leqslant 0 ∀ i , x i − x 0 ⩽ 0
可以构造出一个约束图 ,建一个编号为 0 0 0 的超级源点 s s s
并且令 x 0 = f ( 0 ) = 0 x_0 = f(0) = 0 x 0 = f ( 0 ) = 0 ,其中 f f f 为最短路求解中的距离数组
从 0 0 0 出发向图中所有的点 i i i 连边,边的权值为 0 0 0
结论 ,如果差分约束系统有解,一个可行解为 ( x 1 , x 2 , ⋯ , x n ) = ( f ( 1 ) , f ( 2 ) , ⋯ , f ( n ) ) (x_1, x_2, \cdots, x_n) = (f(1), f(2), \cdots, f(n)) ( x 1 , x 2 , ⋯ , x n ) = ( f ( 1 ) , f ( 2 ) , ⋯ , f ( n ) )
其中 f ( i ) f(i) f ( i ) 为从 0 → i 0 \to i 0 → i 的单源最短路径
如果约束图 G G G 中有负环,那么差分约束系统没有可行解
{ f ( 1 ) , f ( 2 ) , ⋯ f ( n ) } \{f(1), f(2), \cdots f(n)\} { f ( 1 ) , f ( 2 ) , ⋯ f ( n ) } 是差分约束系统的一个可行解,因为此时任意的点都满足路径松弛
f ( i ) ⩽ f ( j ) + w ( j , i ) f(i) \leqslant f(j) + w(j, i) f ( i ) ⩽ f ( j ) + w ( j , i ) ,其中 w ( j , i ) = c k w(j, i) = c_k w ( j , i ) = c k
G G G 中执行 bellman ford \text{bellman ford} bellman ford 存在负环,那么差分约束系统没有可行解
下面用反正法,不妨设负环上点编号为 { u 1 , u 2 , ⋯ , u k } \{u_1, u_2, \cdots, u_k\} { u 1 , u 2 , ⋯ , u k } ,那么此时如果存在可行解,对应有
x 2 − x 1 ⩽ c 1 , x 3 − x 2 ⩽ c 2 ⋯ , x k − x k − 1 ⩽ c k x_2 - x_1 \leqslant c_1, \ x_3 - x_2 \leqslant c_2 \ \cdots, \ x_k - x_{k-1} \leqslant c_k x 2 − x 1 ⩽ c 1 , x 3 − x 2 ⩽ c 2 ⋯ , x k − x k − 1 ⩽ c k
对不等式求和,此时有
x k − x 1 ⩽ ∑ c x_k - x_1 \leqslant \sum c x k − x 1 ⩽ ∑ c ,由于是环路,所以 1 1 1 和 k k k 实际上是一个点,那么 x k = x 1 x_k = x_1 x k = x 1
也就是说,此时有 0 ⩽ ∑ c 0 \leqslant \sum c 0 ⩽ ∑ c ,与该路径是负环矛盾
算法实现
对于每一个不等式 x j − x i ⩽ c k x_j - x_i \leqslant c_k x j − x i ⩽ c k ,新建一条边 add ( i , j , c k ) \text{add}(i, j, c_k) add ( i , j , c k )
有向,i → j i \to j i → j ,权值为 c k c_k c k
添加一个超级源点 s = 0 s = 0 s = 0 ,和其他所有点连一条边,权值为 0 0 0
从 s s s 开始执行 bellman ford \text{bellman ford} bellman ford 算法,如果有负环,那么差分约束系统无解
否则 x i = f ( i ) x_i = f(i) x i = f ( i ) 就是一个可行解,其中 f f f 为最短距离数组
Halum 操作
由于权值的增加过程是相互独立的,对于点 u u u
令 S ( u ) S(u) S ( u ) 为增加在 u u u 这个点上的所有 d d d 的和,问边的最小值非负且尽量大
可以二分最小值 x x x ,如果满足 check \text{check} check 所有边的权值 ⩾ x \geqslant x ⩾ x ,尝试增大 x x x
问题就是 check ( x ) \text{check}(x) check ( x ) 如何写
check ( x ) \text{check}(x) check ( x ) 为 true \text{true} true ,等价于对于所有边 ( u , v ) (u, v) ( u , v )
S ( u ) − S ( v ) + w ( u , v ) ⩾ x S(u) - S(v) + w(u, v) \geqslant x S ( u ) − S ( v ) + w ( u , v ) ⩾ x ,此时 x , w ( u , v ) x, w(u, v) x , w ( u , v ) 确定,问题转换为是否存在合法的 S ( u ) , S ( v ) S(u), S(v) S ( u ) , S ( v ) ,使得
S ( v ) − S ( u ) ⩽ w ( u , v ) − x S(v) - S(u) \leqslant w(u, v) - x S ( v ) − S ( u ) ⩽ w ( u , v ) − x ,这可以用差分约束来解决
在原图的基础上,添加一个超级源点 S S S ,这个超级源点到所有点 u u u 连边,权值为 0 0 0
遍历每一条边 ( u , v ) ∈ E (u, v) \in E ( u , v ) ∈ E ,尝试将这条边的权值 − x -x − x ,并且执行 spfa \text{spfa} spfa 判负环
如果有负环,check ( x ) \text{check}(x) check ( x ) 返回 false \text{false} false 并且减小 x x x
拆点与拆边
Steam Roller
由于存在方向,以及一条道路不能反复加倍,所以考虑拆点
对每个点 ( r , c ) (r, c) ( r , c ) 拆成 8 8 8 个点 ( r , c , d i r , d b ) (r, c, dir, db) ( r , c , d i r , d b )
其中 d i r dir d i r 表示这条边的方向,d b db d b 表示这条边是否被加倍
m p ( r , c , d i r , d b ) mp(r, c, dir, db) m p ( r , c , d i r , d b ) 表示状态哈希
这样我们可以暴力枚举每一个状态转移
d i r dir d i r 表示当状态的方向,n d i r ndir n d i r 表示下一个状态的方向,d b db d b 表示当前边是否被加倍
∀ d i r , n d i r ∈ [ 0 , 3 ] , ∀ d b , n d b ∈ [ 0 , 1 ] \forall dir, ndir \in [0, 3], \ \forall db, ndb \in [0, 1] ∀ d i r , n d i r ∈ [ 0 , 3 ] , ∀ d b , n d b ∈ [ 0 , 1 ] ,暴力枚举状态转移
( r , c , d i r , d b ) ⟶ ( n r , n c , n d i r , n d b ) (r, c, dir, db) \longrightarrow (nr, nc, ndir, ndb) ( r , c , d i r , d b ) ⟶ ( n r , n c , n d i r , n d b )
构图之后用 dijkstra \text{dijkstra} dijkstra 单源最短路解决
接下来就是编程实现的一些细节,保存路径的时候,我们需要判断从 ( r , c ) (r, c) ( r , c ) 出发 沿着 d i r dir d i r 能否走的通
我们保存的是从 u u u 出发的边 ( u ⟶ ) (u \longrightarrow) ( u ⟶ )
存储状态时候,( u , d i r ) (u, dir) ( u , d i r ) 表示从 d i r dir d i r 方向走进 u u u 节点,即 ( ⟶ u ) (\longrightarrow u) ( ⟶ u )
需要写 valid ( r , c , d i r ) \text{valid}(r, c, dir) valid ( r , c , d i r ) 判断,问题是终点的状态是什么?
我们还需要一个状态,表示沿着 d i r dir d i r 到达 这个点 ( r , c ) (r, c) ( r , c ) ,此时状态是否合法
这时候我们需要拆边 了,以向左为例,此时 d i r = 0 dir = 0 d i r = 0 ,那么向右记为 inv ( d i r ) \text{inv}(dir) inv ( d i r )
输入的时候,令 g ( r , c , d i r ) = g ( r , c + 1 , inv ( d i r ) ) = read ( ) g(r, c, dir) = g(r, c+1, \text{inv}(dir)) = \text{read}() g ( r , c , d i r ) = g ( r , c + 1 , inv ( d i r ) ) = read ( )
这样在终点的时候,只需要处理 valid ( r 2 , c 2 , inv ( d i r ) ) = true \text{valid}(r2, c2, \text{inv}(dir)) = \text{true} valid ( r 2 , c 2 , inv ( d i r ) ) = true 的状态
接着考虑 db \text{db} db 加倍的问题,需要在可以加倍的时候,立刻加倍(即 n d i r ≠ d i r ndir \neq dir n d i r = d i r 时)
然后看一看上一次的边加倍了没?
1 2 3 4 5 6 if (ndir != dir) { // 此时新边肯定必须加倍,立刻加倍 v += g(r, c, ndir); // 再看一下上一条边有没有漏加倍了? if (!db == 0) v += g(r, c, inv[dir]) }
最后用状态哈希,在 ( r , c , d i r , d b ) , ( n r , n c , n d i r , n d b ) (r, c, dir, db), (nr, nc, ndir, ndb) ( r , c , d i r , d b ) , ( n r , n c , n d i r , n d b ) 连一条权值为 v v v 的边
初始阶段,建立一个超级源点 S = 0 S = 0 S = 0 ,之后的状态,需要枚举 4 4 4 个方向,如果
g ( r 1 , c 1 , d i r ) > 0 g(r1, c1, dir) > 0 g ( r 1 , c 1 , d i r ) > 0 ,那么从 ( r 1 , c 1 ) (r1, c1) ( r 1 , c 1 ) 往 d i r dir d i r 可以走的通,并且我们让边权立刻加倍
r = r 1 + d r ( d i r ) , c = c 1 + d c ( d i r ) r = r1 + dr(dir), \quad c = c1 + dc(dir) r = r 1 + d r ( d i r ) , c = c 1 + d c ( d i r )
也就是说,从 S S S 向 ( r , c , d i r , 1 ) (r, c, dir, 1) ( r , c , d i r , 1 ) 连一条权值为 2 ⋅ g ( r 1 , c 1 , d i r ) 2\cdot g(r1, c1, dir) 2 ⋅ g ( r 1 , c 1 , d i r ) 的边
然后枚举所有可能的状态 ( r , c , d i r , d b ) (r, c, dir, db) ( r , c , d i r , d b ) ,首先我们要判断当前状态是否合法
实际上就是判断当前状态能否到达 ,也就是 valid ( r , c , inv ( d i r ) ) \text{valid}(r, c, \text{inv}(dir)) valid ( r , c , inv ( d i r ) )
然后枚举 4 4 4 个方向 ndir \text{ndir} ndir ,并且判断从 ( r , c ) (r, c) ( r , c ) 能否走到 ( n r , n c ) (nr, nc) ( n r , n c )
也就是 valid ( r , c , ndir ) \text{valid}(r, c, \text{ndir}) valid ( r , c , ndir ) ,如果可以走,那么按以上分析
从 ( r , c , d i r , d b ) → ( n r , n c , n d i r , n d b ) (r, c, dir, db) \to (nr, nc, ndir, ndb) ( r , c , d i r , d b ) → ( n r , n c , n d i r , n d b ) 连边
这里还必须注意,储存状态的时候我们存的是前向弧 ( ⟶ u ) (\longrightarrow u) ( ⟶ u )
但是拆边的时候,判断一条边可以不可以走,我们用的是 ( u ⟶ ) (u \longrightarrow) ( u ⟶ )
所以对应关系是 ( r , c , d i r , d b ) ⇔ g ( r , c , inv ( d i r ) ) (r, c, dir, db) \Leftrightarrow g(r, c, \text{inv}(dir)) ( r , c , d i r , d b ) ⇔ g ( r , c , inv ( d i r ) )
拆点技巧
本例的拆点技巧很常用,点的状态 用前向弧 ( d i r , u ) (dir, u) ( d i r , u ) 表示
而从一个点出发的边用 g ( u , d i r ) g(u, dir) g ( u , d i r ) 表示,双向边成对存储
输入边 x = ( u , v ) x = (u, v) x = ( u , v ) 的时候,实际上输入 g ( u , d i r ) = g ( v , inv ( d i r ) ) = x g(u, dir) = g(v, \text{inv}(dir)) = x g ( u , d i r ) = g ( v , inv ( d i r ) ) = x
这样一个状态 ( d i r , u ) (dir, u) ( d i r , u ) 是合法状态,当且仅当 g ( u , inv ( d i r ) ) ≠ 0 g(u, \text{inv}(dir)) \neq 0 g ( u , inv ( d i r ) ) = 0
Low Cost Air Travel
一种很显然地构图方式,是暴力枚举每一张票,然后判断用这张票 i i i ,能够经过的城市 ( u , v ) (u, v) ( u , v )
e ( u , v ) = min i ( cost ( i ) ) e(u, v) = \displaystyle\min_{i}(\text{cost}(i)) e ( u , v ) = i min ( cost ( i ) )
但这种构图方式并不正确,因为规定每一张票必须从头坐 ,也就是说只能用它的一个前缀
也就是说对于票 A → B → C A \to B \to C A → B → C ,此时从 B → C B \to C B → C 不能用这张票,B → C B \to C B → C 路径不存在
此时考虑给状态增加维度,注意到行程单 list \text{list} list 上所有的点必须走完,考虑公共子序列的状态表示
对于行程单 list [ 1 ⋯ n ] \text{list}[1\cdots n] list [ 1 ⋯ n ] ,用 i ∈ [ 1 , n ] i \in [1, n] i ∈ [ 1 , n ] 表示当前完成行程单 上第 i i i 个位置(作为阶段)
此时人在城市 P P P ,这时候的状态为 ( i , P ) (i, P) ( i , P )
这样就可以枚举 i ∈ list [ 1 , n ] i \in \text{list}[1, n] i ∈ list [ 1 , n ] ,并且枚举每一张票 ∀ k , t ( k ) \forall k, \ \text{t}(k) ∀ k , t ( k )
注意到每一张票必须从头开始坐 ,对于第 k k k 张票 t ( k ) t(k) t ( k ) ,所经过的城市为
t ( k , 0 ) , t ( k , 1 ) , ⋯ , t ( k , m ) t(k, 0), t(k, 1), \cdots, t(k, m) t ( k , 0 ) , t ( k , 1 ) , ⋯ , t ( k , m ) ,票价为 cost ( k ) \text{cost}(k) cost ( k )
那么从 s = ( i , t ( k , 0 ) ) s = (i, t(k, 0)) s = ( i , t ( k , 0 ) ) 依次向 ( ⋯ , t ( k , p ) ) (\cdots, t(k, p)) ( ⋯ , t ( k , p ) ) 连一条权值为 cost ( k ) \text{cost}(k) cost ( k ) 的边
具体是如何连?先令 j ← i j \leftarrow i j ← i
对于 ∀ p ∈ [ 1 , m ] \forall p \in [1, m] ∀ p ∈ [ 1 , m ] ,此时用票 k k k 经过了城市 t ( k , p ) t(k, p) t ( k , p )
如果 t ( k , p ) = list ( j ) t(k, p) = \text{list}(j) t ( k , p ) = list ( j ) ,那么 j ← j + 1 j \leftarrow j+1 j ← j + 1 (当然还必须满足 j < n j < n j < n )
此时从 s = ( i , t ( k , 0 ) ) s = (i, t(k, 0)) s = ( i , t ( k , 0 ) ) 向 ( j , t ( k , p ) ) (j, t(k, p)) ( j , t ( k , p ) ) 连一条权值为 cost ( k ) \text{cost}(k) cost ( k ) 的边
实际上类似动态规划,我们处理 i → i + 1 i \to i+1 i → i + 1 阶段的转移
在执行最短路的时候,行程清单为 list [ 0 ⋯ n − 1 ] \text{list}[0\cdots n-1] list [ 0 ⋯ n − 1 ]
源点为 S = ( 0 , list ( 0 ) ) S = (0, \text{list}(0)) S = ( 0 , list ( 0 ) ) ,终点为 T = ( n − 1 , list ( n − 1 ) ) T = (n-1, \text{list}(n-1)) T = ( n − 1 , list ( n − 1 ) )
Flying to Fredericton
由于有最大停留次数 ⩽ s \leqslant s ⩽ s 的限制,所以考虑拆点,将 n n n 个点拆成 100 ⋅ n 100 \cdot n 1 0 0 ⋅ n 个点
其中 ( k , u ) (k, u) ( k , u ) 状态表示到达 u u u 之前已经停留了 k k k 次
对于每一条边 ( u , v , w ) (u, v, w) ( u , v , w ) ,枚举停留次数 i ∈ [ 0 , n ] i \in [0, n] i ∈ [ 0 , n ]
连边 ( i , u ) → ( i + 1 , v ) (i, u) \to (i+1, v) ( i , u ) → ( i + 1 , v ) ,权值为 w w w ,源点为 S = ( 0 , Calgary ) S = (0, \text{Calgary}) S = ( 0 , Calgary )
求单源最短路,最后的答案为 ∀ i ∈ [ 1 , 1 + s ] , min ( ( i , Fredericton ) ) \forall i \in [1, 1+s], \ \min((i, \text{Fredericton})) ∀ i ∈ [ 1 , 1 + s ] , min ( ( i , Fredericton ) )
平面图转最短路
有一些问题是经典的
Animal Run
如果把平面图中的点看成图的节点,横线和竖线看成边,那么问题转换为一个经典的最小割模型
但 n ∗ m = 1 0 6 n * m = 10^6 n ∗ m = 1 0 6 ,用最大流算法无法求解
如图所示,假设将图中左上角看作起点,右下角看作终点,要想成功拦截
那么切割边必须满足一定的条件,比如说从左边界连到上边界,不难写出以下几种可行切割
( left , up ) , ( left , right ) , ( down , right ) , ( down , up ) (\text{left}, \text{up}), \ (\text{left}, \text{right}), \ (\text{down}, \text{right}), \ (\text{down}, \text{up}) ( left , up ) , ( left , right ) , ( down , right ) , ( down , up )
于是可以考虑状态建图,将每一条边压缩成一个点 u ( i , j , f l ) u(i, j, fl) u ( i , j , f l )
点的权值为原先这条边的权值 w ( u ) = w ( i , j , f l ) w(u) = w(i, j, fl) w ( u ) = w ( i , j , f l )
其中 i , j i, j i , j 表示边的坐标,f l fl f l 表示边的种类,即横线,竖线,还是斜线
另外为了处理边的代价以及边界问题,添加一个超级源点 S S S
每一条边实际上是用前向弧 表示,也就是说,在状态图中添加边 ( u , v ) (u, v) ( u , v ) 时候
e ( u , v ) = w ( v ) e(u, v) = w(v) e ( u , v ) = w ( v ) ,添加超级源点时,实际上加入边 ( S , s , w ( s ) ) (S, s, w(s)) ( S , s , w ( s ) )
这样可以设计出如下算法
对每一组三角形内的三条边 u ( i , j , f l ) u(i, j, fl) u ( i , j , f l ) ,两两连边
编程实现的时候,可以把一个三角形看成一个单位 vector \text{vector} vector ,里面有 3 3 3 个节点(边)
两两相连来建图
添加一个超级源点 S S S ,由于合法切割起点是左边界和下边界
所以从 S S S 向这些点 u u u 连边 ( S , u ) (S, u) ( S , u ) ,边的权值为 w ( u ) w(u) w ( u )
接着从 S S S 开始执行单源最短路即可,左右解为右边界和上边界所有道路的最小值
矩阵运算与最短路
Dice Contest
骰子旋转问题的状态表示
如上图所示,骰子的标准姿态为 P = { 0 , 1 , 2 , 3 , 4 , 5 } P = \{0, 1, 2, 3, 4, 5\} P = { 0 , 1 , 2 , 3 , 4 , 5 } ,这里的数值表示下标
以向上旋转为例,新姿态为 P 2 P_2 P 2 ,那么此时 P ( 0 ) ⇔ P 2 ( 2 ) P(0) \Leftrightarrow P_2(2) P ( 0 ) ⇔ P 2 ( 2 )
即原来下标为 0 0 0 的数,在新姿态下,位于下标 2 2 2 处,写成矩阵运算 T \bm{T} T 的作用
P ( 0 ) ⇔ P 2 ( 2 ) = T ( P ( 0 ) ) P(0) \Leftrightarrow P_2(2) = \bm{T}(P(0)) P ( 0 ) ⇔ P 2 ( 2 ) = T ( P ( 0 ) ) ,用一维数组表示为 T ( 0 ) = 2 \bm{T}(0) = 2 T ( 0 ) = 2
以此类推,向上旋转的矩阵变换为 T = { 2 , 1 , 5 , 0 , 4 , 3 } \bm{T} = \{2, 1, 5, 0, 4, 3\} T = { 2 , 1 , 5 , 0 , 4 , 3 }
这样枚举姿态的时候,P 2 ( i ) = T ( P ( i ) ) P_2(i) = \bm{T}(P(i)) P 2 ( i ) = T ( P ( i ) ) 即可实现
以上就可以封装出骰子所有的姿态,这样在编程实现的时候,我们枚举骰子的姿态 i ∈ [ 0 , 24 ) i \in [0, 24) i ∈ [ 0 , 2 4 )
表示骰子处于姿态 i i i ,那么此时骰子第 j j j 表面上的数为多少呢?
假设骰子原来第 j j j 表面的数为 A [ j ] A[j] A [ j ] ,执行 B ( dice24 [ i ] [ j ] ) ← A [ j ] B(\text{dice24}[i][j]) \leftarrow A[j] B ( dice24 [ i ] [ j ] ) ← A [ j ]
对 B B B 按以上映射染色 ,这样我们就可以得到旋转之后,骰子每个面的数值
回到该问题
如果 x x x 不是无限大,那么该问题就是一个标准的隐式图最短路 ,每一个位置 ( x , y ) (x, y) ( x , y ) 有 24 24 2 4 种状态
根据 ( x , y , dice24 ) (x, y, \text{dice24}) ( x , y , dice24 ) 来建图,然后暴力枚举 24 24 2 4 种状态之间的合法转移(即上下左右翻转)
求一遍最短路,终点为 ( x 2 , y 2 ) (x_2, y_2) ( x 2 , y 2 ) ,最后 min k ∈ [ 0 , 24 ) ( x , y , dice24 ( k ) ) \displaystyle\min_{k \in [0, 24)}(x, y, \text{dice24}(k)) k ∈ [ 0 , 2 4 ) min ( x , y , dice24 ( k ) ) 就是答案
但由于 x x x 太大,以上算法无法通过,借助动态规划和矩阵变换的思想 ,仍然可以找到突破口
因为骰子的姿态只有 24 24 2 4 种,并且 y ∈ [ 0 , 4 ) y \in [0, 4) y ∈ [ 0 , 4 ) 范围也很小
也就是说,对于第 i i i 列而言,它总共的姿态才 24 ⋅ 4 = 96 24 \cdot 4 = 96 2 4 ⋅ 4 = 9 6 种
来看看从第 i i i 列到第 i + 1 i+1 i + 1 列骰子的姿态是如何变化的?
骰子从 i i i 列走到 i + 1 i+1 i + 1 ,一种走法是向右翻转,有可能此时的代价太大
一种更优的策略是先向左翻转,然后向上,再向右,再向下
不妨假设骰子在第 x x x 列,姿态为 i i i ,想要走到第 x + 1 x+1 x + 1 列,目标姿态为 j j j
那么存在中间的过渡姿态 k k k ,使得 i → k 1 ⋯ → k m → j i \to k_1 \cdots \to k_m \to j i → k 1 ⋯ → k m → j 更优
x → x + 1 x \to x+1 x → x + 1 转移的时候,可以枚举 x x x 位置所有的 96 96 9 6 种姿态 u u u ,对于每一种姿态
挨个 求一遍单源最短路 dijkstra ( u ) \text{dijkstra}(u) dijkstra ( u ) ,得到距离向量 f f f
此时 v ∈ [ 0 , 96 ) v \in [0, 96) v ∈ [ 0 , 9 6 ) 对应 x + 1 x+1 x + 1 的 96 96 9 6 种姿态,由此可以构造出完全图
M ( u , v ) = f ( v ) \bm{M}(u, v) = f(v) M ( u , v ) = f ( v )
到了这里,问题解法就呼之欲出了,以上方法我们可以得到一个 96 × 96 96 \times 96 9 6 × 9 6 的完全图 M \bm{M} M
floyd \text{floyd} floyd 算法中有一个结论,完全图 A \bm{A} A 中 s → t s \to t s → t 恰好经过 n n n 条边的最短路为 A n ( s , t ) \bm{A}^n(s, t) A n ( s , t )
借鉴这个思路,x x x 列骰子的姿态为 i i i ,x + 1 x+1 x + 1 列骰子姿态为 j j j
那么姿态 i → j i \to j i → j 转移的最小代价为 M ( i , j ) \bm{M}(i, j) M ( i , j )
x → x + 1 → x + 2 x \to x+1 \to x+2 x → x + 1 → x + 2 呢?假设 x x x 列姿态为 i i i ,状态压缩为 ( x , i ) (x, i) ( x , i ) ,第 x + 2 x+2 x + 2 列姿态为 j j j ,压缩为 ( x + 2 , j ) (x+2, j) ( x + 2 , j )
( x , i ) (x, i) ( x , i ) 到 ( x + 2 , j ) (x+2, j) ( x + 2 , j ) 转移的最小代价不妨记为 M 2 ( i , j ) \bm{M}_2(i, j) M 2 ( i , j )
枚举 x + 1 x+1 x + 1 列的姿态 k k k ,那么 M 2 ( i , j ) = min k ∈ [ 0 , 96 ) ( M ( i , k ) + M ( k , j ) ) \bm{M}_2(i, j) = \displaystyle \min_{k \in [0, 96)} \left(\bm{M}(i, k) + \bm{M}(k, j) \right) M 2 ( i , j ) = k ∈ [ 0 , 9 6 ) min ( M ( i , k ) + M ( k , j ) )
令 d = ∣ x 2 − x 1 ∣ d = |x_2 - x_1| d = ∣ x 2 − x 1 ∣ ,假设起点 x 1 x_1 x 1 处姿态为 i i i ,终点 x 2 x_2 x 2 处姿态为 j j j ,那么
( x 1 , i ) → ( x 2 , j ) (x_1, i) \to (x_2, j) ( x 1 , i ) → ( x 2 , j ) 的最小代价为 M d \bm{M}^d M d ,这可以用矩阵快速幂求解
算法设计,建图
将骰子所有姿态预处理出来,并用一个 vector \text{vector} vector 保存,不妨设为 dice24 ( ⋯ ) \text{dice24}(\cdots) dice24 ( ⋯ )
状态压缩,举个例子,假设当前在 x x x 位置,y y y 列,骰子的姿态为 dice24 ( i ) = { 0 , 1 , 2 , 3 , 4 , 5 } \text{dice24}(i) = \{0, 1, 2, 3, 4, 5\} dice24 ( i ) = { 0 , 1 , 2 , 3 , 4 , 5 }
当前状态为 ( x , y , dice24 ( i ) ) (x, y, \text{dice24}(i)) ( x , y , dice24 ( i ) )
枚举所有可能的状态并建图,为了能把骰子所有状态都枚举到
一开始的时候要建立一个 X × Y X \times Y X × Y 区域,Y Y Y 题目规定为 4 4 4 ,X X X 呢?
由于骰子水平翻转 4 4 4 次就会回到原来姿态,可以令 X = 10 X = 10 X = 1 0 ,这样一定能够把所有姿态都枚举到
∀ x ∈ [ 0 , X ) , ∀ y ∈ [ 0 , 4 ) , ∀ i ∈ [ 0 , 24 ) \forall x \in [0, X), \ \forall y \in [0, 4), \forall i \in [0, 24) ∀ x ∈ [ 0 , X ) , ∀ y ∈ [ 0 , 4 ) , ∀ i ∈ [ 0 , 2 4 ) ,对于每一种状态 ( x , y , dice24 ( i ) ) (x, y, \text{dice24}(i)) ( x , y , dice24 ( i ) )
要处理以下几个细节
先是费用计算,按前面所说的,先对 dice24 ( i ) \text{dice24}(i) dice24 ( i ) 进行“染色”,即枚举 j ∈ [ 0 , 6 ) j \in [0, 6) j ∈ [ 0 , 6 ) 个面,骰子标准姿态设为 A ( j ) A(j) A ( j )
B ( dice24 ( i , j ) ) = A ( j ) B(\text{dice24}(i, j)) = A(j) B ( dice24 ( i , j ) ) = A ( j ) ,则此时 w = B [ 0 ] w = B[0] w = B [ 0 ] 就是翻转后 朝上的面的数值,即翻转代价
每一种状态 ( x , y , dice24 ( i ) ) (x, y, \text{dice24}(i)) ( x , y , dice24 ( i ) ) ,尝试前后左右 4 4 4 个方向
这里需要一个 rot ( d i r , dice24 ( i ) ) \textbf{rot}(dir, \text{dice24}(i)) rot ( d i r , dice24 ( i ) ) 函数,计算出旋转后相对应的姿态 dice24 ( n i ) \text{dice24}(ni) dice24 ( n i )
根据 dice24 ( n i ) \text{dice24}(ni) dice24 ( n i ) 计算出翻转代价 w w w
( x , y , dice24 ( i ) ) (x, y, \text{dice24}(i)) ( x , y , dice24 ( i ) ) 到 ( n x , n y , dice24 ( n i ) ) (nx, ny, \text{dice24}(ni)) ( n x , n y , dice24 ( n i ) ) 之间连一条权值为 w w w 的边
最短路与矩阵快速幂
初始状态是已知的,令 x ← X 2 x \leftarrow \displaystyle\frac{X}{2} x ← 2 X ,y y y 为给定值
d = ∣ x 1 − x 2 ∣ d = |x_1 - x_2| d = ∣ x 1 − x 2 ∣ 为始末状态 x x x 坐标的差,标准姿态 A = { 0 , 1 , 2 , 3 , 4 , 5 } \bm{A} = \{0, 1, 2, 3, 4, 5\} A = { 0 , 1 , 2 , 3 , 4 , 5 }
则初始状态为 S ( x , y , A ) S(x, y, \bm{A}) S ( x , y , A )
执行 dijkstra ( S ) \text{dijkstra}(S) dijkstra ( S ) 并且得到距离向量 f f f ,f ( u ) f(u) f ( u ) 表示从 S → u S \to u S → u 的最短路径
f ( u ) ≠ + ∞ f(u) \neq +\infty f ( u ) = + ∞ 的状态 u u u ,就是从 S S S 可达的状态
这里如果对状态 ( x , y , d i c e 24 ) (x, y, \bm{dice24}) ( x , y , d i c e 2 4 ) 设计哈希函数 get = 96 ⋅ x + hash ( y , d i c e 24 ) \text{get} = 96 \cdot x + \text{hash}(y, \bm{dice24}) get = 9 6 ⋅ x + hash ( y , d i c e 2 4 )
就可以枚举所有 96 96 9 6 种可能的状态 ∀ y ∈ [ 0 , 4 ) , i ∈ [ 0 , 24 ) \forall y \in [0, 4), \bm{i} \in [0, 24) ∀ y ∈ [ 0 , 4 ) , i ∈ [ 0 , 2 4 ) ,令 u = get ( x , y , i ) u = \textbf{get}(x, y, \bm{i}) u = get ( x , y , i )
将 dijkstra \text{dijkstra} dijkstra 可达状态 的最短路径保存下来,g ( mp [ ( y , i ) ] ) ← f ( u ) \bm{g}(\text{mp}[(y, \bm{i})]) \leftarrow f(u) g ( mp [ ( y , i ) ] ) ← f ( u ) 即可
说明,u ( x , y , d i c e 24 ) u(x, y, \bm{dice24}) u ( x , y , d i c e 2 4 ) 为三元组,实际上我们并不需要 x x x 这个维度的信息
( y , d i c e 24 ) (y, \bm{dice24}) ( y , d i c e 2 4 ) 才是我们关注的,用一个 mp \text{mp} mp 将 ( y , d i c e 24 ) (y, \bm{dice24}) ( y , d i c e 2 4 ) 映射到哈希值
接下来构造 96 × 96 96 \times 96 9 6 × 9 6 的完全图矩阵,用来表示任意两个姿态 i , j i, j i , j 间转移的代价
枚举骰子的姿态 i \bm{i} i 和 j \bm{j} j ,对于姿态 i → j \bm{i} \to \bm{j} i → j 的变换,构造邻接矩阵
简单来说,分别从每个起始姿态 i \bm{i} i 开始执行 dijkstra \text{dijkstra} dijkstra ,看看它能够到达 哪些姿态
可达的姿态不妨记为 j j j ,可以根据 dijkstra \text{dijkstra} dijkstra 的距离向量 f f f ,构造 M ( i , j ) = f ( j ) \bm{M}(i, j) = f(j) M ( i , j ) = f ( j )
具体来说,每一个起始状态 u = ( x , y , i ) u = (x, y, \bm{i}) u = ( x , y , i ) 对应 96 96 9 6 种姿态中的 u u ( y , i ) \bm{uu}(y, \bm{i}) u u ( y , i )
每个起始状态都执行 dijkstra ( u ) \text{dijkstra}(u) dijkstra ( u ) ,然后扫描每一个终点状态
终点状态为 v = ( x + 1 , y 2 , j ) v = (x+1, y2, \bm{j}) v = ( x + 1 , y 2 , j ) ,同样对应 96 96 9 6 种姿态中的 v v ( y 2 , j ) \bm{vv}(y2, \bm{j}) v v ( y 2 , j )
M ( u u , v v ) = f ( v ) \bm{M}(\bm{uu}, \bm{vv}) = f(v) M ( u u , v v ) = f ( v )
执行矩阵快速幂,答案为 ∀ i ∈ [ 0 , 96 ) , ∀ j ∈ [ 0 , 96 ) \forall i \in [0, 96), \forall j \in [0, 96) ∀ i ∈ [ 0 , 9 6 ) , ∀ j ∈ [ 0 , 9 6 )
min ( g ( i ) + M d ( i , j ) ) \displaystyle \min \left(\bm{g}(i) + \bm{M}^{d}(i, j) \right) min ( g ( i ) + M d ( i , j ) )
注意这里 j j j 状态必须合法,j j j 压缩了状态 ( y , d i c e 24 ) (y, \bm{dice24}) ( y , d i c e 2 4 ) ,用一个 map \text{map} map 映射即可解决
只有当 mp ( j ) . y = Y 2 \text{mp}(j).y = Y2 mp ( j ) . y = Y 2 时,才合法 (Y 2 Y2 Y 2 是终点的纵坐标)
骰子旋转与染色
以图中为例子,往上翻转为例子,关注下标
往上翻转,侧面不变,1 → 1 , 4 → 4 1 \to 1, \ 4 \to 4 1 → 1 , 4 → 4
其他面下标变化 0 → 2 , 2 → 5 5 → 3 , 3 → 0 0 \to 2, \ 2 \to 5 \ 5 \to 3, \ 3 \to 0 0 → 2 , 2 → 5 5 → 3 , 3 → 0
1 2 3 4 5 6 7 8 9 10 11 12 const vector<int> up = {2, 1, 5, 0, 4, 3}; inline void rot(const vector<int> &T, vector<int> &p) { vector<int> q = p; for (int i = 0; i < 6; i++) p[i] = T[q[i]]; } inline vector<int> paint(const vector<int> &dice, const vector<int> &p) { vector<int> res = p; for (int i = 0; i < 6; i++) res[dice[i]] = p[i]; return res; }
骰子枚举所有姿态的代码
具体来说,就是从 0 , 1 , 2 , 3 , 4 , 5 {0, 1, 2, 3, 4, 5} 0 , 1 , 2 , 3 , 4 , 5 中任意选一个数转到顶面或者正面
然后这个面不动,左右依次翻转 3 3 3 次,就可以打表出所有姿态了
下面的例子是 dice contest 问题中的骰子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void getDice24 () { const vector<int> p0 = {0, 1, 2, 3, 4, 5}; for (int i = 0; i < 6; i++) { // in face vector<int> p = p0; if (i == 0) { rot(rdown, p); } if (i == 2) { rot(rri, p), rot(rdown, p); } if (i == 3) { rot(rle, p), rot(rdown, p); } if (i == 4) { rot(rdown, p), rot(rdown, p); } if (i == 5) { rot(rup, p); } _for(_, 0, 4) { printf ("{%d, %d, %d, %d, %d, %d},\n" , p[0], p[1], p[2], p[3], p[4], p[5]); rot(rle, p); } } }
最长路与正环
bellman ford \text{bellman ford} bellman ford 算法不仅可以用来求最短路,如果将松弛方程改为
d ( y ) < d ( x ) + e ( x , y ) , d ( y ) = d ( x ) + e ( x , y ) d(y) < d(x) + e(x, y), \ d(y) = d(x) + e(x, y) d ( y ) < d ( x ) + e ( x , y ) , d ( y ) = d ( x ) + e ( x , y )
统计点 u u u 入队次数,如果 cnt ( u ) > n \text{cnt}(u) > n cnt ( u ) > n 那么就有正环
Tomato Automata
因为循环的处理比较复杂,先来看没有循环语句的情况
如果没有循环语句,那么其他语句构成了链式结构,如果从 u u u 语句跳转到 v v v 语句
可以用单向链表来存储,判环可以用快慢指针,如果没有环,就看走到 die \text{die} die 处要多少步
借助该思路,可以对程序语句建图 ,对于跳转语句,比如 Ifgo v \text{Ifgo} \ v Ifgo v
表示当前在 u u u 行,跳转到 v v v 行,用前向弧的方法来加边
此时在 ( u , v ) (u, v) ( u , v ) 间添加权值为 1 1 1 的边,表示从 u u u 到达 v v v 需要多执行 1 1 1 步程序
那么 loop s x \text{loop} \ s \ x loop s x 呢?即从 s → u s \to u s → u 循环 x x x 次
那么需要高效统计出 ( s → u ) (s \to u) ( s → u ) 一共执行了多少句语句 s u m sum s u m
进一步考虑该问题
题目中要求循环严格嵌套,并且不能从循环内部跳到循环外部
程序能停机,程序语句必须构成一个有向无环图 ,先简单地根据程序语句建一张图
然后判断这张图是否为 DAG,可以用 tarjan 的 dfs 来判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void dfs(int u, int fa, int &fl) { vis[u] = 1; for (auto v : G[u]) { if (vis[v] == 1) { fl = 1; continue ; } // 处理其他信息 if (vis[v] == 0) dfs(v, u, fl); } vis[u] = 2; }
对于语句 Ifgo x \text{Ifgo} \ x Ifgo x 有两种选择,一种是跳到 x x x 行,另一种是执行下一行
假设当前行为 i i i ,Ifgo x \text{Ifgo} \ x Ifgo x 语句,( i , x ) , ( i , i + 1 ) (i, x), (i, i+1) ( i , x ) , ( i , i + 1 ) 都要连边
loop \text{loop} loop 语句,( i , i + 1 ) (i, i+1) ( i , i + 1 ) 要连边,由于不能从循环内部跳到外部
如果有死循环一定是循环内部有环,这一点可以构建完 DAG 之后先判环
具体来说,执行 dfs 判断有向图是否为 DAG,在 dfs 的同时维护有关信息
对于 dfs 的树边 ( u , v ) (u, v) ( u , v ) ,(即 dfs 的一个分支),此时程序语句从 u u u 执行到 v v v
一定会多执行一条语句,在最终的新图 G G G 中加边 add ( u , v , 1 ) \text{add}(u, v, 1) add ( u , v , 1 )
考虑到会有多条 die \text{die} die 语句,可以新建一个超级汇点 T T T ,add ( die , T , 0 ) \text{add}(\text{die}, T, 0) add ( die , T , 0 )
循环语句 loop p x \text{loop} \ p \ x loop p x ,表示从 p p p 行到当前行 i i i ,多执行 x − 1 x-1 x − 1 次
这取决于 p → i p \to i p → i 之间有多少条语句
注意到我们在新图 G G G 中已经离线保存程序相邻两步 u → v u \to v u → v 需要执行多少条语句
可以采用一边 spfa \text{spfa} spfa 一边维护 的方法,具体来说
如果当前语句 u u u 是循环语句,那么 ( u , v ) (u, v) ( u , v ) 需要在原来的基础上多执行循环的部分
循环部分的总语句数是 ( x − 1 ) ⋅ Δ (x-1) \cdot \Delta ( x − 1 ) ⋅ Δ ,只需要在线修改 ( u , v ) (u, v) ( u , v ) 的权值,增加 ( x − 1 ) ⋅ Δ (x-1) \cdot \Delta ( x − 1 ) ⋅ Δ
而 spfa \text{spfa} spfa 的距离向量 f ( i ) f(i) f ( i ) 恰好表示从第一条语句到第 i i i 条语句,一共执行的语句数量
所以 Δ = ( f ( i ) − f ( p ) + 1 ) \Delta = (f(i) - f(p) + 1) Δ = ( f ( i ) − f ( p ) + 1 )
综上,执行 spfa ( 1 ) \text{spfa}(1) spfa ( 1 ) ,对于边 ( u , v ) , w = e ( u , v ) (u, v), \ w = e(u, v) ( u , v ) , w = e ( u , v ) ,特别地,如果 u u u 为循环语句
w + = ( f ( u ) − f ( p ) + 1 ) ⋅ ( x − 1 ) w += (f(u) - f(p) + 1) \cdot (x-1) w + = ( f ( u ) − f ( p ) + 1 ) ⋅ ( x − 1 ) ,执行松弛 f ( y ) = max ( f ( y ) , f ( x ) + w ) f(y) = \max (f(y), f(x) + w) f ( y ) = max ( f ( y ) , f ( x ) + w )
初始化 f ( 1 ) = 1 f(1) = 1 f ( 1 ) = 1 ,因为第一条语句总会执行,建立一个超级汇点 T T T ,答案为 f ( T ) f(T) f ( T )