1. 程式人生 > >差分約束系統的建立詳解+題集

差分約束系統的建立詳解+題集

差分約束系統

一、何為差分約束系統:

差分約束系統(system of difference constraints),是求解關於一組變數的特殊不等式組之方法。如果一個系統由n個變數和m個約束條件組成,其中每個約束條件形如xj-xi<=bk(i,j∈[1,n],k∈[1,m]),則稱其為差分約束系統(system of difference constraints)。亦即,差分約束系統是求解關於一組變數的特殊不等式組的方法。

通俗一點地說,差分約束系統就是一些不等式的組,而我們的目標是通過給定的約束不等式組求出最大值或者最小值或者差分約束系統是否有解。

比如:

二、差分約束系統的求解:

差分約束系統可以轉化為圖論來解決,對應於上面的不等式組,如果要求出x3-x0的最大值的話,疊加不等式可以推匯出x3-x0<=7,最大值即為7,我們可以通過建立一個圖,包含6個頂點,對每個xj-xi<=bk,建立一條i到j的有向邊,權值為bk。通過求出這個圖的x0到x3的最短路可以知道也為7,這是巧合嗎?並不是。

之所以差分約束系統可以通過圖論的最短路來解,是因為xj-xi<=bk,會發現它類似最短路中的三角不等式d[v] <=d[u]+w[u,v],即d[v]-d[u]<=w[u,v]。而求取最大值的過程類似於最短路演算法中的鬆弛過程。

三角不等式:(在此引用大牛的部落格)

B - A <= c     (1)

C - B <= a     (2)

C - A <= b     (3)

 如果要求C-A的最大值,可以知道max(C-A)= min(b,a+c),而這正對應了下圖中C到A的最短路。 

                                

因此,對三角不等式加以推廣,變數n個,不等式m個,要求xn-x1的最大值,便就是求取建圖後的最短路。

同樣地,如果要求取差分約束系統中xn-x1的最小值,便是求取建圖後的最長路。最長路可以通過spfa求出來,只需要改下鬆弛的方向即可,即if(d[v] < d[u] + dist(u,v)) d[v] = d[u] + dist(u,v)。當然我們可以把圖中所有的邊權取負,求取最短路,兩者是等價的。

最長路求解演算法證明如下:

最後一點,建圖後不一定存在最短路/最長路,因為可能存在無限減小/增大的負環/正環,題目一般會對應於不同的輸出。判斷差分約束系統是否存在解一般判環即可。

3、差分約束系統的應用

差分約束系統的應用很廣,都會有一定的背景,我們只需要根據題意構造出差分約束系統,然後再根據題目的要求求解就行了。

一般題目會有三種情況:(1)、求取最短路 (2)、求取最長路 (3)、判斷差分約束系統的解是否存在

當然這三種也可能會相互結合。

差分約束系統的解法如下:

1、  根據條件把題意通過變數組表達出來得到不等式組,注意要發掘出隱含的不等式,比如說前後兩個變數之間隱含的不等式關係。

2、  進行建圖:

首先根據題目的要求進行不等式組的標準化。

(1)、如果要求取最小值,那麼求出最長路,那麼將不等式全部化成xi – xj >= k的形式,這樣建立j->i的邊,權值為k的邊,如果不等式組中有xi – xj > k,因為一般題目都是對整形變數的約束,化為xi – xj >= k+1即可,如果xi – xj = k呢,那麼可以變為如下兩個:xi – xj >= k, xi – xj <= k,進一步變為xj – xi >= -k,建立兩條邊即可。

(2)、如果求取的是最大值,那麼求取最短路,將不等式全部化成xi – xj <= k的形式, 這樣建立j->i的邊,權值為k的邊,如果像上面的兩種情況,那麼同樣地標準化就行了。

(3)、如果要判斷差分約束系統是否存在解,一般都是判斷環,選擇求最短路或者最長路求解都行,只是不等式標準化時候不同,判環地話,用spfa即可,n個點中如果同一個點入隊超過n次,那麼即存在環。

值得注意的一點是:建立的圖可能不聯通,我們只需要加入一個超級源點,比如說求取最長路時圖不聯通的話,我們只需要加入一個點S,對其他的每個點建立一條權值為0的邊圖就聯通了,然後從S點開始進行spfa判環。最短路類似。

3、  建好圖之後直接spfa或bellman-ford求解,不能用dijstra演算法,因為一般存在負邊,注意初始化的問題。

下面通過幾個題來體會下上面的解題思路:

(1)、求取最小值

1、poj 1201- Intervals

