引子
差分约束,是一个建模的思想。也就是把一些代数上的约束关系建模成图论的相关问题。差分约束的一些题目往往对思维上建模能力的要求比较高,而对具体算法的考察却比较低,所以做差分约束的题目,一般建模之后会给人一种敲板子的那种流畅和虐题的感觉哈哈哈哈。
我们从最简单的不等式说起。(不等式的关系就是代数式之间的大小约束关系)
已知
x1−x0≤2
x2−x0≤7
x3−x0≤8
x2−x1≤3
x3−x2≤2
如果要求
x3−x0
的最大值,应该怎么做呢?
前面已经提到,用差分约束的思想可以把一个代数的不等关系抽象成图上的一些问题。我们观察第一个不等式,可以写成:
x1≤x0+2
如果把
x0
等看成某点到某源点的距离,那么这个不等式就代表0号点到1号点有一条长度为2的有向边。最后我们可以得到这样一张图:
然后这道题目就变成了0到3的最短距离了。用图论最短路算法的某一种就能求出来。
这样我们就完成了一个从约束的关系到图论模型的一个转换了。
预备知识
图论最短路算法
差分约束
在上面的引子中我们已经可以总结道:对于每个不等式
x[i]−x[j]≤a[k]
,我们可以从j到i建立一条有向边,权值为
a[k]
x[j]−x[i]
的最大值就是求i到j的最短路。
三角不等式
此部分来源大佬博客
如果还没有完全理解,我们可以先来看一个简单的情况,如下三个不等式:如果还没有完全理解,我们可以先来看一个简单的情况,如下三个不等式:
B−A≤c
C−B≤a
C−A≤b
我们想要知道C - A的最大值,通过(1) + (2),可以得到
C−A≤a+c
,所以这个问题其实就是求
min{b,a+c}
。将上面的三个不等式按照数形结合的方式建图,可以得到:
我们发现
min{b,a+c}
正好对应了A到C的最短路,而这三个不等式就是著名的三角不等式。将三个不等式推广到m个,变量推广到n个,就变成了n个点m条边的最短路问题了。
解的存在性
上文提到最短路的时候,会出现负环或者根本就不可达的情况,所以在不等式组转化的图上也有可能出现上述情况,先来看负权圈的情况,如图下图为5个变量5个不等式转化后的图,需要求得是
X[t]−X[s]
的最大值,可以转化成求s到t的最短路,但是路径中出现负权圈,则表示最短路无限小,即不存在最短路,那么在不等式上的表现即
X[t]−X[s]≤T
中的T无限小,得出的结论就是
X[t]−X[s]
的最大值 不存在。
(看到负环时是不是有用bellman或者spfa的冲动?)
再来看另一种情况,即从起点s无法到达t的情况,如图,表明
X[t]
和
X[s]
之间并没有约束关系,这种情况下
X[t]−X[s]
的最大值是无限大,这就表明了
X[t]
和
X[s]
的取值有无限多种。
在实际问题中这两种情况会让你给出不同的输出。综上所述,差分约束系统的解有三种情况:
1. 有解
2. 无解
3. 无限多解;
最大值 => 最小值
然后,我们将问题进行一个简单的转化,将原先的”
≤
”变成”>=”,转化后的不等式如下:
B - A >= c (1)
C - B >= a (2)
C - A >= b (3)
然后求
C−A
的最小值,类比之前的方法,需要求的其实是
max{b,c+a}
,于是对应的是图上从A到C的最长路。同样可以推广到n个变量m个不等式的情况。
标准化建图
我们发现,很多约束关系并不是标准的 ≤ ,有时候会出现一些等号啊等等。
处理等号
对于等式
A−B=c
等号的处理就是把它拆成两个不等式:
A−B≤c
A−B≥c
处理其他不等号
对于 <> ,在整数中我们可以转化成 ≤≥ 例如我们把 a<b 写成了 a<=b−1 (满足 a、b∈Z )
例题
例题1
狡猾的商人
题面传送门
分析
这道题目的大意就是:给一些账本上的信息,让你判断账本是真是假。如果有第s月到第t月总共收益为w,那么我们可以发现,从第s-1个月末开始,商人拿了w元钱。如果令dis[i]为第i个月末的收入,那么我们有:
dis[s−1]+w=dis[t]
。这是一个再将这个等式标准化,可以得到:
dis[s−1]+w≤dis[t]
和
dis[s−1]+w≥dis[t]
,所以我们可以从s-1到t建一条w长度的边,同时从t到s-1建一条-w长度的边。建图好之后,我们的差分约束就完成了,判断这个图是否符合现实账本的实际的方法就是用跑spfa判断是否存在负环。如果存在负环就与现实矛盾了。(想一想,为什么?)
我们这里的spfa采用dfs版本,只要某一个点在同一条路径上出现多次,我们可以认为它是在负环上了。
code
#include<bits/stdc++.h>
#define maxn 105
#define INF 2e9
using namespace std;
inline int read()
{
int num=0;
bool flag=true;
char c;
for(;c>'9'||c<'0';c=getchar())
if(c=='-')
flag=false;
for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar());
return flag ? num : -num;
}
int w,n,m;
namespace graph
{
struct node
{
int to,val;
};
vector<node>G[maxn];
void init()
{
n=read();
m=read();
for(int i=1;i<=m;i++)
{
int s=read();
int t=read();
int v=read();
G[s-1].push_back((node){t,v});
G[t].push_back((node){s-1,-v});
}
}
}using namespace graph;
//建图
namespace shortest
{
int dis[maxn];
bool vis[maxn],flag;
void spfa(int u)//其实就是dfs
{
vis[u]=true;
for(int i=0;i<G[u].size();i++)
//遍历邻接表
{
node v=G[u][i];
if(dis[v.to]>dis[u]+v.val)
{
if(vis[v.to])//如果这个点在这条路径上已经出现过
{
flag=1;
//存在负环!!
return;
//直接搞掉
}
dis[v.to]=dis[u]+v.val;
//更新最短路
spfa(v.to);
//继续dfs
}
}
vis[u]=0;return;
}
}using namespace shortest;
//最短路
int main()
{
w=read();
for(int i=1;i<=w;i++)
{
init();
for(int i=0;i<=n;i++)
{
dis[i]=0;
spfa(i);
if(flag)break;
}
if(flag)printf("false\n");
else printf("true\n");
for(int i=0;i<=n;i++)
{
vis[i]=0;
dis[i]=0;
G[i].clear();
//每次建图之前都要初始化!
}
flag=false;
}
return 0;
}