前置知识
- 最短路/最长路
- 判负环
形式化描述
给出一个含 m m m 个不等式的不等式组,形如:
{ x i 1 − x j 1 ≤ k 1 x i 2 − x j 2 ≤ k 2 ⋯ x i m − x j m ≤ k m \begin{cases} x_{i_1}-x_{j_1}\leq k_1 \\x_{i_2}-x_{j_2} \leq k_2 \\ \cdots\\ x_{i_m} - x_{j_m}\leq k_m\end{cases} ⎩ ⎨ ⎧xi1−xj1≤k1xi2−xj2≤k2⋯xim−xjm≤km
求出任意一组解 $ a_1=x_1,a_2=x_2,… a_n=x_n $,使得这组解满足这个不等式组。
讲解
左边所有的式子保持减法,和差分相似;一个不等式,称为一个约束条件。因此,这类题目我们称之为差分约束。
乍一看,好像只能使用暴力来解决这道题(通过不断枚举 x i x_i xi 来找到正确的解),但很显然,暴力的复杂度太高了,当 y y y 变大的时候,暴力所需要的时间就越多。
既然题目不符合暴力,二分,dp等算法或思想的要求,我们来尝试使用图论解决它。
使用图论的前提,就是需要建图。我们观察这些式子并与一些图论相关的算法相比较,显然我们能够发现最短路算法中的松弛条件 $ if(dis[v]>dis[u]+w) $ 与该不等式极其相似。我们将松弛条件变化一下,得到:
d i s [ v ] ≤ d i s [ u ] + w dis[v] \leq dis[u] + w dis[v]≤dis[u]+w
在最短路中,它的正确性是毋庸置疑的。
将不等式组中的式子变化,得到:
x i ≤ x j + k x_i \leq x_j + k xi≤xj+k
此时, i i i 对应 v v v, j j j 对应 u u u, k k k 对应 w w w。从 j j j 向 i i i 建一条长度为 k k k 的边,就完成了我们的建图。至此,我们成功的将差分约束问题转化成了最短路问题。
(真佩服想到这个的人)
接下来就是跑最短路,如果存在负环,说明这个不等式组是无解的。否则, d i s dis dis 数组中的值就是一组解。
一些说明
-
注意到,如果 { a 1 , a 2 , … , a n } \{a_1,a_2,\dots,a_n\} {a1,a2,…,an} 是该差分约束系统的一组解,那么对于任意的常数 d, { a 1 + d , a 2 + d , … , a n + d } \{a_1+d,a_2+d,\dots,a_n+d\} {a1+d,a2+d,…,an+d} 显然也是该差分约束系统的一组解,因为这样做差后 d 刚好被消掉。
(oiwiki讲的很清楚,就直接贴上去了)
-
如果不等式全是 ≥ \geq ≥ 号(求最小距离),此时需要跑最长路;如果不等式全是 ≤ \leq ≤ 号(求最大距离),则需要跑最短路。
我们可以从公式的角度来理解:满足 ≤ \leq ≤ 的公式与 d i s [ u ] ≤ d i s [ v ] + w dis[u]\leq dis[v]+w dis[u]≤dis[v]+w 相似,而 d i s [ u ] ≤ d i s [ v ] + w dis[u]\leq dis[v]+w dis[u]≤dis[v]+w 这个式子对应的是最短路。反之,满足 ≥ \geq ≥ 的公式与 d i s [ u ] ≥ d i s [ v ] + w dis[u]\geq dis[v]+w dis[u]≥dis[v]+w 相似,而 d i s [ u ] ≥ d i s [ v ] + w dis[u]\geq dis[v]+w dis[u]≥dis[v]+w 这个式子对应的是最长路。
-
求最远距离用最短路,求最近距离用最长路。
这个点我们可以
感性理解一下:求最长距离,数学公式中为 $ dis \leq x $ ,此时是小于号,即最长距离对应的是最短路。同理我们也可以得出最短距离对应的是最短路。 -
判负环需的前提是整张图联通,因此我们需要建立一个超级源点保证我们能走到图上的每一个点。
超级源点:到图上的每一点距离为0
总结
差分约束基本步骤:
统一符号(大于号或者小于号) -> 建图 -> 跑 SPFA 求出通解。
这里需要注意有些题可能需要先判断图是否联通,所以可能需要跑两次 SPFA。
总的来说,只要理解了原理,做起题目来就和做入门题没什么区别。
模板见下。
例题
这题就是把差分约束的最基础的东西抽象出来了。根据不等式建图跑 SPFA 即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6+10;
const int inf = 1e9+7;
struct edge {
int from,to,next,w;
}e[N];
int n,m,k,l,r,ans,cnt;
int head[N],dis[N],vis[N],ti[N]; //ti:记录每个点被经过的次数
void add(int u,int v,int w) {
e[++cnt].next=head[u];
head[u]=cnt;
e[cnt].from=u; e[cnt].to=v; e[cnt].w=w;
}
bool spfa(int s) {
for(int i=0;i<=n;++i) dis[i]=inf;
dis[s]=0; vis[s]=1;
queue<int> q;
q.push(s);
while(!q.empty()) {
int u=q.front(); q.pop();
vis[u]=0;
for(int i=head[u];i;i=e[i].next) {
int v=e[i].to; //cout<<"u= "<<u<<" v= "<<v<<"\n";
if(dis[v]>dis[u]+e[i].w) {
dis[v]=dis[u]+e[i].w;
if(!vis[v]) {
ti[v]++;
if(ti[v]>=n+1) return 0;
q.push(v); vis[v]=1;
}
}
}
}
return 1;
}
signed main()
{
ios::sync_with_stdio(0);
//memset(head,-1,sizeof(head));
cin>>n>>m;
for(int i=1;i<=n;++i) add(n+1,i,0);
for(int i=1,u,v,w;i<=m;++i) {
cin>>u>>v>>w;
add(v,u,w);
}
int ret=spfa(n+1);
if(ret) {
for(int i=1;i<=n;++i) cout<<dis[i]<<" ";
}
else cout<<"NO\n";
return 0;
}
这题需要注意隐含条件:
- 开始时,奶牛们按照编号顺序来排队(暗示 i<j)
- 可能有多头奶牛在同一位置上(暗示需要给所有相邻的牛建立一条从 i i i 到 i − 1 i-1 i−1,长度为 0 0 0 的边)
- 这题需要跑 2 2 2 次 SPFA。(第一次确认是否存在可行解,从超级源点开始跑;第二次确认是否能从 1 1 1 到达 n n n,从 1 1 1 开始跑)
这题需要注意:当农场 a 的作物与农场 b 的作物数量一致时,需要在 ab 之间建立双向边。