初探差分约束系统
1. 概述
前置知识:单源最短路径,负环。
差分约束系统是一种特殊的
差分约束系统实际是应用了最短路算法的松弛的思想,是一类最短路模型。借鉴其思想可以解决很多性质相类似的题目。
2. 算法思想
观察每个约束条件
于是可以把
但是,如果以这些变量中的任意一个作为起点,这会使得该变量被钦定为
我们不妨求解一组非正数解,即
同时注意到对于一组合法的解
此时以
考虑以下约束条件:
把图建出来以后,很明显是个负环。
根据不等式的可加性,
使用 SPFA 即可判负环并求解
关于判负环的方法,详情请看P3385 【模板】负环 题解,这里不再赘述。
3. 参考代码
模板题入口。本代码需要 C++11。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
#include<cstring>
#include<climits>
using namespace std;
const int MAXN=5e3+5;
int n,m;
struct node
{
int to,v;
};
vector<node> edge[MAXN];
int dis[MAXN],cnt[MAXN];
bool visit[MAXN];
bool spfa(void)//模板 spfa
{
for(int i=1;i<=n;i++) dis[i]=INT_MAX;
dis[0]=0;
queue<int> q;
q.push(0);
visit[0]=true;
while(!q.empty())
{
const int u=q.front();
q.pop();
visit[u]=false;
for(auto it:edge[u])
{
const int v=it.to,w=it.v;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if(!visit[v])
{
visit[v]=true;
cnt[v]++;
if(cnt[v]>=n+1) return false;//存在负环
q.push(v);
}
}
}
}
return true;
}
int main()
{
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,w;cin>>u>>v>>w;
edge[v].push_back(node{u,w});//根据约束条件来建图
}
for(int i=1;i<=n;i++)//新建0号节点并建图
edge[0].push_back(node{i,0});
if(!spfa())
{
cout<<"NO"<<endl;
return 0;
}
for(int i=1;i<=n;i++) cout<<dis[i]<<endl;
return 0;
}
4. 扩展
然而很多时候题目给定的约束条件不会这么裸,经常有变形。我们只要通过某些手段,将其变形为等价的不等式求解即可。下面是一些常见的变形。
-
x_i-x_j\ge c_k
等价于
-
x_i=x_j
等价于
-
x_i>x_j(x_i,x_j\in\mathbb{Z})
等价于
-
\dfrac{x_i}{x_j}\le c_k(x_i,x_j,c_k>0)
一种常用的化乘为加的 trick:用
不少题目的约束条件还需要我们根据题意构造得到,难度也就更大些。还要尤其注意部分题目的隐含条件。
5. 例题
广告:差分约束基础应用题单。
差分约束系统的应用通常分为三种:求不等式组解的最大值,最小值,判断有无解。
对于求最大值,我们有
而求最小值,约束条件就都变形为 dis=-INF,松弛取max。
判断有无解即为判断负环,最长路对应正环。方法也都是看入队次数。
5.1 P3275 [SCOI2011]糖果
尽管本题正解不是差分约束,但是可以当做差分约束基础题来做,只需要基本的不等式变形技巧。
对于取不到等号的,比如
不过在这里我们变形为大于等于号,跑最长路。因为题目要求求出最小值。
然后一个隐含条件是每个小朋友分到的糖果数必须是正整数,因此我们钦定
答案即为
不过朴素的 SPFA 会 TLE。但在本题一种奇怪的优化方案是
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<deque>
#include<cstring>
#include<numeric>
#include<climits>
using namespace std;
const int MAXN=1e5+5;
int n,m;
struct node
{
int to,v;
};
vector<node> edge[MAXN];
int dis[MAXN],cnt[MAXN];
bool visit[MAXN];
bool spfa(void)
{
for(int i=1;i<=n;i++) dis[i]=INT_MIN;
dis[0]=0;
deque<int> q;//貌似比 queue 快一些。当然可以手写。
q.push_front(0);
visit[0]=true;
while(!q.empty())
{
const int u=q.front();
q.pop_front();
visit[u]=false;
for(auto it:edge[u])
{
const int v=it.to,w=it.v;
if(dis[v]<dis[u]+w)
{
dis[v]=dis[u]+w;
if(!visit[v])
{
visit[v]=true;
cnt[v]++;
if(cnt[v]>=n+1) return false;
q.push_front(v);
}
}
}
}
return true;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,a,b;cin>>x>>a>>b;
if(x==1)
{
edge[a].push_back(node{b,0});
edge[b].push_back(node{a,0});
}
if(x==2)
edge[a].push_back(node{b,1});
if(x==3)
edge[b].push_back(node{a,0});
if(x==4)
edge[b].push_back(node{a,1});
if(x==5)
edge[a].push_back(node{b,0});
}
for(int i=n;i>=1;i--)//奇怪的优化
edge[0].push_back(node{i,1});
if(!spfa())
{
cout<<-1<<endl;
return 0;
}
cout<<accumulate(dis+1,dis+1+n,0LL)<<endl;//注意答案可能会爆 int
return 0;
}
5.2 P2294 [HNOI2005]狡猾的商人
本题需要自己构造约束条件,但是思维难度也比较简单。
显然,判断账单真假就是问存不存在一组
于是约束条件就转化为
剩下的就是差分约束模板了。判负环即可。
另外,由于题目只要求我们判负环,因此我们可以不新建超级源点,只将所有连通图遍历一遍。对于入队次数为
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
const int MAXN=3e5+5;
int n,m,st;
struct node
{
int to,val;
};
vector<node> edge[MAXN];
bool visit[MAXN];
int dis[MAXN],cnt[MAXN];
void clear(void)//别忘了多测要清空
{
memset(dis,0x3f,sizeof(dis));
memset(visit,false,sizeof(visit));
memset(cnt,0,sizeof(cnt));
for(int i=0;i<=n*2;i++) edge[i].clear();
}
bool solve(int s)
{
queue<int> q;q.push(s);
dis[s]=0;visit[s]=true;
while(!q.empty())
{
const int u=q.front();
q.pop();
visit[u]=false;
for(auto it:edge[u])
{
const int v=it.to,w=it.val;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if(!visit[v])
{
visit[v]=true;
q.push(v);
cnt[v]++;
if(cnt[v]>=n+1)//判负环
return false;
}
}
}
}
return true;
}
int main()
{
ios::sync_with_stdio(false);
int T;cin>>T;
while(T--)
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,w;cin>>u>>v>>w;
edge[v].push_back(node{u-1,-w});
edge[u-1].push_back(node{v,w});
}
bool f=true;
for(int i=0;i<=n && f;i++)//对于每个连通图都判一遍
if(!cnt[i] && !solve(i))
f=false;
puts(!f?"false":"true");
clear();
}
return 0;
}
6. 参考资料
- 李煜东《算法竞赛进阶指南》0x65 节。
- OI-wiki。
- ix35 的博客文章。