浅谈差分约束系统

Posted lpf-666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈差分约束系统相关的知识,希望对你有一定的参考价值。

简述

差分约束是一个建立与最短路实现的算法,通常用来解决一些不等式组相关的问题。

其实这并不是一个新的算法,只是加入了新的思想罢了,使用范围比较小。

话不多说,直接进入正题吧。ヾ(?°?°?)??

引例

差分约束模板题

其实差分约束系统就是一种特殊的 (N) 元不等式组,它包含 (N) 个变量 ([X_1,X_N]) 以及 (M) 各约束条件。

每个约束条件都是两个变量的差构成的,形如 (X_i-X_jleq c_k),其中 (c_k) 是一个可正可负的常数。

熟悉最短路的同学不难发现,当原式变形为 (X_ileq X_j+c_k) 时,这与最短路问题中 (dis_ileq dis_j+w_{i,j}) 十分相似。

尝试建立图论模型,那么显然,(X_ileq X_j+c_k),就可以看成是:节点 (j) 与节点 (i) 之间有一条长度为 (c_k) 的有向边。

同时建立一个超级节点 (0),对于每一个节点 (i) 建立 (0 o i) 长度为 (0) 的有向边((1leq ileq N))。

至此,求解的过程就变为求从 (0) 点出发的单源最短路径,解集就是 ({dis_1,dis_2,...,dis_N})

显然,当上式为解时,(dis_1+d,dis_2+d,...,dis_N+d) 也为一组合法的解集,因为差分时将 (d) 的同时减去了。

所以这种特殊的 (N) 元不等式的解并不唯一,因此做题时通常要求我们求最小解或任意一组解。

当然,这种不等式也是有可能无解的:当最短路出现负环时无解

再普及一下:通常形如 (dis_ileq dis_j+w_{i,j}) 的不等式被称为三角形不等式,其实知道也没什么用,但是当 dalao 讲题目时你要听得懂。

代码实现如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iostream>
#include<queue>
#include<cstdlib>
#define N 200010
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;

int n,m,head[N],tot=0,cnt[N],dis[N];
bool vis[N];
struct Edge{
	int nxt,to,val;
}ed[N];

int read(){
	int x=0,f=1;char c=getchar();
	while(c<‘0‘ || c>‘9‘) f=(c==‘-‘)?-1:1,c=getchar();
	while(c>=‘0‘ && c<=‘9‘) x=x*10+c-48,c=getchar();
	return x*f;
}

void add(int u,int v,int w){
	tot++;
	ed[tot].nxt=head[u];
	ed[tot].to=v;
	ed[tot].val=w;
	head[u]=tot;
	return;
}

void SPFA(int s){
	queue<int>q;
	memset(dis,0x3f,sizeof(dis));
	memset(vis,false,sizeof(vis));
	memset(cnt,0,sizeof(cnt));
	vis[s]=true;dis[s]=0;
	q.push(s);
	while(!q.empty()){
		int u=q.front();q.pop();
		vis[u]=false;
		if(++cnt[u]>n){puts("NO");exit(0);}
		for(int i=head[u];i;i=ed[i].nxt){
			int v=ed[i].to,w=ed[i].val;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				if(!vis[v]) q.push(v);
			}
		}
	}
	return;
}

int main(){
	n=read();m=read();
	int u,v,w;
	for(int i=1;i<=m;i++)
		u=read(),v=read(),w=read(),add(v,u,w);
	for(int i=1;i<=n;i++) add(0,i,0);
	SPFA(0);
	for(int i=1;i<=n;i++) printf("%d ",dis[i]);
	return 0;
}

一点小技巧

这个放在前面说是因为后面讲解例题时会用到。

一般做差分约束的题目时,大家往往苦于找到不等关系,但是通常并不会直接给你形如 (X-Yleq Z) 的式子。

这是就需要一下这些技巧来帮助你确定不等关系了:

  1. (X-Y<Z):可变为 (X-Yleq Z-1)。(一般都为正整数)
  2. (X-Ygeq Z):可变为 (Y-Xleq -Z)
  3. (X-Y>Z):可变为 (Y-Xleq -Z-1)
  4. (X-Y=Z):可变为 (X-Yleq 0,Y-Xleq 0)。(两个不等式,即建立双向边)

当然,我们不一定要强制不等式为 (X-Yleq Z),同样可以用 (X-Ygeq Z)

此时问题就变为求单源最长路,当有正环时无解。

应用

