每日算法 — 使用java实现差分约束系统:最短路建模与负环检测

问题背景

差分约束系统(Difference Constraints)是一类特殊的不等式组,每个不等式的形式为 x_i - x_j ≤ c_k。这类系统可以高效地判定是否有解,并在有解时求出一组可行赋值。其核心思想是将不等式转化为图论中的最短路问题,利用Bellman-Ford或SPFA算法求解。

差分约束广泛应用于项目进度规划、资源分配、时序约束验证等实际场景。例如,任务A必须在任务B开始前完成,可以建模为 start_B - finish_A ≥ 0,转化为差分约束的标准形式。

核心算法思想

不等式转图论模型

对于约束 x_i - x_j ≤ c,等价于 x_i ≤ x_j + c,对应图中的有向边 j → i,边权为 c。从源点出发求最短路,若图中不存在负环,则最短路径长度即为一组可行解。

为处理不连通情况,通常引入超级源点 S,向所有变量节点添加权值为0的边 S → x_i。

负环判定

差分约束系统无解的充要条件是图中存在负环。Bellman-Ford算法在 n-1 轮松弛后,若仍能松弛,则说明存在负环。SPFA算法通过统计每个节点的入队次数,若超过 n 次则说明存在负环。

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;

    public DifferenceConstraints(int n) {
        this.n = n;
        // 节点0为超级源点,1~n为变量节点
        this.graph = new ArrayList[n + 1];
        for (int i = 0; i <= n; i++) {
            graph[i] = new ArrayList<>();
        }
        // 超级源点向所有变量连权值为0的边
        for (int i = 1; i <= n; i++) {
            graph[0].add(new Edge(i, 0));
        }
    }

    /**
     * 添加约束: x_u - x_v <= w
     * 对应边 v -> u,权值为 w
     * 注意:节点编号从1开始
     */
    public void addConstraint(int u, int v, int w) {
        // x_u <= x_v + w  =>  v -> u, weight = w
        graph[v].add(new Edge(u, w));
    }

    /**
     * 添加约束: x_u - x_v >= w
     * 转化为: x_v - x_u <= -w
     */
    public void addGreaterOrEqual(int u, int v, int w) {
        // x_u >= x_v + w  =>  x_v <= x_u - w
        graph[u].add(new Edge(v, -w));
    }

    /**
     * 添加等式约束: x_u == x_v + w
     * 拆分为两个不等式
     */
    public void addEqual(int u, int v, int w) {
        addConstraint(u, v, w);      // x_u - x_v <= w
        addGreaterOrEqual(u, v, w);  // x_u - x_v >= w
    }

    /**
     * SPFA算法求解最短路并检测负环
     * @return 若存在负环返回null;否则返回各变量的可行解(节点1~n)
     */
    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[0] = 0;
        inQueue[0] = true;

        Queue<Integer> queue = new LinkedList<>();
        queue.offer(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;
                    if (!inQueue[v]) {
                        queue.offer(v);
                        inQueue[v] = true;
                        count[v]++;
                        // 若某节点入队超过n次,说明存在负环
                        if (count[v] > n) {
                            return null;
                        }
                    }
                }
            }
        }

        // 返回变量节点的解(去掉超级源点)
        int[] result = new int[n];
        System.arraycopy(dist, 1, result, 0, n);
        return result;
    }

    /**
     * 使用Bellman-Ford算法检测负环(更稳定但较慢)
     */
    public int[] solveByBellmanFord() {
        int totalNodes = n + 1;
        int[] dist = new int[totalNodes];
        Arrays.fill(dist, INF);
        dist[0] = 0;

        // 松弛n轮
        for (int i = 0; i < n; i++) {
            boolean updated = false;
            for (int u = 0; u < totalNodes; u++) {
                if (dist[u] == INF) continue;
                for (Edge e : graph[u]) {
                    int v = e.to;
                    int w = e.weight;
                    if (dist[u] + w < dist[v]) {
                        dist[v] = dist[u] + w;
                        updated = true;
                    }
                }
            }
            if (!updated) break;
        }

        // 第n+1轮检查是否还能松弛
        for (int u = 0; u < totalNodes; u++) {
            if (dist[u] == INF) continue;
            for (Edge e : graph[u]) {
                int v = e.to;
                int w = e.weight;
                if (dist[u] + w < dist[v]) {
                    return null; // 存在负环
                }
            }
        }

        int[] result = new int[n];
        System.arraycopy(dist, 1, result, 0, n);
        return result;
    }

    public static void main(String[] args) {
        // 示例:变量 x1, x2, x3
        // 约束: x1 - x2 <= 3
        //       x2 - x3 <= -2
        //       x1 - x3 <= 4
        DifferenceConstraints dc = new DifferenceConstraints(3);
        dc.addConstraint(1, 2, 3);  // x1 - x2 <= 3
        dc.addConstraint(2, 3, -2); // x2 - x3 <= -2
        dc.addConstraint(1, 3, 4);  // x1 - x3 <= 4

        int[] solution = dc.solve();
        if (solution != null) {
            System.out.println("可行解: ");
            for (int i = 0; i < solution.length; i++) {
                System.out.println("x" + (i + 1) + " = " + solution[i]);
            }
        } else {
            System.out.println("约束系统无解(存在负环)");
        }

        // 不可满足示例:x1 - x2 <= 1, x2 - x1 <= -3
        DifferenceConstraints dc2 = new DifferenceConstraints(2);
        dc2.addConstraint(1, 2, 1);   // x1 <= x2 + 1
        dc2.addConstraint(2, 1, -3);  // x2 <= x1 - 3  =>  x1 >= x2 + 3
        int[] sol2 = dc2.solve();
        System.out.println(sol2 == null ? "不可满足(预期结果)" : "可满足");
    }
}

算法复杂度分析

  • 建图时间:O(m),m为约束数量
  • SPFA求解:平均 O(m),最坏 O(nm)(存在负环时)
  • Bellman-Ford求解:稳定 O(nm)
  • 负环检测:与最短路算法同时进行
  • 总时间复杂度:O(nm)(最坏情况)
  • 空间复杂度:O(n + m)

实际应用案例

项目工期规划

设 x_i 为任务i的最早开始时间,若任务j依赖于任务i且需要耗时 d,则有 x_j ≥ x_i + d,即 x_i - x_j ≤ -d。

最大差值求解

要求 max(x_i - x_j) 在约束下的最大值,可二分答案 mid,检验 x_i - x_j ≤ mid 是否有解。

差分约束的精髓在于”不等式即边”的转化思想。一旦掌握这种建模方式,许多看似复杂的约束问题都能迎刃而解。