題意:給定n個區間,[ai,bi]這個區間至少選選出ci個整數,求一個集合z,滿足每個區間的要求,輸出集合z的大小。
分析:令d[i]表示0到i這個區間內至少要選出d[i]個數,那麼對於每個[ai,bi],有d[b] - d[ai-1] >= ci,同時隱含的一個條件是0 <= d[i] - d[i-1] <= 1,但是因此d[-1]不能表示,令d[i+1]示0到i這個區間內至少要選出d[i+1]個數,然後d[0] = 0,直接求取最長路就行了。邊的儲存使用鏈式向前星,這樣效率最高。


#include <cstdio>
#include <cstring>
#include <queue>
#include <cctype>
#include <algorithm>
using namespace std;
 
template<class T>
inline void read(T &res) {
    char c; res = 0;
    while (!(isdigit(c = getchar())));
    while (isdigit(c)) res = res * 10 + c - '0', c = getchar();
}
const int N = 50005;
const int INF = 0x3f3f3f3f;
int head[N], vis[N], d[N];
struct edge{
    int v, d, next;
    edge(int v = 0, int d = 0, int n = 0) : v(v), d(d), next(n){}
}e[N<<2];
int n, ma, mi = INF, k;
queue<int> q;
void add(int u, int v, int d) {
    e[k] = edge(v, d, head[u]);
    head[u] = k++;
}
int spfa() {
    fill(d+mi+1, d+ma+1, -INF);
    vis[mi] = 1;
    q.push(mi);
    while (!q.empty()) {
        int t = q.front(); q.pop();
        vis[t] = 0;
        for (int i = head[t]; i != -1; i = e[i].next) {
            int x = e[i].v;
            if (d[x] < d[t] + e[i].d) {
                d[x] = d[t] + e[i].d;
                if (!vis[x]) vis[x] = 1, q.push(x);
            }
        }
    }
    return d[ma];
}
int main() {
    int k = 0;
    memset(head, -1, sizeof(head));
    read(n);
    for (int i = 0; i < n; i++) {
        int a, b, c;
        read(a); read(b); read(c);
        add(a, b+1, c);
        ma = max(ma, b); mi = min(mi, a);
    }
    ma++;
    for (int i = mi; i < ma; i++) add(i, i+1, 0), add(i+1, i, -1);
    printf("%d\n", spfa());
    return 0;
}

2、poj1275 - Cashier Employment

題意:在一家超市裡,每個時刻都需要有營業員看管,R(i) (0 <= i < 24)表示從i時刻開始到i+1時刻結束需要的營業員的數目,現在有N(N <= 1000)個申請人申請這項工作,並且每個申請者都有一個起始工作時間 ti,如果第i個申請者被錄用,那麼他會從ti時刻開始連續工作8小時。現在要求選擇一些申請者進行錄用,使得任何一個時刻i,營業員數目都能大於等於R(i)。求出至少需要錄用多少營業員。

分析:每天的工作情況都是一樣的,我們只需要求出一天的即可。根據題意,令s[i]為一天內前i+1個小時錄用的人數,如果i>=7,則s[i] - s[i-8] >= R[i],如果0 <= i < 7,則可以推出s[23] - s[i+16] + s[i] >= R[i],同時每個時刻錄用的人數有個上限,假設第i時刻最多可以錄用的人數為b[i],則對於每一個i有0 <=s[i] - s[i-1] <= b[i]。
現在需要解決的一個問題是,第二個不等式中包含3個s組的變數,這該怎麼建圖呢?可以知道我們只需要列舉s[23],那麼這個量就是已知的了,因此不等式可以變為s[i] - s[i+16] >= R[i] - s[23],但是必須明白的一點是,既然s[23]是列舉的一天的錄用人數的最小數目,我們建圖之後求出的s[23]也應該為列舉的那個值,可以從0到n列舉s[23],第一個值便是答案,但是可以更高效地求解,因為問題具有單調性,直接二分.

因為s[-1] = 0,-1無法表示,只需要向右移動一個單位即可,那麼令s[0] = 0,思路很清晰了,二分s[24]搜尋最小值。

#include <cstdio>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
 
const int N = 30, M = 1010;
const int INF = 0x3f3f3f3f;
 
