写在前面
在我看来,差分约束系统就是利用图论来直观的解决多种逻辑关系叠加的方法。类似的方法还有2-SAT问题(我只学过这两种),你可以发现这两种问题的共同点都是极其繁多的约束条件,而且彼此之间交互联系,错综复杂,如果采用线性的思维一个一个去递进解决的话花费的时间以及精力是巨大的。因此我们迫切的需要解决这类问题的方法,于是差分约束系统就诞生了。。。
什么是差分约束系统
wiki定义
差分约束系统(System of Difference Constraints),是求解关于一组变数的特殊不等式组之方法。
如果一个系统由n个变量和m个约束条件组成,其中每个约束条件形如 x i − x j ≤ b k ( i , j ∈ [ 1 , n ] , k ∈ [ 1 , m ] ) x_{i} - x_{j} \leq b_{k} (i,j \in [1,n] , k \in [1,m]) xi−xj≤bk(i,j∈[1,n],k∈[1,m]) 则称其为差分约束系统。亦即,差分约束系统是求解关于一组变量的特殊不等式组的方法。
我所遇到的关于差分约束系统的ACM题目就只有一个特点,那就是非常多的不等式,而你要在满足这些不等式的情况下求得题目要求的最优解(一般是最小最大值的求解)。
而在实际问题中,我们是把xi,xj这些值当作图论中的一个个的点,每一个不等式条件作为一条边联系两个点,最后用最长路或最短路求解。
这里可能会有两个问题(关于原理在后面会有)
- 不是
≤
o
r
≥
\leq or\geq
≤or≥形式的不等式, 而是
<
o
r
>
\lt or \gt
<or> 形式的怎么办?
这种况下往往可以对右侧的数k,+1or-1,使之满足等号的条件即可(差分约束系统一般k都是不等式,浮点数不知道能不能解决,你可以根据情况来选择+or-的大小) - 如果不等式很复杂,右边不只两个x怎么办?
在实际题目中,我们往往是需要自己对题目给的条件做处理的,所以要自己去化简条件,以此来迎合差分约束系统的条件,如果做不到的话,只能换一种算法了,差分约束系统只能解决形如 x i − x j ≤ b k ( i , j ∈ [ 1 , n ] , k ∈ [ 1 , m ] ) x_{i} - x_{j} \leq b_{k} (i,j \in [1,n] , k \in [1,m]) xi−xj≤bk(i,j∈[1,n],k∈[1,m])类的条件组成的问题
原理、第一个要点
-
为什么差分约束系统成立?
差分约束系统的成立,是基于最长路最短路算法中应用的三角不等式,恰好也符合不等式简化的特点。
对于 x i − x j ≤ k 1 x_{i} - x_{j} \leq k_{1} xi−xj≤k1与 x i − x j ≤ k 2 x_{i} - x{j} \leq k_{2} xi−xj≤k2由不等式我们可以推出两条件同时满足的情况为 x i − x j ≤ k 2 ( k 1 ≥ k 2 ) x_{i} - x_{j} \leq k_{2} (k_{1} \geq k_{2}) xi−xj≤k2(k1≥k2)你会发现这个推导过程与最短路的松弛过程是一样的。那么就很好理解我们若是将每个x设为图上一个点,每一条不等式关系作为一条边,k作为边的权值,在这个图上跑一遍最短路的过程,所有的不等式就自动符合并且达成最佳情况了。(整个最短路就算法就等同于一个多条不等式的求最优解)。因此我们说,用最长路与最短路算法求解不等式组是可行的。 -
在实际问题中如何判断使用最短路还是最长路算法?
简单的记忆,就是不等式取 ≤ \leq ≤就是最短路,取 ≥ \geq ≥就是最长路。那么如何去理解其中的原因呢?-
从不等式的角度出发,小于号取小,大于号取大,那么在松弛的过程中恰好对应着遇到两种路径时取短路径与长路径两种情况,也就对应了最短路与最长路两种算法。
-
时刻注意,差分约束系统就是利用最短/最长路的松弛操作来模拟不等式合并,所以哪种算法的 松弛过程符合题目要求,就选择哪种算法。
-
同一个题目中总是会出现多个不等式,很多时候不等式的方向(大于等于or小于等于)不一致,这时我们只需要乘以负号,就可以使方向一致。
-
差分约束系统中常遇到的问题和解决技巧
如何建立不等式关系
这一条应该就是差分约束系统的最难点。 例题1:POJ-1275
题意大概是
一天24小时,从0点到23点,告诉你每个时间段i(一个小时)至少要有 R i R_{i} Ri个人值班,有N个收银员,告诉你每个收银员上班的时间,每个收银员可以值班8个小时,问最少要安排几个收银员,才能满足条件。
这道题二分答案,利用差分约束系统检验。
-
为什么二分可行?
因为答案符合单调性。 -
如何利用差分约束系统?
先统计一下每个时间段开始上班的人数 x 1 − n x_{1-n} x1−n,每个时间段要求的值班人数 n e e d 1 − n need_{1-n} need1−n,sum为当前要验证的答案(雇佣的收银员最小人数)。为了使得i-1任何时候都不会数组越界,我把所有时间段都前移了一位。
设 S i S_{i} Si为前i个小时上班的人数,自然有 S i − S i − 1 ≤ x i , S i − S i − 1 ≥ 0 S_{i}-S_{i-1} \leq x_{i}, S_{i} - S_{i-1} \geq 0 Si−Si−1≤xi,Si−Si−1≥0。
为满足条件,又有 S i − S i − 8 ≥ n e e d i S_{i} - S_{i-8} \geq need_{i} Si−Si−8≥needi(i>8) , S i − S i + 16 ≥ n e e d i − s u m S_{i} - S_{i+16} \geq need_{i}-sum Si−Si+16≥needi−sum (i<=9) (只要在i时间段及前7个时间段上班的人都会值班到i时刻)
最后有 S 24 − S 0 ≥ s u m S_{24} - S_{0} \geq sum S24−S0≥sum (保证总雇佣的人数大于等于sum,同时也让整个图形成环)
建完图跑一遍最长路即可。
//注意我每一个时间段都加了1,我的时间段是1-24而不是0-23
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int N = 3e5+100;
const int inf = 0x3f3f3f;
int need[30],st[30],d[30],vis[30],cnt[30];
struct Edge
{
int to,w,next;
}e[2100];
int head[30],tot;
queue<int>q;
void add(int u,int v,int w)
{
e[tot].to = v;
e[tot].w = w;
e[tot].next = head[u];
head[u] = tot++;
}
void build(int sum) //按要求建图
{
tot=0;
memset(head,-1,sizeof(head));
for(int i=1;i<=24;i++)
add(i-1,i,0),add(i,i-1,-st[i]);
for(int i=1;i<=8;i++)
add(16+i,i,need[i]-sum);
for(int i=9;i<=24;i++)
add(i-8,i,need[i]);
add(0,24,sum);
}
bool spfa(int sum)
{
while(!q.empty())
q.pop();
for(int i=0;i<=25;i++)
d[i]=-inf,vis[i]=0;
q.push(0);
d[0]=0;
vis[0]=1;
int ct = 0;
while(!q.empty())
{
int f = q.front();
q.pop();
vis[f] = 0;
for(int i=head[f];i!=-1;i=e[i].next)
{
int v = e[i].to;
int w = e[i].w;
if(d[v]<d[f]+w)
{
ct++;
d[v] = d[f]+w;
if(!vis[v])
vis[v]=1,q.push(v);
}
}
if(ct>1000)return false; //需要判断一下成环
}
if(d[24]==sum)return true; //刚好满足才算符合条件
return false;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(st,0,sizeof(st));
for(int i=1;i<=24;i++)
scanf("%d",&need[i]);
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
++st[x+1];
}
int l = 0, r = n+1,fi=n+1;
while(l<r) //二分
{
int mid = (r+l)/2;
build(mid);
if(spfa(mid))r = mid, fi = mid;
else l = mid+1;
}
if(fi==n+1)
printf("No Solution\n");
else
printf("%d\n",fi);
}
return 0;
}
超级原点
在实际的题目中,往往给出的限制条件即使加上隐藏条件也不足以将整个图连成连通图。这时就要求建立一个超级原点,要么使得图中所有点与之相连,要么使之作为中间点,连接图的首尾(如例1中的 S 0 S_{0} S0)。
例题2:POJ-2983
大概题意:
两种信息:
P A B X 代表A在B北边X个单位长度
V A B 代表A在B北边至少一光年,具体位置未知。
给出你M个信息,问你所有的信息是否有冲突。
这种情况就比较明显,V信息就是常规不等式,P操作同时建立两个方向相反的不等式就可以达到设定确定长度的目的。另外一点就是要设立一个超级原点0,把所有的点都连一条权值为0的边到0号点,表示所有点在0点北侧至少0单位长度,同时还可以保证整个图一定联通。
参考代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long LL;
const int N = 1e5+100;
const int inf = 0x3f3f3f;
struct Edge
{
int to,w;
LL next;
Edge(){}
Edge(int a,LL b,int c):to(a),w(b),next(c){}
}e[6*N];
int head[2*N],vis[2*N],tot,cnt[2*N];
LL ds[2*N];
void add(int u,int v,int w)
{
e[tot] = Edge(v,w,head[u]);
head[u] = tot++;
}
bool spfa(int n)
{
deque<int>q;
while(!q.empty())
q.pop_back();
memset(vis,0,sizeof(vis));
memset(ds,-inf,sizeof(ds));
memset(cnt,0,sizeof(cnt));
q.push_front(0);
vis[0]=1;
ds[0]=0;
cnt[0]=1;
while(!q.empty())
{
int cur = q.front();
q.pop_front();
vis[cur]=0;
for(int i=head[cur];i!=-1;i=e[i].next)
{
int v = e[i].to;
int w = e[i].w;
if(ds[v]<ds[cur]+w)
{
ds[v] = ds[cur]+w;
if(!vis[v])
{
cnt[v]++;
vis[v]=1;
if(!q.empty()&&ds[v]<ds[q.front()])
q.push_back(v);
else
q.push_front(v);
if(cnt[v]>n)return false; //判断是否成环,成环即矛盾。
}
}
}
}
return true;
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m))
{
tot = 0;
memset(head,-1,sizeof(head));
char c[5];
int u,v;
LL w;
while(m--)
{
scanf("%s",c);
if(c[0]=='P')
{
scanf("%d%d%lld",&u,&v,&w);
add(v,u,w);
add(u,v,-w);
}
else
{
scanf("%d%d",&u,&v);
add(v,u,1);
}
}
for(int i=1;i<=n;i++) //每个点都连向超级原点0
add(0,i,0);
if(spfa(n))
printf("Reliable\n");
else
printf("Unreliable\n");
}
return 0;
}
其他一些例题
POJ-1201
参考代码:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <map>
#include <cstdlib>
#include <queue>
#include <set>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 5e5+100;
const int inf = 0x3f3f3f;
struct edge
{
int to,w,next;
}E[2*N];
int tot,head[N],mx,mn,dis[N],vis[N];
void add(int u,int v,int w)
{
E[tot].to=v;
E[tot].w=w;
E[tot].next = head[u];
head[u]=tot++;
}
queue<int>q;
int spfa()
{
memset(vis,0,sizeof(vis));
while(!q.empty())
q.pop();
fill(dis+mn+1,dis+mx+1,-inf);
dis[mn]=0;
q.push(mn);
vis[mn]=1;
while(!q.empty())
{
int cur = q.front();
q.pop();
vis[cur]=0;
for(int i=head[cur];i!=-1;i=E[i].next)
{
int v = E[i].to,w=E[i].w;
if(dis[v]<dis[cur]+w)
{
dis[v] = dis[cur]+w;
if(!vis[v])vis[v]=1,q.push(v);
}
}
}
return dis[mx];
}
int main()
{
memset(head,-1,sizeof(head));
int n;
mx = -inf,mn = inf,tot=0;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v+1,w);
mx = max(mx,v);mn = min(mn,u);
}
mx++;
for(int i=mn;i<mx;i++)
{
add(i,i+1,0);
add(i+1,i,-1);
}
printf("%d\n",spfa());
return 0;
}
HDU-3440
参考代码:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <map>
#include <cstdlib>
#include <queue>
#include <set>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 1100;
const int inf = 1<<30;
int tot=0,head[N],cnt[N];
inline int Abs(int a)
{
if(a<0)return -a;
else return a;
}
struct Edge
{
int to,w,next;
}e[4*N];
struct node
{
int id,nu;
}no[N];
void add(int u,int v ,int w)
{
e[tot].to = v;
e[tot].w = w;
e[tot].next = head[u];
head[u] = tot++;
}
bool cmp(node a,node b)
{
return a.nu < b.nu;
}
int ds[N],vis[N];
int spfa(int st,int en,int n)
{
memset(vis,0,sizeof(vis));
for(int i=0;i<=n;i++)
ds[i]=inf;
queue<int>q;
while(!q.empty())
q.pop();
q.push(st);
vis[st]=1;
ds[st]=0;
while(!q.empty())
{
//cout << 1 << endl;
int cur = q.front();
q.pop();
vis[cur]=0;
for(int i=head[cur];i!=-1;i=e[i].next)
{
int v = e[i].to;
int w = e[i].w;
if(ds[v]>ds[cur]+w)
{
ds[v] = ds[cur] + w;
if(!vis[v])
{
cnt[v]++;
vis[v] = 1 , q.push(v);
}
}
}
}
return ds[en];
}
int main()
{
int T;
scanf("%d",&T);
int cntt=0;
while(T--)
{
int flag=1;
tot=0;
memset(head,-1,sizeof(head));
++cntt;
int n,d;
scanf("%d%d",&n,&d);
for(int i=0;i<n;i++)
{
scanf("%d",&no[i].nu);
no[i].id = i+1;
}
for(int i=0;i<n-1;i++)
{
add(no[i+1].id,no[i].id,-1);
}
sort(no,no+n,cmp);
for(int i=0;i<n-1;i++)
{
if(Abs(no[i].id-no[i+1].id)>d)
{
flag = 0;
printf("Case %d: -1\n",cntt);
break;
}
if(no[i].id<no[i+1].id)
add(no[i].id,no[i+1].id,d);
else
add(no[i+1].id,no[i].id,d);
}
if(flag)
{
int st = no[0].id, en = no[n-1].id;
if(st>en)swap(st,en);
int fi = spfa(st,en,n);
printf("Case %d: %d\n",cntt,fi);
}
}
}
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <map>
#include <cstdlib>
#include <queue>
#include <set>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 1e6+110;
const int inf = 0x3f3f3f3f;
const int cinf = 1044266559;
int head[N],tot=0;
struct Edge
{
int to,next;
int w;
Edge(){}
Edge(int a,int b,int c):to(a),w(b),next(c){}
}e[4*N];
void add(int u,int v,int w)
{
e[tot] = Edge(v,w,head[u]);
head[u]=tot++;
}
int vis[N],ds[N],cnt[N];
LL spfa(int n)
{
queue<int>q;
while(!q.empty())
q.pop();
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
memset(ds,inf,sizeof(ds));
q.push(1);
vis[1]=1;
ds[1]=0;
while(!q.empty())
{
int cur = q.front();
q.pop();
vis[cur] = 0;
cnt[cur]++;
if(cnt[cur]>n)return -1;
for(int i=head[cur];i!=-1;i=e[i].next)
{
int v = e[i].to;
int w = e[i].w;
if(ds[v]>ds[cur]+w)
{
ds[v] = ds[cur] + w;
if(!vis[v])
vis[v]=1,q.push(v);
}
}
}
if(ds[n]!=inf)return ds[n];
else return -2;
}
int main()
{
tot = 0;
memset(head,-1,sizeof(head));
int n,ml,md;
scanf("%d%d%d",&n,&ml,&md);
for(int i=0;i<ml;i++)
{
int u,v;
int w;
scanf("%d%d%d",&u,&v,&w);
if(u>v)swap(u,v);
add(u,v,w);
}
for(int i=0;i<md;i++)
{
int u,v;
int w;
scanf("%d%d%d",&u,&v,&w);
if(u<v)swap(u,v);
add(u,v,-w);
}
LL fi = spfa(n);
printf("%d\n",fi);
return 0;
}
POJ-1364
参考代码:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <map>
#include <cstdlib>
#include <queue>
#include <set>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 1e3+110;
const int inf = 1<<29;
struct Edge
{
int to,w,next;
Edge(){}
Edge(int a,int b,int c):to(a),w(b),next(c){}
}e[4*N];
int head[N],vis[N],tot;
void add(int u,int v,int w)
{
e[tot] = Edge(v,w,head[u]);
head[u] = tot++;
}
int ds[N],cnt[N];
bool spfa(int n)
{
queue<int>q;
while(!q.empty())
q.pop();
for(int i=0;i<=n+1;i++)
{
vis[i] = 0,
ds[i] = inf,
cnt[i] = 0;
}
q.push(n+1);
ds[n+1] = 0;
vis[n+1]=1;
while(!q.empty())
{
int cur = q.front();
q.pop();
vis[cur] = 0;
cnt[cur]++;
if(cnt[cur]>n+1)
return false;
for(int i=head[cur];i!=-1;i=e[i].next)
{
int v = e[i].to,
w = e[i].w;
if(ds[v]>ds[cur]+w)
{
ds[v] = ds[cur] + w;
if(!vis[v])
{
vis[v] = 1;
q.push(v);
}
}
}
}
return true;
}
int main()
{
int n,m;
while(scanf("%d",&n)&&n)
{
scanf("%d",&m);
memset(head,-1,sizeof(head));
tot=0;
while(m--)
{
int u,v,w;
char c[5];
scanf("%d%d%s%d",&u,&v,c,&w);
if(c[0]=='g')
{
add(u+v,u-1,-w-1);
}
else
{
add(u-1,u+v,w-1);
}
}
for(int i=1;i<=n;i++)
add(n+1,i,0);
if(spfa(n))
printf("lamentable kingdom\n");
else
printf("successful conspiracy\n");
}
return 0;
}