码迷,mamicode.com
首页 > 其他好文 > 详细

浅谈差分约束系统

时间:2020-04-13 09:12:31      阅读:85      评论:0      收藏:0      [点我收藏+]

标签:typedef   注意   国家   超级   数组   amp   讲解   name   差分约束系统   

简述

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

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

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

引例

差分约束模板题

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

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

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

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

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

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

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

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

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

再普及一下:通常形如 \(dis_i\leq 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-Y\leq Z\) 的式子。

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

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

当然,我们不一定要强制不等式为 \(X-Y\leq Z\),同样可以用 \(X-Y\geq 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_i\geq 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\n", dis[t]);
    return 0;
}

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

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

例题二

糖果

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

  1. \(A=B\),可化为:\(A-B\geq 0,B-A\geq 0\)
  2. \(A<B\),可化为:\(B-A\geq 1\)
  3. \(A\geq B\),可化为:\(A-B\geq 0\)
  4. \(A>B\),可化为:\(A-B\geq 1\)
  5. \(A\leq B\),可化为:\(B-A\geq 0\)

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

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

既然每个小朋友至少有一个糖果,就令 \(dis_i=1(1\leq i\leq 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\n");
                    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\n");
            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\n", ans);
    return 0;
}

例题三

排队布局

显然,简单的差分关系:

  1. \(P_B-P_A\leq D\)
  2. \(P_B-P_A\geq D\)\(P_A-P_B\leq -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\n", dis[n]);
    return 0;
}

总结

总结一下:

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

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

的国家集训队论文。

完结撒花。

浅谈差分约束系统

标签:typedef   注意   国家   超级   数组   amp   讲解   name   差分约束系统   

原文地址:https://www.cnblogs.com/lpf-666/p/12689007.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!