struct edge{
    int v, d, next;
    edge(int v, int d, int n):v(v), d(d), next(n){}
    edge(){}
}ed[M];
int head[N], d[N], vis[N], R[N], tim[N], k;
queue<int> q;
void init() {
    k = 0;
    memset(head, -1, sizeof(head));
    memset(d, -INF, sizeof(d));
    memset(vis, 0, sizeof(vis));
    while (!q.empty()) q.pop();
}
void add(int u, int v, int d) {
    ed[k] = edge(v, d, head[u]);
    head[u] = k++;
}
int cal(int m) {
    q.push(0); d[0] = 0;
    while (!q.empty()) {
        int x = q.front(); q.pop();
        vis[x] = 0;
        if (x == 24 && d[x] > m) return 0;
        for (int i = head[x]; i != -1; i = ed[i].next) {
            int t = ed[i].v;
            if (d[t] < d[x] + ed[i].d) {
                d[t] = d[x] + ed[i].d;
                if (!vis[t]) vis[t] = 1, q.push(t);
            }
        }
    }
    return d[24] == m ? 1 : 0;
}
int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        for (int i = 0; i < 24; i++) scanf("%d", R+i);
        int n;
        scanf("%d", &n);
        memset(tim, 0, sizeof(tim));
        for (int i = 0; i < n; i++) {
            int tmp;
            scanf("%d", &tmp);
            tim[tmp]++;
        }
        int r = n+1, l = -1;
        while (r - l > 1) {
            int m = (r + l) / 2;
            init();
            for (int i = 0; i <= 23; i++) add(i+1, i, -tim[i]), add(i, i+1, 0);
            for (int i = 7; i <= 23; i++) add(i-7, i+1, R[i]);
            add(0, 24, m); add(24, 0, -m);
            for (int i = 0; i < 7; i++) add(i+17, i+1, R[i]-m);
            cal(m) ? r = m : l = m;
        }
        l >= n ? puts("No Solution") : printf("%d\n", r);
    }
    return 0;
}

(2)、求取最大值

1、hdu3440 - House man

題意:有n棟房子,給出每棟房子的高度和開始時的相對位置,可以移動一些房子,但不能改變這些房子的相對位置,現在從最矮的房子開始,每次跳至比它高的第一棟房子, 而且每次跳躍的水平距離最大是D,房子不能在同一位置,只能在整點位置。問最矮的房子和最高的房子之間的最大距離可以是多少?如果不能從最矮的房子到達最高的房子則輸出-1.

分析:令d[i]表示第i棟房子與第一棟房子之間的最大距離,那麼我們要求的就是的的d[n],求最短路即可,首先每棟房子之間的相對位置已經確定且不能在同一位置,那麼d[i+1] > d[i],每次要跳至比它高的房子上,那麼我們需要對房子按高度排序。因為開始時已經規定標號小的點在標號大的點的左邊,這樣,我們如果從標號大的點到標號小的點,建一條這樣的邊就會有問題,只能按小到大建邊,而且如果兩個排序後相鄰房子之間的標號大於D的話則不可能到最高的房子,因為房子不可能在同一位置,他們之間的距離至少是D。約束條件只有這兩者,建邊時需要處理一下方向。最後如果最高的房子標號比矮的房子小的話,則以最高的房子為源點進行spfa,如果存在負環則輸出-1.

因此建圖不能一概而論,還是要思考前後的聯絡。建圖也算是圖論題的一個重要部分吧。

#include <bits/stdc++.h>
using namespace std;
 
const int N = 1010, M = 10000;
const int INF = 0x3f3f3f3f;
 
struct house{
    int he, id;
    bool operator < (const house& x)const { return he < x.he; }
}h[N];
struct edge{
    int v, d, next;
    edge(int v, int d, int n):v(v), d(d), next(n){}
    edge(){}
}ed[M];
int head[N], d[N], vis[N], cnt[N];
int n, s, e, k;
queue<int> q;
void init() {
    k = 0;
    memset(head, -1, sizeof(int) * n);
    memset(d, INF, sizeof(int) * n);
    memset(vis, 0, sizeof(int) * n);
    memset(cnt, 0, sizeof(int) * n);
    for (int i = 0;  i < n; i++) h[i].id = i;
    while (!q.empty()) q.pop();
}
void add(int u, int v, int d) {
    ed[k] = edge(v, d, head[u]);
    head[u] = k++;
}
int spfa() {
    d[s] = 0; cnt[s]++;
    q.push(s);
    while (!q.empty()) {
        int x = q.front(); q.pop();
        vis[x] = 0;
        for (int i = head[x]; i != -1; i = ed[i].next) {
            int t = ed[i].v;
            if (d[t] > d[x] + ed[i].d) {
                d[t] = d[x] + ed[i].d;
                if (!vis[t]) {
                    vis[t] = 1; q.push(t);
                    if (++cnt[t] > n) return -1;
                }
            }
        }
    }
    return d[e];
}
int main() {
    int t, ca = 0;
    scanf("%d", &t);
    while (t--) {
        int d;
        scanf("%d %d", &n, &d);
        init();
        for (int i = 0; i < n; i++) scanf("%d", &h[i].he);
        sort(h, h+n);
        int flag = 1;
        for (int i = 0; i < n-1 && flag; i++) {
            add(i+1, i, -1);
            int u = min(h[i].id, h[i+1].id), v = max(h[i].id, h[i+1].id);
            if (v - u > d) flag = 0;
            add(u, v, d);
        }
        s = min(h[0].id, h[n-1].id), e = max(h[0].id, h[n-1].id);
        printf("Case %d: %d\n", ++ca, flag ? spfa() : -1);
    }
    return 0;
}

