图论专题-学习笔记:差分约束

一些 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. 详解

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

初看本题,可能会毫无头绪:这跟最短路有什么关系?

但实际上,\(x_i-x_j \leq k\) 这个式子我们稍微做个变形:

\[x_i \leq x_j + k \]

对比一下在最短路中我们经常用到的式子:

\[dis_u \leq dis_j+val \]

你会发现这两个式子实际上长得非常像。

有的人会问了:我们一般的转移式子不是 \(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)\)
    由于是最长路的三角形不等式,因此原图要求最长路。
posted @ 2022-04-17 15:22  Plozia  阅读(59)  评论(0编辑  收藏  举报