浅谈差分约束系统
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) 的式子。
这是就需要一下这些技巧来帮助你确定不等关系了:
- (X-Y<Z):可变为 (X-Yleq Z-1)。(一般都为正整数)
- (X-Ygeq Z):可变为 (Y-Xleq -Z)。
- (X-Y>Z):可变为 (Y-Xleq -Z-1)。
- (X-Y=Z):可变为 (X-Yleq 0,Y-Xleq 0)。(两个不等式,即建立双向边)
当然,我们不一定要强制不等式为 (X-Yleq Z),同样可以用 (X-Ygeq Z)。
此时问题就变为求单源最长路,当有正环时无解。
应用
前面提到过,差分约束的应用并不是很广泛,这里用几道例题来讲解。
在看例题之前,要提一句。众所周知,解决有负权边的最短路/最长路用的是 SPFA,所以不要给我用什么 Dijkscal。
例题一
设输入数据为 (u,v,w),(s) 为起点(即最小的 (u-1)),(t) 为终点(即最大的 (v)),(dis_i) 表示 ([s,i]) 中选择了几个数。
那么对于每一组输入数据显然有 (dis_v-dis_{u-1}geq w),于是可以在点 (u-1) 与点 (v) 中连接一条边权为 (w) 的边。
这里提出一个重要的道理说法:
在差分约束题目中,通常隐含着一些巧妙的不等关系,而这些因素往往容易忽略却至关重要。
首先,笔者要求读者时刻记住这句话。
然后来举个栗子解释一下这句话,在本题中就有以下几个非常容易忽略却至关重要的条件:
- (dis_{i+1}-dis_igeq 0)。(即后面一个不小于前面一个)
- (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;
}
当然这道题也可以用贪心+树状数组做,而且跑得比差分约束快。
具体方法由于限于篇幅(同时也因为这不是本章的重点),不做过多介绍,可以看看我的代码。
例题二
条件很多?不慌,一一分析即可。(记得用上面的技巧)
- (A=B),可化为:(A-Bgeq 0,B-Ageq 0)。
- (A<B),可化为:(B-Ageq 1)。
- (Ageq B),可化为:(A-Bgeq 0)。
- (A>B),可化为:(A-Bgeq 1)。
- (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;
}
例题三
显然,简单的差分关系:
- (P_B-P_Aleq D)。
- (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;
}
总结
总结一下:
差分约束是个好东西。- 差分约束一定要考虑到没有明说的条件。
- 差分约束一定要考虑到无解情况。
然后...,就推荐大家看一下《数与图的完美结合——浅析差分约束系统 written by 华中师大一附中 冯威》。
的国家集训队论文。
完结撒花。
以上是关于浅谈差分约束系统的主要内容,如果未能解决你的问题,请参考以下文章