2、poj3169 - Layout

題意:有n頭牛,他們按順序排成了一排,有些牛關係比較好,他們的距離不能超過某個距離,還有些牛關係不好,他們之間的距離不能小於某個距離,可能會有多頭牛擠在同一位置上,問1號牛和n號牛之間的最大距離是多少,如果不存在滿足條件的排列則輸出-1,如果距離無限大則輸出-2.

分析:令d[i]表示第i頭牛的位置,因為牛按順序排列,則有d[i] <= d[i+1],關係好的牛有d[a] +D >= d[b], 關係不好的牛有d[a] + d <= d[b],把不等式標準化之後直接跑最短路即可。

如果無限大則輸出-2,不存在排列輸出-1.

#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
 
const int N = 1010, M = 100010;
const int INF = 0x3f3f3f3f;
struct edge{ int v, d; };
vector<edge> G[N];
queue<int> q;
int d[N], vis[N], cnt[N];
 
int spfa(int n) {
    fill(d + 2, d + 1 + n, INF);
    q.push(1);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        vis[u] = 0;
        for (int i = 0; i < G[u].size(); i++) {
            edge e = G[u][i];
            int v = e.v;
            if (d[u] + e.d < d[v]) {
                d[v] = d[u] + e.d;
                if(!vis[v]) {
                    if(++cnt[v] > n) return -1;
                    q.push(v); vis[v] = 1;
                }
            }
        }
    }
    return d[n] == INF ? -2 : d[n];
}
int main() {
    int n, ml, md, u, v, d;
    scanf("%d %d %d", &n, &ml, &md);
    for(int i = 0; i < ml; i++) {
        scanf("%d %d %d", &u, &v, &d);
        G[u].push_back((edge){v, d});
    }
    for(int i = 0; i < md; i++) {
        scanf("%d %d %d", &u, &v, &d);
        G[v].push_back((edge){u, -d});
    }
    for(int i = 1; i < n; i++) G[i+1].push_back((edge){i, 0});
    printf("%d\n", spfa(n));
    return 0;
}

(3)、判斷解是否存在

1、poj1364 - KIng

題意: 給定一個序列a1, a2, ..., an,給出它的一些子序列以及對該子序列的約束條件,例如asi, asi+1, asi+2, ..., asi+ni,且asi+asi+1+asi+2+......+asi+ni 小於或者大於 ki。求是否存在滿足以上要求的數列。是 則輸出“lamentable kingdom”,否則輸出“successful conspiracy”。

分析:判斷解是否存在只要判正環就行了,很明顯,題目需要從區間和下手,令S[i]為1到i這i個元素的和,那麼根據題意建立不等式然後建圖,s[si+ni] - s[si-1] > k,或者s[si+ni] - s[si-1] < k最後加入一個源點,然後對每個點i建立一條權為0的邊.如果不存在環則解一定存在。當然求最短路判負環也可以。
#include <cstdio>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
 
const int N = 110, M = 1000;
const int INF = 0x3f3f3f3f;
 
