1. 程式人生 > 其它 >圖論專題-學習筆記:差分約束

圖論專題-學習筆記:差分約束

目錄

一些 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)\)
    由於是最長路的三角形不等式,因此原圖要求最長路。

其實上面的第一個結論就是前面步步推理的結論,而第二個結論則是類比第一個結論的推理過程,採用了最長路來解決問題。

這兩種結論都是對的,但是在運用這兩種結論的時候需要注意一點:邊權在題目中必須有意義!

比如說 P3275 [SCOI2011]糖果 這道題,如果採用最長路則答案正確,但是如果採用最短路答案是錯誤的。

為什麼?

比如說 \(z=2\) 的時候有 \(a-b\leq-1\),如果是採用最短路,那麼需要連邊 \((b,a,-1)\)但是糖果數量顯然非負,因此邊權都無意義,求出來的答案自然是錯的。

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)\)
    由於是最長路的三角形不等式,因此原圖要求最長路。