图论专题-学习笔记:差分约束
一些 Update
Update 2021/11/16:发现之前推的结论有严重错误,现已更正,如果有读者被误导,在此深表歉意。
1. 前言
差分约束是一种最短路算法,专门用来解决下面这类问题:
已知 \(n\) 个正整数 \(x_1,x_2,...,x_n\),与 \(m\) 个形如 \(x_i-x_j \leq k,k \in Z\) 的不等式,问是否存在一组解使得 \(x_1,...,x_n\) 满足上述不等式并求出任意一组解。
前置知识:SPFA。
2. 详解
初看本题,可能会毫无头绪:这跟最短路有什么关系?
但实际上,\(x_i-x_j \leq k\) 这个式子我们稍微做个变形:
对比一下在最短路中我们经常用到的式子:
你会发现这两个式子实际上长得非常像。
有的人会问了:我们一般的转移式子不是 \(dis_u \geq dis_j+val\) 吗?为什么是小于等于号而不是大于等于号?
理由非常简单,因为第 1 个式子要求必须满足,而第 2 个式子在求完最短路后也必须满足(否则不是最短路)。
而 \(dis_u \geq dis_j + val\) 实质上是表示此时还没有求出最短路,需要继续转移。
所以采用了小于等于号。
因此如果我们将 \(x_i\) 视作 \(dis_i\),\(k\) 视作 \(val\),那么我们就可以这样做:
首先对于每一个式子 \(x_i-x_j \leq k\),从 \(j\) 向 \(i\) 连一条边。
然后建立一个超级起点 0,0 向所有点连一条边权为 0 的边。
这么做是为了方便求最短路径。
需要注意的是,在不同问题中边权不一定为 0,需要具体问题具体分析。
然后求一遍最短路,此时求出来的所有 \(dis_i\) 应该是满足原题中给出的不等式的。
那么 \(\{dis\}\) 就是满足条件的一组解。
当然所有 \(dis_i\) 同时加上任意常数 \(k\) 也行,反正做差之后被消掉了。
那么什么时候无解呢?
无解的情况:图中出现负环,此时表明有若干个不等式不能同时成立。
特别提醒:不能使用 dijkstra 求差分约束类题目,因为有负权边。
代码:
/*
========= Plozia =========
Author:Plozia
Problem:P5960 【模板】差分约束算法
Date:2021/4/29
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::queue;
typedef long long LL;
const int MAXN = 5e3 + 10;
int n, m, Head[MAXN], cnt_Edge = 1, dis[MAXN], cnt[MAXN];
struct node { int to, val, Next; } Edge[MAXN << 1];
bool book[MAXN];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
void add_Edge(int x, int y, int z) { ++cnt_Edge; Edge[cnt_Edge] = (node){y, z, Head[x]}; Head[x] = cnt_Edge; }
void SPFA()
{
memset(dis, 0x3f, sizeof(dis));
memset(cnt, 0, sizeof(cnt));
memset(book, 0, sizeof(book));
queue <int> q; q.push(0); dis[0] = 0; book[0] = 1;
while (!q.empty())
{
int x = q.front(); q.pop(); book[x] = 0;
for (int i = Head[x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (dis[x] + Edge[i].val < dis[u])
{
dis[u] = dis[x] + Edge[i].val;
if (!book[u])
{
book[u] = 1; q.push(u); ++cnt[u];
if (cnt[u] > n) { printf("NO\n"); exit(0); }
}
}
}
}
}
int main()
{
n = read(), m = read();
for (int i = 1; i <= m; ++i)
{
int y = read(), x = read(), z = read();//注意这里 x 和 y 的顺序
add_Edge(x, y, z);
}
for (int i = 1; i <= n; ++i) add_Edge(0, i, 0);
SPFA();
for (int i = 1; i <= n; ++i) printf("%d ", dis[i]);
printf ("\n"); return 0;
}
3. 扩展
两个结论:
- \(x_i-x_j \leq k \leftrightarrow x_i \leq x_j+k\),这个式子与最短路的三角形不等式 \(dis_i \leq dis_j+k\) 很相似,因此连边 \((j,i,k)\)。
由于是最短路的三角形不等式,因此原图要求最短路。 - \(x_i-x_j \leq k \leftrightarrow x_j \geq x_i+k\),这个式子与最长路的三角形不等式 \(dis_j \geq dis_i-k\) 很相似,因此连边 \((i,j,-k)\)。
由于是最长路的三角形不等式,因此原图要求最长路。
其实上面的第一个结论就是前面步步推理的结论,而第二个结论则是类比第一个结论的推理过程,采用了最长路来解决问题。
4. 总结
结论如下:
- \(x_i-x_j \leq k \leftrightarrow x_i \leq x_j+k\),这个式子与最短路的三角形不等式 \(dis_i \leq dis_j+k\) 很相似,因此连边 \((j,i,k)\)。
由于是最短路的三角形不等式,因此原图要求最短路。 - \(x_i-x_j \leq k \leftrightarrow x_j \geq x_i-k\),这个式子与最长路的三角形不等式 \(dis_j \geq dis_i+k\) 很相似,因此连边 \((i,j,-k)\)。
由于是最长路的三角形不等式,因此原图要求最长路。