struct edge{
    int v, d, next;
    edge(int v, int d, int n):v(v), d(d), next(n){}
    edge(){}
}ed[M];
int head[N], d[N], vis[N], cnt[N], k;
queue<int> q;
void init() {
    k = 0;
    memset(head, -1, sizeof(head));
    memset(d, INF, sizeof(d));
    memset(vis, 0, sizeof(vis));
    memset(cnt, 0, sizeof(cnt));
    while (!q.empty()) q.pop();
}
void add(int u, int v, int d) {
    ed[k] = edge(v, d, head[u]);
    head[u] = k++;
}
int spfa(int n) {
    d[n+1] = 0; cnt[n+1]++;
    q.push(n+1);
    while (!q.empty()) {
        int x = q.front(); q.pop();
        vis[x] = 0;
        for (int i = head[x]; i != -1; i = ed[i].next) {
            int t = ed[i].v;
            if (d[t] > d[x] + ed[i].d) {
                d[t] = d[x] + ed[i].d;
                if (!vis[t]) {
                    vis[t] = 1; q.push(t);
                    if (++cnt[t] > n+2) return 0;
                }
            }
        }
    }
    return 1;
}
int main() {
    int n, m;
    while (scanf("%d", &n), n) {
        init();
        scanf("%d", &m);
        for (int i = 0; i < m; i++) {
            int si, ni, ki;
            char o[100];
            scanf("%d %d %s %d", &si, &ni, o, &ki);
            o[0] == 'g' ? add(si+ni, si-1, -ki-1) : add(si-1, si+ni, ki-1);
        }
        for (int i = 0; i <= n; i++) add(n+1, i, 0);
        puts(spfa(n) ? "lamentable kingdom" : "successful conspiracy");
    }
    return 0;
}
poj2983 - Is the Information Reliable?
題意:銀河系中有n個防禦站排成一條直線,給定m條資訊,每條資訊的格式如下。
P A B X代表A站在B站北方X光年處。
V A B 代表A站在B站北方,而且距離至少為1光年。
問是否存在這樣的一個防禦戰排列滿足上述要求,是輸出Reliable,否輸出 Unreliable。
分析:同上題一樣,令d[i]表示第i號防禦站的位置,根據資訊建立不等式,然後根據選擇的判環方式建邊,然後加入源點s,對每個點建立一條長為0的邊。直接spfa判環即可。
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>
#include <cctype>
using namespace std;
 
const int N = 1005;
const int M = 100005;
const int INF = 0x3f3f3f3f;
struct edge{
    int v, d, next;
    edge(int v, int d, int n) : v(v), d(d), next(n){}
    edge(){}
}e[2*M + N];
int head[N], d[N], vis[N], cnt[N];
int n, m, k;
queue<int> q;
 
template<class T>
inline int read(T &res) {
    char c; res = 0;
    while (!(isdigit(c = getchar())));
    while (isdigit(c)) res = res * 10 + c - '0', c = getchar();
    return 1;
}
void add(int u, int v, int d) { e[k] = edge(v, d, head[u]); head[u] = k++; }
void init(int n) {
    k = 0;
    memset(cnt, 0, sizeof(int) * (n+3));
    memset(vis, 0, sizeof(int) * (n+3));
    memset(head, -1, sizeof(int) * (n+3));
    while (!q.empty()) q.pop();
    fill(d+1, d+1+n, -INF);
}
bool spfa() {
    vis[0] = 1;
    q.push(0); cnt[0]++;
    while (!q.empty()) {
        int x = q.front(); q.pop();
        vis[x] = 0;
        for (int i = head[x]; i != -1; i = e[i].next) {
            int t = e[i].v;
            if (d[t] < d[x] + e[i].d) {
                d[t] = d[x] + e[i].d;
                if (!vis[t]) {
                    vis[t] = 1; q.push(t);
                    if (++cnt[t] > n) return 0;
                }
            }
        }
    }
    return 1;
}
int main() {
    while (~scanf("%d %d", &n, &m)) {
        char s[10];
        init(n);
        for (int i = 0; i < m; i++) {
            int a, b, x = 1;
            scanf("%s", s);
            read(a); read(b);
            if (s[0] == 'P') read(x);
            add(a, b, x);
            if (s[0] == 'P') add(b, a, -x);
        }
        for (int i = 1; i <= n; i++) add(0, i, 0);
        puts(spfa() ? "Reliable" : "Unreliable");
    }
    return 0;
}
關於差分約束的更詳細的講解可以參考下面這位大牛的部落格,而且還有很多練習題。
http://www.cppblog.com/menjitianya/archive/2015/11/19/212292.html

最短路+差分約束題集(轉自上面部落格)

差分約束題集整理
 

    最短路

                                ★★☆☆☆     單源最短路 + 揹包

                                  ★★☆☆☆     二分列舉 + 最短路

                                 ★★☆☆☆     二分列舉 + 最短路

                                   ★★★☆☆     兩條最短路的相交點個數為P,要求最大化P

                                   ★★★☆☆     最短路徑樹思想

差分約束

      Layout                             ★★☆☆☆     差分約束系統 - 最短路模型 + 判負環

      World Exhibition                   ★★☆☆☆     差分約束系統 - 最短路模型 + 判負環

      House Man                          ★★☆☆☆     差分約束系統 - 最短路模型 + 判負環

                                ★★☆☆☆     差分約束系統 - 最長路模型 邊儲存用鏈式前向星

      King                               ★★☆☆☆     差分約束系統 - 最長路模型 + 判正環

      XYZZY                              ★★☆☆☆     最長路 + 判正環

      Advertisement                      ★★★☆☆     限制較強的差分約束 - 可以貪心求解