例题——题目传送门:
题目大意,以及题目分析在后面。
-关于差分约束系统-
差分约束系统充满了玄妙之处:它在一些特定的场合下,能够把许多的限制条件转化在一起,形成一个系统类问题,极大地方便了问题的求解;另一方面,它的运用是一大难点,我们需要对原问题进行分析,建模,建立出合适的模型之后,才能使用差分约束进行操作。因此,如何针对于原问题进行合适的模型构建,是差分约束系统的核心问题。
对于这样的问题,差分约束系统能够比较好地解决:给你许多个二元不等式条件,例如 ai−bi≥ki ,或者是 xi+yi≤ki 这样的式子,然后要求出满足这些条件的 n,并且要使 n 最大或最小。这个不等式的的一边,一般是未知(待求)的两个状态(或者两个待求的数值),而另一边是一个定值(通常是已知值)。这种问题通常可转化成最短路(对应的求最大值)或者最长路(对应的求最小值)求解。
那么对于这些不等式组,为什么它们能被转化为最短路/最长路来做呢?
我们发现,形如
ai+bi≥k1bi+ci≥k2有ci−ai≥(k2−k1)
而关于最短路的松弛操作时的disu+length≤disv经转化变形后得disv−disu≥length两者有非常大的共同点:左边都是由两个不定状态构成,右边为一定值。并且,它们的符号使用也基本一致。因此,我们是否可以有“类比”的想法,将原问题转化为最短路/最长路问题,然后再来求解呢?
事实上,根据单源最短路的“三角形不等式”,这样的操作可以被证明是正确的。
因此,建图的操作,也可以类比求最短路时的方法,从 u 到 v 建边(根据上式就是从 ai 到 ci 建边)。
但是在实际操作中,由于问题既含有求最大值,也含有求最小值,这时候又该怎么操作?
如果是求最大值,那么就跑最短路;如果是求最小值,那么就跑最长路。跑最短路时,先把所有的 dis 设为 +INF;而跑最长路时,先把所有的 dis 设为 -INF。注意,最短路要判断负环,而最长路要判断正环。
两者的操作方法基本一致,只是在符号上有区别。因此在建图的时候,建边的方向会有变化。
最后还有一个问题:我看见有些代码中,有一个关于建立超级源点的操作,而有些代码又没有。这是为什么呢?
我们以条件已得到转化,并且是建好了的图作为基础。
有些题目中明确说明了,整个图是连通的,此时我们就不需要建立超级源点,因为我们可以从起点遍历整个图;但有些题目中,没有明确说明图的连通性,我们需要把所有的连通块联合在一起,这样才能使得所有的约束条件被满足,否则就会出现“只满足了一部分约束条件”的情况。
以上就是本人对于差分约束系统的一些思考。
下面开始上题。
1. 【POJ 1364】King
题目传送门:【POJ 1364】
题目大意: 在一个王国,有一个王后,那个王后正在期待一个婴儿。王后祈祷:“如果我的孩子是一个儿子,只有他是一个健全的国王”,9个月后,她的孩子出生,确实生下了一个漂亮的儿子。
不幸的是,正如以前在皇室中发生的那样,儿子有点迟钝。经过多年的学习,他只能添加整数,并比较结果是否大于或小于给定的整数。此外,这些数字必须以序列的形式写出来,并且只能顺序排列连续的子序列。
老国王对他的儿子非常不满。但他准备做一切事情,让他的儿子在死后统治王国。他想考察儿子的能力,于是下令:新的国王需要作出决定的每一个问题都必须以一个有限的整数序列的形式呈现,并且通过说明有关于该序列的总和的一些约束条件来完成。这样,至少有一些希望,他的儿子能做出一些判断。
老国王死后,年轻的国王开始统治。但很快,很多人对他的决定感到非常不满,并决定废掉他。他们试图证明他的判断是错误的。
因此,一些阴谋者向年轻的国王提出了他必须判断的一系列问题。一组问题以序列S = {a1,a2,…,an}的子序列Si = {aSi,aSi + 1,…,aSi + ni}的形式呈现。国王想了一会儿,然后决定,即他设定每个子序列Si的和aSi + aSi + 1 + … + aSi + ni为整数约束ki(即aSi + aSi + 1 + … + aSi + ni < ki或aSi + aSi + 1 + … + aSi + ni> ki),并将这些约束声明为他的判断。
过了一会儿,他意识到他的一些判断是错误的。他不能撤销宣布的限制条件,但试图拯救自己,他决定“假冒”那些被给予的序列。他命令他的顾问找到这样的一个顺序S,以满足他设定的限制。帮助国王的顾问,并写一个程序,决定这样的序列是否存在。
一句话: 多组数据。每组数据要求找到一个序列 S 满足所有给定的约束条件。如果存在,输出“lamentable kingdom”;否则输出“successful conspiracy”。
(1 ≤ n ≤ 100,1 ≤ m ≤ 100,n 表示序列总长,m 表示约束条件的个数)
题目分析:
只要把题读懂了就好做。就是一道差分约束的模板题。把所有的限制条件转化建图即可。对于每一个 gt,从 a+b 到 a-1 建一条权值为 -k-1 的边;对于每一个 lt,从 a-1 到 a+b 建一条权值为 k-1 的边。注意,由于 0-1= -1 会使得数组越界,所以在实际操作中,最好将所有点的编号都加上 1。
下面附上代码:
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- #include<queue>
- using namespace std;
- const int MX=8005;
- struct Edge{
- int to,len,next;
- }edge[MX*2];
- int n,m,head[MX],now=0,dis[MX],cnt[MX];
- bool inq[MX];
- queue<int> q;
- inline void adde(int u,int v,int len){
- edge[++now].to=v;
- edge[now].len=len;
- edge[now].next=head[u];
- head[u]=now;
- }
- bool spfa(int s){ //跑最短路
- q.push(s);
- inq[s]=true;
- dis[s]=0;
- while (!q.empty()){
- int u=q.front();
- q.pop();
- inq[u]=false;
- for (int i=head[u];i;i=edge[i].next){
- int v=edge[i].to;
- if (dis[u]+edge[i].len<dis[v]){
- dis[v]=dis[u]+edge[i].len;
- if (!inq[v]){
- q.push(v);
- inq[v]=true;
- if (++cnt[v]>n+2){ //记得在这里判环
- return false;
- }
- }
- }
- }
- }
- return true;
- }
- void _init(){
- while (!q.empty()) q.pop();
- memset(inq,0,sizeof(inq));
- memset(dis,0x3f,sizeof(dis));
- memset(edge,0,sizeof(edge));
- memset(head,0,sizeof(head));
- memset(cnt,0,sizeof(cnt));
- now=0;
- }
- int main(){
- int a,b,k;
- char opt[3];
- while (scanf(“%d”,&n)){
- if (n==0) break;
- memset(dis,0x3f,sizeof(dis));
- scanf(”%d”,&m);
- for (int i=1;i<=m;i++){
- scanf(”%d%d”,&a,&b);
- getchar();
- scanf(”%s%d”,opt,&k);
- if (opt[0]==‘g’){ //比k大
- adde(a+b+1,a-1+1,-k-1); //为什么这里全都要 +1?因为,如果不这样,就会出现负数,会带来越界问题
- }
- if (opt[0]==‘l’){ //比k小
- adde(a-1+1,a+b+1,k-1);
- }
- }
- for (int i=1;i<=n+1;i++){ //这里要建立超级源点,然后从 0 开始跑最短路
- adde(0,i,0);
- }
- if (spfa(0)) puts(“lamentable kingdom”);
- else puts(“successful conspiracy”);
- _init();
- }
- return 0;
- }
2.【POJ 3159】Candies
题目传送门:【POJ 3159】
题目大意:有n个人编号为1至n,m个约束条件,每条要求为ui,vi,wi。代表编号ui的人分到的糖果最多只能比编号为vi的人少wi个。要求求出编号为n的人最多能比编号为1的人多几个糖果。(1 ≤ n ≤ 30,000,1 ≤ m ≤ 150,000)
题目分析:
和前面的差不多,也是一道模板题了。由于题目保证,我们连超级源点都不用建。设
Si
代表编号为i的人所分到的糖果数目,可得 S
vi
-S
ui
≤
wi
,根据此条件建边跑最短路即可。
这道题非常坑,队列写的 SPFA 无论如何也过不了,本人先用 STL,之后用手写队列,最后用上 SLF / LLL 优化都还是要 TLE。无奈之下只能写 stack(DFS?)版的 SPFA(当然用 Dijkstra 也可以过)
下面附上代码:
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- #include<stack>
- using namespace std;
- const int MX=30005;
- const int INF=0x3f3f3f3f;
- struct Edge{
- int to,len,next;
- }edge[MX*5];
- int head[MX],dis[MX],n,m,now=0;
- stack<int> sta; //必须以DFS?形式的spfa才能过,因此用 stack ,其余不变
- bool ins[MX];
- inline void adde(int u,int v,int len){
- edge[++now].to=v;
- edge[now].len=len;
- edge[now].next=head[u];
- head[u]=now;
- }
- void spfa(int s){ //跑最短路
- sta.push(s);
- ins[s]=true;
- dis[s]=0;
- while (!sta.empty()){
- int u=sta.top();
- sta.pop();
- ins[u]=false;
- for (int i=head[u];i;i=edge[i].next){
- int v=edge[i].to;
- if (dis[u]+edge[i].len<dis[v]){
- dis[v]=dis[u]+edge[i].len;
- if (!ins[v]){
- sta.push(v);
- ins[v]=true;
- }
- }
- }
- }
- return;
- }
- int main(){
- int a,b,v;
- memset(dis,0x3f,sizeof(dis));
- scanf(”%d%d”,&n,&m);
- for (register int i=1;i<=m;i++){ //根据原题意思建边就好了
- scanf(”%d%d%d”,&a,&b,&v);
- adde(a,b,v);
- }
- spfa(1);
- printf(”%d\n”,dis[n]);
- return 0;
- }
3.【POJ 3169】Layout
题目传送门:【POJ 3169】
题目大意: n 头牛编号为 1 到 n,按照编号的顺序排成一列,每两头牛的之间的距离 ≥ 0。这些牛的距离存在着一些约束关系:
1.有ML组(u, v, w)的约束关系,表示牛 u 和牛 v 之间的距离必须 ≤ w。
2.有MD组(u, v, w)的约束关系,表示牛 u 和牛 v 之间的距离必须 ≥ w。
如果这n头牛无法排成队伍,则输出 -1,如果牛 1 和牛 n 的距离可以无限远,则输出 -2,否则则输出牛 1 和牛 n 之间的最大距离。
题目分析:
(其实这道题也是差分的模板题)
由题,建边的时候,有两个约束条件(以下默认 i < j)x
j
-x
i
≤ len
k
,x
j
-x
i
≥ len
k
,将后者转化成 x
i
-x
j
≤ -len
k
,就可以由约束条件建一个有向图。
返回值 -1 表示存在负环,对应为没有一种排队方式满足约束条件,因为有负环存在会导致后面的牛“插队”;
返回值 -2 表示dis
n
= INF,对应为 n 的位置可以无限远。
下面附上代码:
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- #include<queue>
- using namespace std;
- const int MX=100005;
- const int INF=0x3f3f3f3f;
- struct Edge{
- int to,len,next;
- }edge[MX*2];
- int n,ml,md,now=0,head[MX],dis[MX],cnt[MX];
- queue<int> q;
- bool inq[MX];
- inline void adde(int u,int v,int len){
- edge[++now].to=v;
- edge[now].len=len;
- edge[now].next=head[u];
- head[u]=now;
- }
- int spfa(int s){ //最短路,注意返回值有区别
- q.push(s);
- inq[s]=true;
- dis[s]=0;
- while (!q.empty()){
- int u=q.front();
- q.pop();
- inq[u]=false;
- for (int i=head[u];i;i=edge[i].next){
- int v=edge[i].to;
- if (dis[u]+edge[i].len<dis[v]){
- dis[v]=dis[u]+edge[i].len;
- if (!inq[v]){
- q.push(v);
- inq[v]=true;
- if (++cnt[v]>n+1){
- return -1;
- }
- }
- }
- }
- }
- return dis[n]<INF ? dis[n] : -2;
- }
- int main(){
- int a,b,v;
- memset(dis,0x3f,sizeof(dis));
- scanf(”%d%d%d”,&n,&ml,&md);
- for (int i=1;i<=ml;i++){
- scanf(”%d%d%d”,&a,&b,&v);
- adde(a,b,v);
- }
- for (int i=1;i<=md;i++){
- scanf(”%d%d%d”,&a,&b,&v);
- adde(b,a,-v); //这里为什么是 (b,a,-v)? 因为条件变了,符号也要跟着变
- //为了保证所有符号的一致性,所以建边的方向要变
- //(其实只要自己画个图就明白了)
- }
- printf(”%d”,spfa(1));
- return 0;
- }
其实差分约束的应用还有很多,还有待于大家去完善。
参考文献:
1.http://blog.csdn.net/murmured/article/details/19285713
2.http://www.cnblogs.com/hollowstory/p/5677078.html ( POJ 3169 的部分叙述)