差分约束系统
n 个未知量 x 1 , x 2 ⋯ x n ,m 个不等式条件
这样的一组不等式就是一个差分约束系统,差分约束系统的求解可以巧妙的转化为最短路问题
和最短路的关系
先移项,得到
,可以发现和最短路中的三角不等式非常相似, 。因而可以转化成最短路问题,具体来说,先对不等式建图,每个不等式都可看成边,让
j 向 i 连一条权为 c 的边,此图上跑最短路,得到的 d i s i
就是一组满足条件的解。
转化的充要性证明
限制条件转化为有向边,跑最短路得到的 d i s 满足
,把 d i s u , d i s v
看成 x u , x v
的话,也就是
,因而当最短路有解,也就是没有负环,原不等式就有解,且解就为
d i s ,必要性成立
思考一下变量关系,若是关系无环比如说是一条链,容易想到,此时的不等式和最短路都是有解的。若是变量关系成环,设存在一个有向环
,对应的边权依次为 , , , 。
由每条边对应的约束,有 将上述不等式逐项代入相消可得 若不等式组有解,则必有
否则会出现矛盾,而这也意味着图中不存在负环,所以此时最短路也是有解的,充分性成立
因此,不等式组有解等价于图中的最短路有解,这个转化是充分且必要的
最小解和最大解
若是 ,这样从 u 跑到 v ,得到 ,可以发现
x v
的解正好取等,即最大解,也可推出其他变量的解都为最大解。若我们想要最小解,那么让
,这样从 u 跑到
v ,不就有最小解了吗。但是,这样还是跑最短路吗?再观察
,回想一下我们前面的证明,可以发现最短路的关键性质, ,和此不等式不再契合。然而有没有什么满足
这个性质呢?仔细一看,这不正是最长路的性质吗,对的,我们直接跑最长路就可以了,相似的,无解条件为存在正环
因此, 这样 建边跑最短路能得到最大解, 这样 建边跑最长路能得到最小解
负权图最短路
有负权边后,dijkstra 就不再适用了,因为 dijkstra
基于贪心思想,每走一步期望的 dis
都应该是单调不减的。所以我们需要可以在负权图上跑最 短/长 路,且能判
负/正 环的算法
Bellman-Ford O (n ⋅ m )
此算法暴力的进行 n − 1
轮,每轮都从所有的点向所有方向走,这样最短路一定可以延展出来,因为一条简单路最多
n − 1
条边嘛。正是因为它非常之暴力,所以能处理 d i j k s t r a
处理不了的负权问题。还能检测负环,可以额外跑一轮,若还有 d i s
可以更新的话,那只能是存在负环了
MYCODE
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 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int , int >;constexpr ll inf = 1e18 ;void solve () { int n, m; cin >> n >> m; vector<vector<pii>> gra (n + 1 ); for (int i=1 ; i<=m; i++){ int u, v, w; cin >> u >> v >> w; if (w >= 0 ){ gra[u].push_back ({v, w}); gra[v].push_back ({u, w}); } else gra[u].push_back ({v, w}); } vector<ll> dis (n + 1 , inf) ; dis[1 ] = 0 ; for (int k=0 ; k<n-1 ; k++){ for (int i=1 ; i<=n; i++){ if (dis[i] == inf) continue ; for (auto [v, w] : gra[i]){ dis[v] = min (dis[v], dis[i] + w); } } } bool neg = false ; for (int i=1 ; i<=n; i++){ if (dis[i] == inf) continue ; for (auto [v, w] : gra[i]){ neg |= dis[i]+w < dis[v]; } } cout << (neg? "YES" :"NO" ) << '\n' ; } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int t = 1 ; cin >> t; while (t--) solve (); return 0 ; }
SPFA O (k ⋅ m )
在 B e l l m a n -F o r d
中,最短路还没延展到的点的遍历是多余的。所以可以用一个队列,存已经延展的点,从这些点去扩展。s p f a
非常类似换掉 p q 的
d i j k s t r a ,它是
B e l l m a n -F o r d
算法的队列优化版,复杂度为 O (k ⋅ m ) ~ O (n ⋅ m ) 。此外,也有启发式的
s p f a ,不过比较玄学,启发的大致思想是,更容易松弛别人的点就放队列前,在此不过多赘述
MYCODE
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 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int , int >;constexpr ll inf = 1e18 ;void solve () { int n, m; cin >> n >> m; vector<vector<pii>> gra (n + 1 ); for (int i=1 ; i<=m; i++){ int u, v, w; cin >> u >> v >> w; if (w >= 0 ){ gra[u].push_back ({v, w}); gra[v].push_back ({u, w}); } else gra[u].push_back ({v, w}); } bool neg = false ; int s = 1 ; vector<bool > inq (n + 1 ) ; vector<int > cnt (n + 1 ) ; vector<ll> dis (n + 1 , inf) ; dis[s] = 0 , inq[s] = true ; queue<int > q; q.push (s); while (!q.empty () && !neg){ int u = q.front (); q.pop (); inq[u] = false ; for (auto [v, w] : gra[u]){ if (dis[u] + w < dis[v]){ dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1 ; if (!inq[v]) q.push (v), inq[v]=true ; if (cnt[v] >= n){ neg = true ; break ; } } } } cout << (neg? "YES" :"NO" ) << '\n' ; } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int t = 1 ; cin >> t; while (t--) solve (); return 0 ; }
题目链接: 【模板】负环
差分约束例题
我们先建一个超级源点 s,其对所有点建一条权为 0
的边,主要目的是让图连通,方便处理,也相当于加了限制条件
,而且这样操作是不影响可解性的,因为若有解,则整体平移后也是一个解。建好图,从
s 开始跑一下最短路就可以了。
注意 : 加源点后图中就是 n + 1 个节点了 , 要判断 c n t [v ] ≥ n + 1
MYCODE
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 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int , int >;constexpr ll inf = 3e18 ;void solve () { int n, m; cin >> n >> m; vector<vector<pii>> gra (n + 1 ); for (int i=1 ; i<=m; i++){ int u, v, w; cin >> v >> u >> w; gra[u].push_back ({v, w}); } for (int i=1 ; i<=n; i++){ gra[0 ].push_back ({i, 0 }); } bool neg = false ; int s = 0 ; vector<bool > inq (n + 1 ) ; vector<int > cnt (n + 1 ) ; vector<ll> dis (n + 1 , inf) ; dis[s] = 0 , inq[s] = true ; queue<int > q; q.push (s); while (!q.empty () && !neg){ int u = q.front (); q.pop (); inq[u] = false ; for (auto [v, w] : gra[u]){ if (dis[u] + w < dis[v]){ dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1 ; if (!inq[v]) q.push (v), inq[v]=true ; if (cnt[v] >= n+1 ){ neg = true ; break ; } } } } if (neg){ cout << "NO" << '\n' ; return ; } for (int i=1 ; i<=n; i++){ cout << dis[i] << " \n" [i == n]; } } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int t = 1 ; while (t--) solve (); return 0 ; }
题意
n 个区间 [a i , b i ] ,每个区间至少选
c i
个整数。满足这 n
条限制的情况下,至少要取多少个整数
解析
每条限制可转化成
,很容易想到差分约束。关键是如何建图去描述这个问题,思考
p r e
数组的特点,大概为
, ,这两个条件已经足够描述出合法的前缀和数组,我们将此额外限制加入图中,这样就可以得到合法的解
整理一下 : 这题需要求最小解,我们要跑最长路。此外,这题值域有
0,我们简单平移一下即可
MYCODE
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 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int , int >;constexpr ll inf = 1e18 ;const int MX = 5e4 +1 ;void solve () { int n; cin >> n; vector<vector<pii>> gra (MX + 1 ); for (int i=1 ; i<=n; i++){ int u, v, c; cin >> u >> v >> c; u++, v++, c; gra[u-1 ].push_back ({v, c}); } for (int i=1 ; i<=MX; i++){ gra[i-1 ].push_back ({i, 0 }); gra[i].push_back ({i-1 , -1 }); } int s = 0 ; vector<bool > inq (MX + 1 ) ; vector<ll> dis (MX + 1 , -inf) ; dis[s] = 0 , inq[s] = true ; queue<int > q; q.push (s); while (!q.empty ()){ int u = q.front (); q.pop (); inq[u] = false ; for (auto [v, w] : gra[u]){ if (dis[u] + w > dis[v]){ dis[v] = dis[u] + w; if (!inq[v]) q.push (v); inq[v] = true ; } } } cout << dis[MX] << '\n' ; } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int t = 1 ; cin >> t; while (t--) solve (); return 0 ; }
和上面那道题比较相似,但多了一个二分,限制条件要想好,注意 spfa
中的剪枝,否则会 tle
题意
n 个立方体一排,涂色,有两种限制,分别有 M 1 ,M 2 个。第一种:[L i , R i ]
中的立方体涂色个数 ≥ k ,第二种:[L i , R i ]
外的立方体涂色个数 ≥ k 。求满足限制的最小染色立方体数
(n ≤ 3000, 0 ≤ M 1 , M 2 ≤ 3000)
解析
和上面的题比较相似。不同点在于限制二是
,多一个变量,容易想到二分 p r e n
去掉一个变量,这样就转化成了差分约束问题。我们二分 p r e n ,将 设为 ,因为前缀和的性质,我们还有
, 。
限制条件即为 :
要注意的是,为了解的正确性,我们至少将所有的限制边都遍历过一遍,而此题仅从超级源点出发不是很能保证满足这个条件。因为
而 ,可能会有 ,这导致点 n
不会入队,也就会使得点 n
相关的限制边没有遍历到,从而导致错解。解决办法为,不建超级源点
s,而是直接将每个点都加入队列中,这样一定可以保证每个限制边都遍历过
另,此人竟然提交了惊人的 77 发之后 ? ? 终于算是过掉了这题 ? ? QAQ
( 这人也太菜了,我无言以对 )
MYCODE
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 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int , int >;constexpr ll inf = 3e18 ;void solve () { int n, m1, m2; cin >> n >> m1 >> m2; vector<tuple<int , int , int >> q1 (m1), q2 (m2); for (auto & [l, r, c] : q1) cin >> l >> r >> c; for (auto & [l, r, c] : q2) cin >> l >> r >> c; vector<bool > inq (n + 1 ) ; vector<int > cnt (n + 1 ) ; vector<ll> dis (n + 1 , -inf) ; vector<vector<pii>> gra (n + 1 ); auto check = [&](int pn){ inq.assign (n+1 , 0 ), cnt.assign (n+1 , 0 ), dis.assign (n+1 , -inf); for (int i=0 ; i<=n; i++) gra[i].clear (); for (auto [l, r, c] : q1){ gra[l-1 ].push_back ({r, c}); } for (auto [l, r, c] : q2){ gra[r].push_back ({l-1 , c-pn}); } for (int i=1 ; i<=n; i++){ gra[i-1 ].push_back ({i, 0 }); gra[i].push_back ({i-1 , -1 }); } queue<int > q; for (int i=0 ; i<=n; i++) q.push (i), inq[i] = true ; dis[0 ] = 0 , dis[n] = pn; while (!q.empty ()){ int u = q.front (); q.pop (); inq[u] = false ; if (u == 0 && dis[u] > 0 ) return false ; if (u == n && dis[u] > pn) return false ; for (auto [v, w] : gra[u]){ if (dis[u] + w > dis[v]){ dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1 ; if (!inq[v]) q.push (v), inq[v] = true ; if (cnt[v] >= n+1 ) return false ; } } } return true ; }; int lo = -1 , hi = n+1 ; while (lo + 1 != hi){ int mid = lo + hi >> 1 ; if (check (mid)) hi = mid; else lo = mid; } cout << hi << '\n' ; } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int t = 1 ; cin >> t; while (t--) solve (); return 0 ; }
此外 : ,也可以转化成差分约束系统,两边取 log 即可
此篇完结.