差分约束系统(System of Difference Constraints)是一类特殊的不等式组,形如 xᵢ - xⱼ ≤ cₖ。这类系统可以巧妙地转化为图论中的单源最短路径问题求解,是算法竞赛和实际工程中判断约束系统可行性的重要工具。本文用Java完整实现差分约束系统的建模与求解。
一、问题引入:项目排期约束
假设有 n 个任务,任务之间存在如下约束:
- 任务A必须在任务B开始前完成:
B_start - A_end ≥ 0 - 两个任务之间至少间隔3天:
B_start - A_end ≥ 3 - 任务C必须在任务D开始后5天内启动:
C_start - D_start ≤ 5
将这些约束统一变形为 xⱼ - xᵢ ≤ c 的形式,就构成了差分约束系统。
二、核心定理:差分约束转最短路
定理:差分约束系统 xⱼ - xᵢ ≤ cₖ 有解,当且仅当对应的图中不存在负权环。
建图方法:
– 对每个变量 xᵢ,创建节点 i
– 对每条约束 xⱼ - xᵢ ≤ c,添加有向边 i → j,权值为 c
– 添加超级源点 S,向所有节点连权值为0的边(保证图连通)
求解方法:从超级源点运行Bellman-Ford或SPFA算法求最短路。若 dist[j] 表示从源点到 j 的最短距离,则 xⱼ = dist[j] 就是一组可行解。
为什么有效?
最短路满足三角不等式:dist[j] ≤ dist[i] + w(i,j),即 dist[j] - dist[i] ≤ w(i,j) = c,正好对应原约束。
三、完整Java实现
import java.util.*;
/**
* 差分约束系统求解器
* 将不等式组转化为最短路问题,使用SPFA算法检测负环
*/
public class DifferenceConstraints {
static class Edge {
int to; // 目标节点
int weight; // 边权 (对应约束中的常数c)
Edge(int to, int weight) {
this.to = to;
this.weight = weight;
}
}
private int n; // 变量个数
private List<Edge>[] graph; // 邻接表
private static final int INF = Integer.MAX_VALUE / 2;
/**
* 初始化求解器
* @param n 变量个数 (x₀ 到 xₙ₋₁)
*/
@SuppressWarnings("unchecked")
public DifferenceConstraints(int n) {
this.n = n;
// 节点0~n-1对应变量x₀~xₙ₋₁,节点n为超级源点
graph = new ArrayList[n + 1];
for (int i = 0; i <= n; i++) {
graph[i] = new ArrayList<>();
}
}
/**
* 添加约束: x_j - x_i <= c
* 对应边 i -> j,权值为 c
* @param i 变量i的下标
* @param j 变量j的下标
* @param c 常数
*/
public void addConstraint(int i, int j, int c) {
graph[i].add(new Edge(j, c));
}
/**
* 添加约束: x_j - x_i >= c
* 变形为: x_i - x_j <= -c
*/
public void addGreaterEqual(int i, int j, int c) {
addConstraint(j, i, -c);
}
/**
* 添加约束: x_j - x_i == c
* 分解为: x_j - x_i <= c 且 x_i - x_j <= -c
*/
public void addEqual(int i, int j, int c) {
addConstraint(i, j, c);
addConstraint(j, i, -c);
}
/**
* 使用SPFA求解,返回可行解或检测负环
* @return 若系统可行,返回各变量的一个解;若不可行,返回null
*/
public int[] solve() {
int totalNodes = n + 1; // 包含超级源点
int[] dist = new int[totalNodes];
int[] count = new int[totalNodes]; // 记录每个节点入队次数
boolean[] inQueue = new boolean[totalNodes];
Arrays.fill(dist, INF);
dist[n] = 0; // 超级源点距离为0
Queue<Integer> queue = new LinkedList<>();
queue.offer(n);
inQueue[n] = true;
// 超级源点向所有节点连权值为0的边
for (int i = 0; i < n; i++) {
graph[n].add(new Edge(i, 0));
}
while (!queue.isEmpty()) {
int u = queue.poll();
inQueue[u] = false;
for (Edge e : graph[u]) {
int v = e.to;
int w = e.weight;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
count[v] = count[u] + 1;
// 若某节点入队超过n次,说明存在负环
if (count[v] >= totalNodes) {
return null; // 系统不可行
}
if (!inQueue[v]) {
queue.offer(v);
inQueue[v] = true;
}
}
}
}
// 返回x₀~xₙ₋₁的解(dist[0]~dist[n-1])
return Arrays.copyOf(dist, n);
}
/**
* 求解并输出详细信息
*/
public void solveWithInfo() {
System.out.println("约束系统信息:");
System.out.println(" 变量个数: " + n);
System.out.println(" 约束条数: " + (graph[n].size() - n)); // 减去超级源点边
for (int i = 0; i < n; i++) {
for (Edge e : graph[i]) {
System.out.printf(" x%d - x%d <= %d%n", e.to, i, e.weight);
}
}
int[] solution = solve();
if (solution == null) {
System.out.println("\n结果: 系统不可行(存在矛盾约束/负环)");
} else {
System.out.println("\n结果: 系统可行,一组解为:");
for (int i = 0; i < n; i++) {
System.out.printf(" x%d = %d%n", i, solution[i]);
}
}
}
public static void main(String[] args) {
// 示例1: 可行系统
System.out.println("========== 示例1: 项目排期约束 ==========");
DifferenceConstraints solver1 = new DifferenceConstraints(4);
// x1 - x0 <= 5 (任务1在任务0开始后5天内开始)
solver1.addConstraint(0, 1, 5);
// x2 - x1 <= 3 (任务2在任务1开始后3天内开始)
solver1.addConstraint(1, 2, 3);
// x3 - x2 <= 2 (任务3在任务2开始后2天内开始)
solver1.addConstraint(2, 3, 2);
// x3 - x0 >= 8 -> x0 - x3 <= -8 (任务3至少在任务0开始8天后)
solver1.addGreaterEqual(0, 3, 8);
solver1.solveWithInfo();
// 示例2: 不可行系统(存在矛盾)
System.out.println("\n========== 示例2: 矛盾约束 ==========");
DifferenceConstraints solver2 = new DifferenceConstraints(3);
solver2.addConstraint(0, 1, 2); // x1 - x0 <= 2
solver2.addConstraint(1, 2, 3); // x2 - x1 <= 3
solver2.addConstraint(2, 0, -10); // x0 - x2 <= -10 => x2 - x0 >= 10
// 链式推导: x2 - x0 <= 5, 但要求 >= 10,矛盾!
solver2.solveWithInfo();
// 示例3: 区间约束(常用于求解变量范围)
System.out.println("\n========== 示例3: 区间范围约束 ==========");
DifferenceConstraints solver3 = new DifferenceConstraints(3);
// 0 <= x0 <= 10
solver3.addConstraint(3, 0, 10); // x0 <= 10 (使用虚拟节点3作为参考点,其值为0)
// 实际实现中可以通过固定一个变量的值来求解相对范围
// 更常见的方式: 求最大值时求最长路,求最小值时求最短路
// 演示:求每个变量的最大值(转化为最长路)
System.out.println("求解变量最大值(最长路):");
solveMaxValues();
}
/**
* 求每个变量的最大可能值
* 转化为最长路问题:x_j - x_i >= c => 边 i->j 权值为 c
*/
private static void solveMaxValues() {
// 对于求最大值,约束 x_j - x_i <= c 意味着 x_j 的上界
// 最短路结果 dist[j] 就是 x_j 的最小上界(即最大值)
DifferenceConstraints solver = new DifferenceConstraints(4);
solver.addConstraint(0, 1, 5);
solver.addConstraint(1, 2, 3);
solver.addConstraint(0, 2, 10);
int[] maxValues = solver.solve();
if (maxValues != null) {
System.out.println("各变量的一个可行解(也是最小值解):");
for (int i = 0; i < maxValues.length; i++) {
System.out.printf(" x%d <= %d%n", i, maxValues[i]);
}
}
}
}
四、高级应用:区间覆盖与变量范围
差分约束常用于求解”在已知相对约束下,某个变量的取值范围”。
/**
* 差分约束高级应用:求解变量最大值和最小值
*/
public class RangeQueryWithConstraints {
/**
* 求每个变量的最大可能值
* 方法: 以超级源点为起点求最短路,dist[i] 即为 x_i 的最大值
*/
public static int[] maxValues(DifferenceConstraints solver) {
return solver.solve();
}
/**
* 求 x_target 的最大值和最小值
* 最大值: 以超级源点为起点求最短路到 target
* 最小值: 转化为求 -x_target 的最大值,即求最长路
*/
public static void queryRange(List<int[]> constraints, int n, int target) {
// 最大值: 直接求解原系统
DifferenceConstraints maxSolver = new DifferenceConstraints(n);
for (int[] c : constraints) {
maxSolver.addConstraint(c[0], c[1], c[2]); // x_j - x_i <= c
}
int[] maxSolution = maxSolver.solve();
int maxVal = maxSolution != null ? maxSolution[target] : Integer.MAX_VALUE;
// 最小值: 将所有约束取反,x_i - x_j <= -c,求最短路
DifferenceConstraints minSolver = new DifferenceConstraints(n);
for (int[] c : constraints) {
minSolver.addConstraint(c[1], c[0], -c[2]); // x_i - x_j <= -c
}
int[] minSolution = minSolver.solve();
int minVal = minSolution != null ? -minSolution[target] : Integer.MIN_VALUE;
System.out.printf("x%d 的取值范围: [%d, %d]%n", target, minVal, maxVal);
}
public static void main(String[] args) {
List<int[]> constraints = new ArrayList<>();
constraints.add(new int[]{0, 1, 5}); // x1 - x0 <= 5
constraints.add(new int[]{1, 2, 3}); // x2 - x1 <= 3
constraints.add(new int[]{0, 2, 10}); // x2 - x0 <= 10
constraints.add(new int[]{2, 0, -4}); // x0 - x2 <= -4 => x2 - x0 >= 4
System.out.println("约束条件:");
for (int[] c : constraints) {
System.out.printf(" x%d - x%d <= %d%n", c[1], c[0], c[2]);
}
for (int i = 0; i < 3; i++) {
queryRange(constraints, 3, i);
}
}
}
五、建图技巧速查表
| 原约束形式 | 变形为 | 添加的边 | 说明 |
|---|---|---|---|
xⱼ - xᵢ ≤ c |
— | i → j, 权 c |
标准形式 |
xⱼ - xᵢ ≥ c |
xᵢ - xⱼ ≤ -c |
j → i, 权 -c |
反向连边 |
xⱼ - xᵢ = c |
同时≤和≥ | 双向边 | 等式约束 |
xᵢ ≤ c |
xᵢ - x₀ ≤ c |
0 → i, 权 c |
固定参考点 |
xᵢ ≥ c |
x₀ - xᵢ ≤ -c |
i → 0, 权 -c |
固定参考点 |
xᵢ - xⱼ < c |
xᵢ - xⱼ ≤ c-1 |
j → i, 权 c-1 |
严格不等式 |
六、复杂度分析
| 步骤 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 建图 | O(m) | O(n + m) |
| SPFA求解 | O(n × m) 最坏,实际通常很快 | O(n) |
| 负环检测 | 内建于SPFA | O(n) |
其中 n 为变量数,m 为约束数。
七、总结
差分约束系统的精髓在于将代数不等式转化为图论最短路问题。这一转化的桥梁是三角不等式:dist[j] ≤ dist[i] + w(i,j)。掌握这一技巧后,你就能优雅地解决项目排期、资源分配、变量范围估计等一系列约束满足问题。记住核心口诀:”小于等于走最短路,大于等于反向建边走最长路”。