差分约束

差分约束系统用于求解 \(n\) 个未知数 \(x_1 \sim x_n\)\(m\) 个形如 \(x_u - x_v \le w\) 也就是 \(x_u \le x_v + w\) 的不等式的解。

两种建图方式:

  1. \(v\)\(u\) 连边权为 \(w\) 的有向边,建立虚点 \(0\) 向每个点连边权为 \(0\) 的有向边,从点 \(0\) 开始跑最短路,得到的是每个 \(x_u \le 0\)\(x_u\) 的最大值,如果图中有负环则无解。
  2. \(u\)\(v\) 连边权为 \(-w\) 的有向边,建立虚点 \(0\) 向每个点连边权为 \(0\) 的有向边,从点 \(0\) 开始跑最长路,得到的是每个 \(x_u \ge 0\)\(x_u\) 的最大值,如果图中有负环则无解。

例题:P5960 【模板】差分约束

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <utility>
const int N = 5005;
const int INF = 1e9;
std::vector<std::pair<int, int>> g[N];
int cnt[N], dis[N];
bool inq[N];
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int c1, c2, y;
        scanf("%d%d%d", &c1, &c2, &y);
        g[c2].push_back({c1, y});
    }
    for (int i = 1; i <= n; i++) {
        dis[i] = INF; 
        g[0].push_back({i, 0});
    }
    std::queue<int> q;
    q.push(0); dis[0] = 0; inq[0] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop(); inq[u] = false;
        for (auto e : g[u]) {
            int v = e.first, w = e.second;
            if (dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1;
                if (cnt[v] >= n + 1) { // 注意多了一个虚点0
                    printf("NO\n"); return 0;
                }
                if (!inq[v]) {
                    q.push(v); inq[v] = true;
                }
            }
        }
    }
    for (int i = 1; i <= n; i++) printf("%d ", dis[i]);
    return 0;
}

例题:P1993 小 K 的农场

  1. \(x_a - x_b \ge c\) 实际上就是 \(x_b - x_a \le -c\),可以让 \(a\)\(b\) 连一条边权为 \(-c\) 的边
  2. \(x_a - x_b \le c\) 是差分约束系统的标准形式,让 \(b\)\(a\) 连一条边权为 \(c\) 的边
  3. \(x_a = x_b\) 也可以转化成不等式关系,即 \(x_b - x_a \le 0\)\(x_a - x_b \le 0\),让 \(a\)\(b\) 之间互相连边权为 \(0\) 的边
参考代码
#include <cstdio>
#include <vector>
#include <utility>
#include <queue>
const int N = 5005;
const int INF = 1e9;
std::vector<std::pair<int, int>> g[N];
int cnt[N], dis[N];
bool inq[N];
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int t, a, b, c; scanf("%d%d%d", &t, &a, &b);
        if (t != 3) scanf("%d", &c);
        if (t == 1) g[a].push_back({b, -c});
        else if (t == 2) g[b].push_back({a, c});
        else {
            g[a].push_back({b, 0}); g[b].push_back({a, 0});
        }
    }
    for (int i = 1; i <= n; i++) {
        g[0].push_back({i, 0}); dis[i] = INF;
    }
    std::queue<int> q;
    q.push(0); inq[0] = true; dis[0] = 0;
    while (!q.empty()) { 
        int u = q.front(); q.pop(); inq[u] = false;
        for (auto e : g[u]) {
            int v = e.first, w = e.second;
            if (dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1;
                if (cnt[v] >= n + 1) {
                    printf("No\n"); return 0;
                }
                if (!inq[v]) {
                    q.push(v); inq[v] = true;
                }
            }
        }
    }
    printf("Yes\n");
    return 0;
}

习题:P1260 工程规划

给定包含 \(n \ (5 \le n \le 1000)\) 个子任务的工程,每个任务的起始时间为 \(T_1, T_2, \dots, T_n\)(均为非负整数,代表整个工程开始后的启动时间)。另外给定 \(m \ (5 \le m \le 5000)\) 个限制条件,每个限制条件形如不等式 \(T_i - T_j \le b\)

任务是:

  1. 找出一种可能的起始时间序列 \(T_1, T_2, \dots, T_n\)
  2. 要求最早进行的任务起始时间必须与整个工程的起始时间相同,即 \(T_1, T_2, \dots, T_n\) 中至少有一个为 \(0\)
  3. 若无可行方案,输出 NO SOLUTION
解题思路

\(T_i - T_j \le b\) 可以变形为 \(T_i \le T_j + b\),这与图论中单源最短路的三角不等式 \(d(v) \le d(u) + w(u,v)\) 形式完全一致。因此,可以将每个任务看作图中的一个顶点:对于每个不等式 \(T_i - T_j \le b\),从顶点 \(j\) 向顶点 \(i\) 连一条有向边,边的权值为 \(b\);如果图中存在 负权环,则说明不等式组无解,输出 NO SOLUTION

由于图可能不是强连通的,引入一个 超级源点 \(0\)。从超级源点 \(0\) 向每个任务节点 \(1 \le i \le n\) 连一条边权为 \(0\) 的有向边,代表不等式 \(T_i - T_0 \le 0\)。设定 \(T_0 = 0\),这样在求最短路时,所有的最短路初始上限都会被约束在 \(0\) 以下,即 \(T_i \le 0\)。同时,从源点 \(0\) 出发可以遍历到图中的所有节点,从而能够检测出图中的任意负权环。

如果未检测到负环,会求得一组解 \(d_1, d_2, \dots, d_n\)。以超级源点为基准算出的最短路值 \(d_i \le 0\),而答案需要满足所有的起始时间均为非负整数(\(T_i \ge 0\))和至少有一个任务的起始时间为 \(0\)。因为差分约束系统的解具有 平移不变性(即若 \(T\) 是一组解,则对任意常数 \(C\)\(T+C\) 也是一组解),可以找出这组解中的最小值 \(\min \{ d \}\),然后将每个节点的起始时间修正为 \(T_i = d_i - \min \{ d \}\)。这样修正后的解不仅满足所有差分约束条件,而且保证了所有起始时间均非负,且至少有一个(即取得最小值的节点)起始时间为 \(0\)

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 1005;
const int INF = 1e9;
struct Edge {
    int to, w;
};
vector<Edge> a[N];
int d[N], cnt[N];
bool inq[N];
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        // T_u - T_v <= w  =>  T_u <= T_v + w
        // 建立从 v 到 u,边权为 w 的有向边
        a[v].push_back({u, w});
    }
    for (int i = 1; i <= n; i++) {
        // 增加超级源点 0,向每个顶点 i 连一条权值为 0 的边
        a[0].push_back({i, 0});
        d[i] = INF;
    }
    queue<int> q;
    q.push(0); inq[0] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop(); inq[u] = false;
        for (Edge e : a[u]) {
            int v = e.to, w = e.w;
            if (d[u] + w < d[v]) {
                d[v] = d[u] + w;
                cnt[v] = cnt[u] + 1;
                if (cnt[v] > n) {
                    printf("NO SOLUTION\n");
                    return 0;
                }
                if (!inq[v]) {
                    q.push(v); inq[v] = true;
                }
            }
        }
    }
    int mind = INF;
    for (int i = 1; i <= n; i++) mind = min(mind, d[i]);
    for (int i = 1; i <= n; i++) printf("%d\n", d[i] - mind);
    return 0;
}
posted @ 2026-05-29 20:45  RonChen  阅读(9)  评论(0)    收藏  举报