0. 前言你需要知道:
SPFA 图论基础建模 不等式基础 1. 负环为什么先讲负环啦?是因为差分约束是要用负环的。
给定一张有向图(无向图看作两条等权边),每条边有权值。若一个边权值是负数,那么我们称这个边为负权边。
若图中存在一个环,如果环上的边有负权边,那么我们称这个为负环。
例如4,5,7构成负环,而5,8,1构成正环
我们可以用最短路算法来跑判断负环,什么你跟我说拓扑排序?如果一张图既有正环也有负环那判断不出来啊,拓扑只能判环不能判是哪个类型的。
但是不是所有都能用…
算法 适用条件 时间复杂度 Dijkstra 不可以,负边权会对之后结果造成影响不一定最小。 O ( n 2 ) → O ( m log n ) O(n^2)\rightarrow O(m \log n) O ( n 2 ) → O ( m log n ) Bellman-Ford 可以,无负环的时候最短路边数一定小于n n n ,无负环的情况下走n − 1 n-1 n − 1 次后一定收敛。 O ( n m O(nm O ( n m )SPFA 同Bellman-Ford O ( n m ) → O ( k m ) O(nm)\rightarrow O(km) O ( n m ) → O ( k m )
所以也就只能用SPFA了…
所以怎么判断?很简单,只需要开个桶,如果一个点走了不少于n n n 次那么一定有负环。那么就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool spfa () { queue<int > q; while (!q.empty ()){ int u=q.front (); q.pop (); vis[u]=0 ; for (auto e:adj[u]){ if (dis[e.v]<dis[u]+e.w){ dis[e.v]=dis[u]+e.w; if (!vis[e.v]){ vis[e.v]=1 ; q.push (e.v); cnt[e.v]++; if (cnt[e.v]>=n) return 1 ; } } } } return 0 ; }
你跟我说负边权图被卡SPFA了?可以考虑一下DAG跑。
2.差分约束系统差分约束系统是一种特殊的N N N 元一次不等式组,有N N N 个变量与M M M 个约束条件(其实就是不等式)。
所以为什么叫差分约束呢?是因为每一个不等式都是两个变量作差得到的:
{ X 2 − X 1 ≤ C 1 X 3 − X 2 ≤ C 2 X 4 − X 3 ≤ C 3 \begin{cases} X_2-X_{1} \le C_{1} \\ X_3-X_{2}\le C_{2} \\ X_4-X_{3}\le C_{3} \\ \end{cases} ⎩ ⎪ ⎪ ⎨ ⎪ ⎪ ⎧ X 2 − X 1 ≤ C 1 X 3 − X 2 ≤ C 2 X 4 − X 3 ≤ C 3
如上,都是作差得到的,所以叫差分约束了。
我们取第一个出来,我们变变形:
X 2 ≤ X 1 + C 1 X_2 \le X_{1}+C_{1} X 2 ≤ X 1 + C 1
是不是有点像dis[e.v]<dis[u]+e.w
,及其相似。
我们怎么求解这些方程组呢?我们可以像上面的式子一样,我们就让后面的连前面的,例如下面的式子:
X i − X j ≤ C i X_i-X_{j}\le C_i X i − X j ≤ C i
我们从X j → X i X_j\rightarrow X_i X j → X i 连边,边权为C i C_i C i 。这样就可以了,但是这个有多组解(两边统一加上一个常数d d d 不等式仍然成立),而且还有负数解,我们不喜欢负数解,但是我们可以加一个源点X 0 X_0 X 0 ,我们不妨令X 0 = 0 X_0=0 X 0 = 0 ,让后显然有X i − X 0 ≤ 0 X_i-X_{0}\le 0 X i − X 0 ≤ 0 ,那么0 → i 0\rightarrow i 0 → i 连边权为0 0 0 的边,这样就可以保证都是整数解了。
不妨设d i s [ 0 ] = 0 dis[0]=0 d i s [ 0 ] = 0 ,让后以此为起点跑单源最短路,这样我们就能求出一组解了。如果有负环那么说明不等式不成立,那么就无解。
2.1 大于等于推导当然做题你会发现如下的式子:
X i − X j ≥ C i X_i-X_{j}\ge C_i X i − X j ≥ C i
这个时候有两个方法,第一个两边都取负,不等式变号,就有X j − X i ≤ − C i X_j-X_{i}\le -C_i X j − X i ≤ − C i ,九二可以连边了;第二种方法就是改成跑单源最长路,如果有正环说明无解。
2.2 等于式推导还有如下的式子:
X i = X j X_i=X_j X i = X j
其实可以转化成:
X i = X j ⇒ { X i − X j ≤ 0 X j − X i ≤ 0 X_i=X_{j}\Rightarrow \begin{cases} X_i-X_{j}\le 0 \\ X_j-X_{i}\le 0 \end{cases} X i = X j ⇒ { X i − X j ≤ 0 X j − X i ≤ 0
2.3 分式推导更有甚者:
X i X j ≤ C i \frac{X_i}{X_{j}}\le C_i X j X i ≤ C i
这是差分约束吗?其实也可以变变形,回忆对数相减代表这什么,其实就可以转化为:
log X i X j = log a X i − log a X j ≤ log a C i \log\frac{X_i}{X_{j}}=\log_aX_i-\log_aX_{j}\le \log_{a}C_i log X j X i = log a X i − log a X j ≤ log a C i
做就可以了,这个a a a 取大于0的实数即可,但是取2就可以了。
总结一下:
差分约束 转化后 连边(w为边权) X i − X j ≥ c i X_i-X_j\ge c_i X i − X j ≥ c i X j − X i ≤ − C i X_j-X_{i}\le -C_i X j − X i ≤ − C i i → j , w i = − C i i \rightarrow j,w_i=-C_i i → j , w i = − C i X i − X j ≤ C i X_i-X_{j}\le C_i X i − X j ≤ C i 同前 j → i , w i = C i j \rightarrow i,w_i=C_i j → i , w i = C i X i = X j X_i=X_j X i = X j X i − X j ≤ 0 , X j − X i ≤ 0 X_i-X_{j}\le 0,X_j-X_{i}\le 0 X i − X j ≤ 0 , X j − X i ≤ 0 i ↔ j , w i = 0 i \leftrightarrow j,w_i=0 i ↔ j , w i = 0 X i X j ≤ C i \frac{X_i}{X_{j}}\le C_i X j X i ≤ C i l o g a X i − log a X j ≤ log a C i log_aX_i-\log_aX_{j}\le \log_{a}C_i l o g a X i − log a X j ≤ log a C i j → i , w i = log a C i j\rightarrow i,w_i=\log_a C_i j → i , w i = log a C i
让后跑spfa就可以了www。
2.1 例题——Interval例题:六倍经验——SP116 Interval
有n n n 个区间,在区间[ a i , b i ] [a_i,b_i] [ a i , b i ] 中至少取任意互不相同的c i c_i c i 个整数。求在满足n n n 个区间的情况下,至少要取多少个正整数。
做法1:差分约束
其实就是让我们选数,变量就是数的数量。
不难发现有x [ b i ] − x [ a i − 1 ] ≥ c i x[b_i]-x[a_{i-1}]\ge c_i x [ b i ] − x [ a i − 1 ] ≥ c i ,但是为了保证我们的解有意义,我们还需要添加$1 ≥ s [ i ] − s [ i − 1 ] ≥ 0 1\ge s[i]-s[i-1] \ge 0 1 ≥ s [ i ] − s [ i − 1 ] ≥ 0 ,保证不能选负数个数,又不能多选出来。
但是0 ≤ a i ≤ 5 × 1 0 4 0\le a_{i}\le 5\times 10^4 0 ≤ a i ≤ 5 × 1 0 4 ,所以我们要从− 1 -1 − 1 开始,很难受,其实也很简单,只需要都加上一即可让后从0开始即可了。
故代码如下:
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 #include <bits/stdc++.h> using namespace std;constexpr int MN=5e4 +15 ,INF=1e9 ;struct Edge { int v,w; }; int n,T,dis[MN];vector<Edge> adj[MN]; bool vis[MN];void init () { for (int i=1 ;i<=n;i++){ adj[i].clear (); dis[i]=-INF; vis[i]=0 ; } } void spfa () { queue<int > q; q.push (0 ); vis[0 ]=1 ; dis[0 ]=0 ; while (!q.empty ()){ int u=q.front (); q.pop (); vis[u]=0 ; for (auto e:adj[u]){ if (dis[e.v]<dis[u]+e.w){ dis[e.v]=dis[u]+e.w; if (!vis[e.v]){ q.push (e.v); vis[e.v]=1 ; } } } } } void solve () { int maxp=-1 ; cin>>n; init (); for (int i=1 ;i<=n;i++){ int a,b,c; cin>>a>>b>>c; adj[a].push_back ({b+1 ,c}); maxp=max (maxp,b+1 ); } for (int i=1 ;i<=maxp;i++){ adj[i-1 ].push_back ({i,0 }); adj[i].push_back ({i-1 ,-1 }); } spfa (); cout<<dis[maxp]<<'\n' ; } int main () { cin>>T; while (T--){ solve (); } return 0 ; }
2.2 强联通分量优化——洛谷P10935,3275我们用一个正整数来表示恒星的亮度,数值越大则恒星就越亮,恒星的亮度最暗是1 1 1 。 现在对于N N N 颗我们关注的恒星,有M M M 对亮度之间的相对关系已经判明。 你的任务就是求出这N N N 颗恒星的亮度值总和至少有多大。 输入格式: 第一行给出两个整数N N N 和M M M 。
之后M M M 行,每行三个整数T , A , B T, A, B T , A , B ,表示一对恒星( A , B ) (A, B) ( A , B ) 之间的亮度关系。恒星的编号从1 1 1 开始。 如果T = 1 T = 1 T = 1 ,说明A A A 和B B B 亮度相等。 如果T = 2 T = 2 T = 2 ,说明A A A 的亮度小于B B B 的亮度。 如果T = 3 T = 3 T = 3 ,说明A A A 的亮度不小于B B B 的亮度。 如果T = 4 T = 4 T = 4 ,说明A A A 的亮度大于B B B 的亮度。 如果T = 5 T = 5 T = 5 ,说明A A A 的亮度不大于B B B 的亮度。 无解输出− 1 -1 − 1 1 ≤ N ≤ 1 0 5 , 1 ≤ M ≤ 1 0 6 1\le N \le 10^5,1\le M \le 10^6 1 ≤ N ≤ 1 0 5 , 1 ≤ M ≤ 1 0 6
首先我们归类一下能直接看出来的式子,这里我们先都转成大于式子,不知道大于式子怎么差分约束的看2.1:
{ X A − X B ≥ 0 , X B − X A ≥ 0 A = B ? A < B X A − X B ≥ 0 A ≥ B ? A > B X B − X A ≥ 0 A ≤ B \begin{cases} X_A-X_B\ge0,X_B-X_{A} \ge 0 & A=B \\ ? & A<B \\ X_A-X_{B} \ge 0 & A\ge B \\ ? & A>B \\ X_B-X_{A}\ge 0 & A\le B \end{cases} ⎩ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎧ X A − X B ≥ 0 , X B − X A ≥ 0 ? X A − X B ≥ 0 ? X B − X A ≥ 0 A = B A < B A ≥ B A > B A ≤ B
我们解决一下小于和大于的情况:
X A − X B < 0 X B − X A > 0 ∵ X B , X A ∈ Z ∴ X B − X A ∈ Z ∴ X B − X A ≥ 1 \begin{aligned} X_A-X_{B}& < 0 \\ X_B-X_{A}& > 0 \\ \because X_B,X_{A} & \in \mathbb{Z} \\ \therefore X_B-X_{A} & \in \mathbb{Z} \\ \therefore X_B-X_{A}& \ge 1 \end{aligned} X A − X B X B − X A ∵ X B , X A ∴ X B − X A ∴ X B − X A < 0 > 0 ∈ Z ∈ Z ≥ 1
大于情况同理。
整理一下:
{ X A − X B ≥ 0 , X B − X A ≥ 0 A = B X B − X A ≥ 1 A < B X A − X B ≥ 0 A ≥ B X A − X B ≥ 1 A > B X B − X A ≥ 0 A ≤ B \begin{cases} X_A-X_B\ge0,X_B-X_{A} \ge 0 & A=B \\ X_B-X_{A}\ge 1 & A<B \\ X_A-X_{B} \ge 0 & A\ge B \\ X_A-X_{B}\ge 1 & A>B \\ X_B-X_{A}\ge 0 & A\le B \end{cases} ⎩ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎧ X A − X B ≥ 0 , X B − X A ≥ 0 X B − X A ≥ 1 X A − X B ≥ 0 X A − X B ≥ 1 X B − X A ≥ 0 A = B A < B A ≥ B A > B A ≤ B
让后我们就可以用SPFA跑,于是代码如下…?怎么N ≤ 1 0 5 N \le 10^5 N ≤ 1 0 5 。
O ( N M ) O(NM) O ( N M ) 炸掉了呜呜呜。怎么做?
观察原图,不难发现每个边的边权不是1就是0,我们判断无解怎么判断?那当然是找环,不难发现如果有环并且环上一旦有权为1的边就是无解(正环无解)。
但是怎么求有向图的环呢?而且复杂度还不能O ( N M ) O(NM) O ( N M ) …Tarjan大法好!强联通分量复杂度O ( N + M ) O(N+M) O ( N + M ) 。
但是不对啊,还有有解的情况呢?怎么求,其实也好,如果有解那么一个强联通分量的答案一定是相同的(因为要求最小所以都填一样就行了),那么直接缩点,缩点完后就是一个DAG,跑拓扑排序单源最长路就可以了。
什么?你不会DAG的单源最长路?
d i s [ v ] = max v ∈ s o n ( u ) ( d i s [ v ] , d i s [ u ] + w e d g e ) dis[v]=\max\limits_{v\in son(u)}(dis[v],dis[u]+w_{edge}) d i s [ v ] = v ∈ s o n ( u ) max ( d i s [ v ] , d i s [ u ] + w e d g e )
这是递推公式,因为初始X X X 最小为1,所以初始化入度为0的节点d i s [ u ] = 0 dis[u]=0 d i s [ u ] = 0 ,答案怎么算?也很简单。
a n s = ∑ i = 1 d c c c n t i × d i s i ans=\sum\limits_{i=1}^{dcc}cnt_{i} \times dis_i a n s = i = 1 ∑ d c c c n t i × d i s i
其中d c c dcc d c c 是强联通分量的个数,c n t cnt c n t 是强联通分量节点个数。
所以代码如下:
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include <bits/stdc++.h> #define int long long using namespace std;constexpr int MN=1e6 +15 ;struct Edge { int v,w; }; int n,m,tot,dcc,top,s[MN],color[MN],in[MN],low[MN],dfn[MN],f[MN];vector<Edge> adj[MN],adj2[MN]; vector<int > vdcc[MN]; bool vis[MN];void tarjan (int u) { low[u]=dfn[u]=++tot; s[++top]=u; vis[u]=1 ; for (auto e:adj[u]){ int v=e.v,w=e.w; if (!dfn[v]){ tarjan (v); low[u]=min (low[u],low[v]); }else if (vis[v]){ low[u]=min (low[u],dfn[v]); } } if (low[u]==dfn[u]){ dcc++; int p; do { p=s[top--]; color[p]=dcc; vdcc[dcc].push_back (p); vis[p]=0 ; } while (p!=u); } } void toposort () { queue<int > q; for (int i=1 ;i<=dcc;i++){ if (!in[i]){ q.push (i); f[i]=1 ; } } while (!q.empty ()){ int u=q.front (); q.pop (); for (auto e:adj2[u]){ in[e.v]--; f[e.v]=max (f[e.v],f[u]+e.w); if (!in[e.v]) q.push (e.v); } } } signed main () { cin>>n>>m; for (int i=1 ;i<=m;i++){ int mode,u,v; cin>>mode>>u>>v; if (mode==1 ){ adj[v].push_back ({u,0 }); adj[u].push_back ({v,0 }); } if (mode==2 ){ adj[u].push_back ({v,1 }); } if (mode==3 ){ adj[v].push_back ({u,0 }); } if (mode==4 ){ adj[v].push_back ({u,1 }); } if (mode==5 ){ adj[u].push_back ({v,0 }); } } for (int i=1 ;i<=n;i++){ if (!dfn[i]) tarjan (i); } for (int i=1 ;i<=n;i++){ for (auto e:adj[i]){ if (color[i]!=color[e.v]){ adj2[color[i]].push_back ({color[e.v],e.w}); in[color[e.v]]++; } if (color[i]==color[e.v]&&e.w==1 ){ cout<<-1 ; return 0 ; } } } toposort (); int ans=0 ; for (int i=1 ;i<=dcc;i++){ ans+=f[i]*vdcc[i].size (); } cout<<ans; return 0 ; }