前面提到过,差分约束的应用并不是很广泛,这里用几道例题来讲解。

在看例题之前,要提一句。众所周知,解决有负权边的最短路/最长路用的是 SPFA,所以不要给我用什么 Dijkscal。

例题一

Intervals

设输入数据为 (u,v,w)(s) 为起点(即最小的 (u-1)),(t) 为终点(即最大的 (v)),(dis_i) 表示 ([s,i]) 中选择了几个数。

那么对于每一组输入数据显然有 (dis_v-dis_{u-1}geq w),于是可以在点 (u-1) 与点 (v) 中连接一条边权为 (w) 的边。

这里提出一个重要的道理说法:

在差分约束题目中,通常隐含着一些巧妙的不等关系,而这些因素往往容易忽略却至关重要。

首先,笔者要求读者时刻记住这句话。

然后来举个栗子解释一下这句话,在本题中就有以下几个非常容易忽略却至关重要的条件:

  1. (dis_{i+1}-dis_igeq 0)。(即后面一个不小于前面一个)
  2. (dis_i-dis_{i+1}geq -1)。(即后面一个最多比前面一个大一)

这都是很显然的规律,但是容易让初学萌新忽略而缺少条件关系,最终身败名裂。

问题:怎么注意到这些条件关系?

答:没什么固定的方法,因为每道题的关系都是不一样的,这需要选手有敏锐的观察能力和强大的逻辑关系。具体方法就是多做题。

代码如下:

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <iostream>
#include <queue>
#define N 50010
using namespace std;

int n, s = N + 1, t, head[N], dis[N], cnt = 0;
bool vis[N];
struct Edge {
    int nxt, to, val;
} ed[3 * N];

int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < ‘0‘ || c > ‘9‘) f = (c == ‘-‘) ? -1 : 1, c = getchar();
    while (c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - 48, c = getchar();
    return x * f;
}

void addedge(int x, int y, int z) {
    cnt++;
    ed[cnt].nxt = head[x];
    ed[cnt].to = y;
    ed[cnt].val = z;
    head[x] = cnt;
    return;
}

void SPFA() {
    queue<int> q;
    memset(dis, 0xcf, sizeof(dis));
    memset(vis, false, sizeof(vis));
    dis[s] = 0;
    vis[s] = true;
    q.push(s);
    while (!q.empty()) {
        int now = q.front();
        q.pop();
        vis[now] = false;
        for (int i = head[now]; i; i = ed[i].nxt) {
            int y = ed[i].to, z = ed[i].val;
            if (dis[y] < dis[now] + z) {
                dis[y] = dis[now] + z;
                if (!vis[y])
                    q.push(y);
            }
        }
    }
    return;
}

int main() {
    n = read();
    int u, v, w;
    for (int i = 1; i <= n; i++) {
        u = read(), v = read(), w = read();
        addedge(u - 1, v, w);
        s = min(s, u - 1), t = max(t, v);
    }
    for (int i = s; i <= t; i++) {
        addedge(i, i + 1, 0);
        addedge(i + 1, i, -1);
    }
    SPFA();
    printf("%d
", dis[t]);
    return 0;
}

当然这道题也可以用贪心+树状数组做,而且跑得比差分约束快。

具体方法由于限于篇幅(同时也因为这不是本章的重点),不做过多介绍,可以看看我的代码

例题二

糖果

条件很多?不慌,一一分析即可。(记得用上面的技巧)

  1. (A=B),可化为:(A-Bgeq 0,B-Ageq 0)
  2. (A<B),可化为:(B-Ageq 1)
  3. (Ageq B),可化为:(A-Bgeq 0)
  4. (A>B),可化为:(A-Bgeq 1)
  5. (Aleq B),可化为:(B-Ageq 0)

然后注意到每个小朋友都有糖,可以考虑建立超级节点:(0),然后就是 (A-0geq 1)

当然,这样比较耗时间(好像被 hack 了),所以有另一种方法:

既然每个小朋友至少有一个糖果,就令 (dis_i=1(1leq ileq N)),然后所有节点都入队即可。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <queue>
#include <cstdlib>
#define N 1000010
using namespace std;

int n, k, dis[N], head[N], cnt = 0, t[N];
bool vis[N];
struct Edge {
    int nxt, to, val;
} ed[N << 1];

int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < ‘0‘ || c > ‘9‘) f = (c == ‘-‘) ? -1 : 1, c = getchar();
    while (c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - 48, c = getchar();
    return x * f;
}

void add(int u, int v, int w) {
    cnt++;
    ed[cnt].nxt = head[u];
    ed[cnt].to = v;
    ed[cnt].val = w;
    head[u] = cnt;
    return;
}

queue<int> q;
void SPFA() {
    vis[0] = true;
    dis[0] = 0;
    q.push(0);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        vis[u] = false;
        for (int i = head[u]; i; i = ed[i].nxt) {
            int v = ed[i].to, w = ed[i].val;
            if (dis[v] < dis[u] + w) {
                t[i]++;
                if (t[i] > n - 1) {
                    printf("-1
");
                    exit(0);
                }
                dis[v] = dis[u] + w;
                if (!vis[v])
                    q.push(v);
            }
        }
    }
    return;
}

int main() {
    n = read(), k = read();
    int u, v, x;
    for (int i = 1; i <= k; i++) {
        x = read(), u = read(), v = read();
        if (x == 1) {
            add(v, u, 0);
            add(u, v, 0);
        } else if (x == 2)
            add(u, v, 1);
        else if (x == 3)
            add(v, u, 0);
        else if (x == 4)
            add(v, u, 1);
        else
            add(u, v, 0);
        if (x % 2 == 0 && u == v) {
            printf("-1
");
            return 0;
        }
    }
    for (int i = 1; i <= n; i++) {
        vis[i] = dis[i] = 1;
        q.push(i);
    }
    SPFA();
    long long ans = 0;
    for (int i = 1; i <= n; i++) ans += dis[i];
    printf("%lld
", ans);
    return 0;
}

例题三

排队布局

显然,简单的差分关系:

  1. (P_B-P_Aleq D)
  2. (P_B-P_Ageq D)(P_A-P_Bleq -D)

当然,此题是让我们求 (max{dis_N-dis_1}),但是由于差分约束的条件,还是应当求最短路。

(1) 开始有一个弊端,就是不一定能判断出无解,因为有些边不是直接与 (1) 相连。

那么为了判断连通性与负环,应当先建立节点 (0),并跑一遍 SPFA,然后再以 (1) 为源点跑一遍 SPFA。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <queue>
#include <cstdlib>
#define N 200010
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;

int n, m1, m2, head[N], tot = 0, cnt[N], dis[N];
bool vis[N];
struct Edge {
    int nxt, to, val;
} ed[N];

int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < ‘0‘ || c > ‘9‘) f = (c == ‘-‘) ? -1 : 1, c = getchar();
    while (c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - 48, c = getchar();
    return x * f;
}

void add(int u, int v, int w) {
    tot++;
    ed[tot].nxt = head[u];
    ed[tot].to = v;
    ed[tot].val = w;
    head[u] = tot;
    return;
}

void SPFA(int s) {
    queue<int> q;
    memset(dis, 0x3f, sizeof(dis));
    memset(vis, false, sizeof(vis));
    memset(cnt, 0, sizeof(cnt));
    vis[s] = true;
    dis[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        vis[u] = false;
        if (++cnt[u] > n) {
            puts("-1");
            exit(0);
        }
        for (int i = head[u]; i; i = ed[i].nxt) {
            int v = ed[i].to, w = ed[i].val;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                if (!vis[v])
                    q.push(v);
            }
        }
    }
    return;
}

int main() {
    n = read();
    m1 = read();
    m2 = read();
    int u, v, w;
    for (int i = 1; i <= m1; i++) u = read(), v = read(), w = read(), add(u, v, w);
    for (int i = 1; i <= m2; i++) u = read(), v = read(), w = read(), add(v, u, -w);
    for (int i = 1; i <= n; i++) add(0, i, 0);
    SPFA(0);
    SPFA(1);
    if (dis[n] == INF)
        puts("-2");
    else
        printf("%d
", dis[n]);
    return 0;
}

总结

总结一下:

  1. 差分约束是个好东西。
  2. 差分约束一定要考虑到没有明说的条件。
  3. 差分约束一定要考虑到无解情况。

然后...,就推荐大家看一下《数与图的完美结合——浅析差分约束系统 written by 华中师大一附中 冯威》。

的国家集训队论文。

完结撒花。

以上是关于浅谈差分约束系统的主要内容,如果未能解决你的问题,请参考以下文章

浅谈差分约束系统

浅谈差分约束

浅谈差分约束系统——图论不等式的变形

浅谈差分约束问题

差分约束

差分约束系统