夜深人靜寫算法(四)- 最短路和差分約束

目錄  

一、引例
      1、一類不等式組的解

二、最短路
      1、Dijkstra
      2、圖的存儲
      3、鏈式前向星
      4、Dijkstra + 優先隊列
      5、Bellman-Ford
      6、SPFA
      7、Floyd-Warshall

三、差分約束
       1、數形結合
       2、三角不等式
       3、解的存在性
       4、最大值 => 最小值
       5、不等式標準化

四、差分約束的經典應用
       1、線性約束
      2、區間約束
       3、未知條件約束
         
五、差分約束題集整理

一、引例
       1、一類不等式組的解
      給定n個變量和m個不等式,每個不等式形如 x[i] - x[j] <= a[k] (0 <= i, j < n, 0 <= k < m, a[k]已知),求 x[n-1] - x[0] 的最大值。例如當n = 4,m = 5,不等式組如圖一-1-1所示的情況,求x3 - x0的最大值。
圖一-1-1
      觀察x3 - x0的性質,我們如果可以通過不等式的兩兩加和得到c個形如 x3 - x0 <= Ti 的不等式,那麼 min{ Ti | 0 <= i < c } 就是我們要求的x3 - x0的最大值。於是開始人肉,費盡千辛萬苦,終於整理出以下三個不等式:
      1.      (3)                       x3 - x0 <= 8
      2.      (2) + (5)              x3 - x0 <= 9
      3.      (1) + (4) + (5)     x3 - x0 <= 7
      這裏的T等於{8, 9, 7},所以min{ T } = 7,答案就是7。的確是7嗎?我們再仔細看看,發現的確沒有其它情況了。那麼問題就是這種方法即使做出來了還是帶有問號的,不能確定正確與否,如何系統地解決這類問題呢?
      讓我們來看另一個問題,這個問題描述相對簡單,給定四個小島以及小島之間的有向距離,問從第0個島到第3個島的最短距離。如圖一-1-2所示,箭頭指向的線段代表兩個小島之間的有向邊,藍色數字代表距離權值。

圖一-1-2
      這個問題就是經典的最短路問題。由於這個圖比較簡單,我們可以枚舉所有的路,發現總共三條路,如下:
      1.       0 -> 3                       長度爲8
      2.       0 -> 2 -> 3               長度爲7+2 = 9
      3.       0 -> 1 -> 2 -> 3       長度爲2 + 3 + 2 = 7
      最短路爲三條線路中的長度的最小值即7,所以最短路的長度就是7。這和上面的不等式有什麼關係呢?還是先來看看最短路求解的原理,看懂原理自然就能想到兩者的聯繫了。

二、最短路
      1、Dijkstra
      對於一個有向圖或無向圖,所有邊權爲正(邊用鄰接矩陣的形式給出),給定a和b,求a到b的最短路,保證a一定能夠到達b。這條最短路是否一定存在呢?答案是肯定的。相反,最長路就不一定了,由於邊權爲正,如果遇到有環的時候,可以一直在這個環上走,因爲要找最長的,這樣就使得路徑越變越長,永無止境,所以對於正權圖,在可達的情況下最短路一定存在,最長路則不一定存在。這裏先討論正權圖的最短路問題。
      最短路滿足最優子結構性質,所以是一個動態規劃問題。最短路的最優子結構可以描述爲:
      D(s, t) = {Vs ... Vi ... Vj ... Vt}表示s到t的最短路,其中i和j是這條路徑上的兩個中間結點,那麼D(i, j)必定是i到j的最短路,這個性質是顯然的,可以用反證法證明。
      基於上面的最優子結構性質,如果存在這樣一條最短路D(s, t) = {Vs ... Vi Vt},其中i和t是最短路上相鄰的點,那麼D(s, i) = {Vs ... Vi} 必定是s到i的最短路。Dijkstra算法就是基於這樣一個性質,通過最短路徑長度遞增,逐漸生成最短路。
      Dijkstra算法是最經典的最短路算法,用於計算正權圖的單源最短路(Single Source Shortest Path,源點給定,通過該算法可以求出起點到所有點的最短路),它是基於這樣一個事實:如果源點到x點的最短路已經求出,並且保存在d[x] ( 可以將它理解爲D(s, x) )上,那麼可以利用x去更新 x能夠直接到達的點 的最短路。即:
      d[y] = min{ d[y], d[x] + w(x, y) }           y爲x能夠直接到達的點,w(x, y) 則表示x->y這條有向邊的邊權
      具體算法描述如下:對於圖G = <V, E>,源點爲s,d[i]表示s到i的最短路,visit[i]表示d[i]是否已經確定(布爾值)。
      1) 初始化 所有頂點 d[i] = INF, visit[i] = false,令d[s] = 0;
      2) 從所有visit[i]爲false的頂點中找到一個d[i]值最小的,令x = i; 如果找不到,算法結束;
      3) 標記visit[x] = true, 更新和x直接相鄰的所有頂點y的最短路: d[y] = min{ d[y], d[x] + w(x, y) }
     (第三步中如果y和x並不是直接相鄰,則令w(x, y) = INF)
      
      2、圖的存儲
     以上算法的時間複雜度爲O(n^2),n爲結點個數,即每次找一個d[i]值最小的,總共n次,每次找到後對其它所有頂點進行更新,更新n次。由於算法複雜度是和點有關,並且平方級別的,所以還是需要考慮一下點數較多而邊數較少的情況,接下來以圖一-2-1爲例討論一下邊的存儲方式。
圖一-2-1
      鄰接矩陣是直接利用一個二維數組對邊的關係進行存儲,矩陣的第i行第j列的值 表示 i -> j 這條邊的權值;特殊的,如果不存在這條邊,用一個特殊標記來表示;如果i == j,則權值爲0。它的優點是實現非常簡單,而且很容易理解;缺點也很明顯,如果這個圖是一個非常稀疏的圖,圖中邊很少,但是點很多,就會造成非常大的內存浪費,點數過大的時候根本就無法存儲。圖一-2-2展示了圖一-2-1的鄰接矩陣表示法。
圖一-2-2
      鄰接表是圖中常用的存儲結構之一,每個頂點都有一個鏈表,這個鏈表的數據表示和當前頂點直接相鄰的頂點(如果邊有權值,還需要保存邊權信息)。鄰接表的優點是對於稀疏圖不會有數據浪費,缺點就是實現相對麻煩,需要自己實現鏈表,動態分配內存。圖一-2-3展示了圖一-2-1的鄰接表表示法。
圖一-2-3
      前向星是以存儲邊的方式來存儲圖,先將邊讀入並存儲在連續的數組中,然後按照邊的起點進行排序,這樣數組中起點相等的邊就能夠在數組中進行連續訪問了。它的優點是實現簡單,容易理解,缺點是需要在所有邊都讀入完畢的情況下對所有邊進行一次排序,帶來了時間開銷,實用性也較差,只適合離線算法。圖一-2-4展示了圖一-2-1的前向星表示法。
圖二-2-4
     那麼用哪種數據結構才能滿足所有圖的需求呢?這裏介紹一種新的數據結構一一鏈式前向星。
      3、鏈式前向星
      鏈式前向星和鄰接表類似,也是鏈式結構和線性結構的結合,每個結點i都有一個鏈表,鏈表的所有數據是從i出發的所有邊的集合(對比鄰接表存的是頂點集合),邊的表示爲一個四元組(u, v, w, next),其中(u, v)代表該條邊的有向頂點對,w代表邊上的權值,next指向下一條邊。
      具體的,我們需要一個邊的結構體數組 edge[MAXM],MAXM表示邊的總數,所有邊都存儲在這個結構體數組中,並且用head[i]來指向 i 結點的第一條邊。
       邊的結構體聲明如下:
    struct EDGE {
                    int u, v, w, next;
        EDGE() {}
        EDGE(int _u, int _v, int _w, int _next) {
            u = _u, v = _v, w = _w, next = _next;
        }
    }edge[MAXM];
       初始化所有的head[i] = INF,當前邊總數 edgeCount = 0
       每讀入一條邊,調用addEdge(u, v, w),具體函數的實現如下:
    void addEdge(int u, int v, int w) {
        edge[ edgeCount ] = EDGE(u, v, w, head[u]);
        head[u] = edgeCount ++;
    }
       這個函數的含義是每加入一條邊(u, v),就在原有的鏈表結構的首部插入這條邊,使得每次插入的時間複雜度爲O(1),所以鏈表的邊的順序和讀入順序正好是逆序的。這種結構在無論是稠密的還是稀疏的圖上都有非常好的表現,空間上沒有浪費,時間上也是最小開銷。
       調用的時候只要通過head[i]就能訪問到由 i 出發的第一條邊的編號,通過編號到edge數組進行索引可以得到邊的具體信息,然後根據這條邊的next域可以得到第二條邊的編號,以此類推,直到next域爲INF(這裏的INF即head數組初始化的那個值,一般取-1即可)。

      4Dijkstra + 優先隊列(小頂堆)
      有了鏈式前向星,再來看Dijkstra算法,我們關注算法的第3)步,對和x直接相鄰的點進行更新的時候,不再需要遍歷所有的點,而是隻更新和x直接相鄰的點,這樣總的更新次數就和頂點數n無關了,總更新次數就是總邊數m,算法的複雜度變成了O(n^2 + m),之前的複雜度是O(n^2),但是有兩個n^2的操作,而這裏是一個,原因在於找d值最小的頂點的時候還是一個O(n)的輪詢,總共n次查找。那麼查找d值最小有什麼好辦法呢?
      數據結構中有一種樹,它能夠在O( log(n) )的時間內插入和刪除數據,並且在O(1)的時間內得到當前數據的最小值,這個和我們的需求不謀而合,它就是最小二叉堆(小頂堆),具體實現不講了,比較簡單,可以自行百度。
      在C++中,可以利用STL的優先隊列( priority_queue )來實現獲取最小值的操作,這裏直接給出利用優先隊列優化的Dijkstra算法的類C++僞代碼(請勿直接複製粘貼到C++編譯器中編譯執行),然後再進行討論:
    void Dijkstra_Heap(s) {
                    for(i = 0; i < n; i++) {   
            d[i] = (i == s) ? 0 : INF;  // 註釋1
        }
        q.push( (d[s], s) );            // 註釋2
                    while!q.empty() ) {
            (dist, u) = q.top();        // 註釋3
            q.pop();                    // 註釋4
                              for (e = head[u]; e != INF; e = edge[e].next) {
                v = edge[e].v;
                w = edge[e].w;
                                        if(d[u] + w < d[v]) {
                    d[v] = d[u] + w;
                    path[v] = u;
                    q.push( (d[v], v) );
                }
            }
        }
    }
   註釋1:初始化s到i的初始最短距離,d[s] = 0
   註釋2:q即優先隊列,這裏略去聲明是爲了將代碼簡化,讓讀者能夠關注算法本身而不是關注具體實現,   push是執行優先隊列的插入操作,插入的數據爲一個二元組(d[u], u)
   註釋3:執行優先隊列的獲取操作,獲取的二元組爲當前隊列中d值最小的
   註釋4:執行優先隊列的刪除操作,刪除隊列頂部的元素(即註釋3中d值最小的那個二元組)
      以上僞代碼中的主體部分竟然沒有任何註釋,這是因爲我要用黑色的字來描述它的重要性,而註釋只是註釋一些和語法相關的內容。
      主體代碼只有一個循環,這個循環就是遍歷了u這個結點的邊鏈表,其中e爲邊編號,edge[e].w即上文提到的w(u, v),即u ->v 這條邊的權值,而d[u] + w(u, v) < d[v]表示從起點s到u,再經過(u, v)這條邊到達v的最短路比之前其它方式到達v的最短路還短,如圖二-4-1所示,如果滿足這個條件,那麼就更新這條最短路,並且利用path數組來記錄最短路中每個結點的前驅結點,path[v] = u,表示到達v的最短路的前驅結點爲u。
圖二-4-1
      補充一點,這個算法求出的是一棵最短路徑樹,其中s爲根結點,結點之間的關係是通過path數組來建立的,path[v] = u,表明u爲v的父結點(樹的存儲不一定要存兒子結點,也可以用存父結點的方式表示)。
      考慮這個算法的複雜度,如果用n表示點數,m表示邊數,那麼優先隊列中最多可能存在的點數有多少?因爲我們在把頂點插入隊列的時候並沒有判斷隊列中有沒有這個點,而且也不能進行這樣的判斷,因爲新插入的點一定會取代之前的點(距離更短纔會執行插入),所以同一時間隊列中的點有可能重複,插入操作的上限是m次,所以最多有m個點,那麼一次插入和刪除的操作的平攤複雜度就是O(logm),但是每次取距離最小的點,對於有多個相同點的情況,如果那個點已經出過一次隊列了,下次同一個點出隊列的時候它對應的距離一定比之前的大,不需要用它去更新其它點,因爲一定不可能更新成功,所以真正執行更新操作的點的個數其實只有n個,所以總體下來的平均複雜度爲O( (m+n)log m),而這個只是理論上界,一般問題中都是很快就能找到最短路的,所以實際複雜度會比這個小很多,相比O(n^2)的算法已經優化了很多了。
      Dijkstra算法求的是正權圖的單源最短路問題,對於權值有負數的情況就不能用Dijkstra求解了,因爲如果圖中存在負環,Dijkstra帶優先隊列優化的算法就會進入一個死循環,因爲可以從起點走到負環處一直將權值變小 。對於帶負權的圖的最短路問題就需要用到Bellman-Ford算法了。

      5Bellman-Ford
      Bellman-Ford算法可以在最短路存在的情況下求出最短路,並且在存在負權圈的情況下告訴你最短路不存在,前提是起點能夠到達這個負權圈,因爲即使圖中有負權圈,但是起點到不了負權圈,最短路還是有可能存在的。它是基於這樣一個事實:一個圖的最短路如果存在,那麼最短路中必定不存在圈,所以最短路的頂點數除了起點外最多隻有n-1個。
      Bellman-Ford同樣也是利用了最短路的最優子結構性質,用d[i]表示起點s到i的最短路,那麼邊數上限爲 j 的最短路可以通過邊數上限爲 j-1 的最短路 加入一條邊 得到,通過n-1次迭代,最後求得s到所有點的最短路。
      具體算法描述如下:對於圖G = <V, E>,源點爲s,d[i]表示s到i的最短路。
   1) 初始化 所有頂點 d[i] = INF, 令d[s] = 0,計數器 j = 0;
      2) 枚舉每條邊(u, v),如果d[u]不等於INF並且 d[u] + w(u, v) < d[v],則令d[v] = d[u] + w(u, v);
      3) 計數器j + +,當j = n - 1時算法結束,否則繼續重複2)的步驟; 
      第2)步的一次更新稱爲邊的“鬆弛”操作。
      以上算法並沒有考慮到負權圈的問題,如果存在負圈權,那麼第2)步操作的更新會永無止境,所以判定負權圈的算法也就出來了,只需要在第n次繼續進行第2)步的鬆弛操作,如果有至少一條邊能夠被更新,那麼必定存在負權圈。
      這個算法的時間複雜度爲O(nm),n爲點數,m爲邊數。
      這裏有一個小優化,我們可以注意到第2)步操作,每次迭代第2)步操作都是做同一件事情,也就是說如果第k(k <= n-1)次迭代的時候沒有任何的最短路發生更新,即所有的d[i]值都未發生變化,那麼第k+1次必定也不會發生變化了,也就是說這個算法提前結束了。所以可以在第2)操作開始的時候記錄一個標誌,標誌初始爲false,如果有一條邊發生了鬆弛,那麼標誌置爲true,所有邊枚舉完畢如果標誌還是false則提前結束算法。
      這個優化在一般情況下很有效,因爲往往最短路在前幾次迭代就已經找到最優解了,但是也不排除上文提到的負權圈的情況,會一直更新,使得整個算法的時間複雜度達到上限O(nm),那麼如何改善這個算法的效率呢?接下來介紹改進版的Bellman-Ford 一一 SPFA。

      6SPFA
      SPFA( Shortest Path Faster Algorithm )是基於Bellman-Ford的思想,採用先進先出(FIFO)隊列進行優化的一個計算單源最短路的快速算法。
      類似Bellman-Ford的做法,我們用數組d記錄每個結點的最短路徑估計值,並用鏈式前向星來存儲圖G。利用一個先進先出的隊列用來保存待鬆弛的結點,每次取出隊首結點u,並且枚舉從u出發的所有邊(u, v),如果d[u] + w(u, v) < d[v],則更新d[v] = d[u] + w(u, v),然後判斷v點在不在隊列中,如果不在就將v點放入隊尾。這樣不斷從隊列中取出結點來進行鬆弛操作,直至隊列空爲止。 
      只要最短路徑存在,SPFA算法必定能求出最小值。因爲每次將點放入隊尾,都是經過鬆弛操作達到的。即每次入隊的點v對應的最短路徑估計值d[v]都在變小。所以算法的執行會使d越來越小。由於我們假定最短路一定存在,即圖中沒有負權圈,所以每個結點都有最短路徑值。因此,算法不會無限執行下去,隨着d值的逐漸變小,直到到達最短路徑值時,算法結束,這時的最短路徑估計值就是對應結點的最短路徑值。
      那麼最短路徑不存在呢?如果存在負權圈,並且起點可以通過一些頂點到達負權圈,那麼利用SPFA算法會進入一個死循環,因爲d值會越來越小,並且沒有下限,使得最短路不存在。那麼我們假設不存在負權圈,則任何最短路上的點必定小於等於n個(沒有圈),換言之,用一個數組c[i]來記錄i這個點入隊的次數,所有的c[i]必定都小於等於n,所以一旦有一個c[i] > n,則表明這個圖中存在負權圈。
       接下來給出SPFA更加直觀的理解,假設圖中所有邊的邊權都爲1,那麼SPFA其實就是一個BFS(Breadth First Search,廣度優先搜索),對於BFS的介紹可以參閱搜索入門。BFS首先到達的頂點所經歷的路徑一定是最短路(也就是經過的最少頂點數),所以此時利用數組記錄節點訪問可以使每個頂點只進隊一次,但在至少有一條邊的邊權不爲1的帶權圖中,最先到達的頂點的路徑不一定是最短路,這就是爲什麼要用d數組來記錄當前最短路估計值的原因了。
       最後給出SPFA的類C++僞代碼(請勿直接複製粘貼到C++編譯器中編譯執行):

    bool spfa(s) {
                    for(i = 0; i < n; i++) {
            d[i] = (i == s) ? 0 : INF;
            inq[i] = (i == s);                  // 註釋1
            visitCount[i] = 0;
        }
        q.push( (d[s], s) );
                    while!q.empty() ) { 
            (dist, u) = q.front();              // 註釋2
            q.pop();
            inq[u] = false;
                              if( visitCount[u]++ > n ) {         // 註釋3
                                           return true;
            }
                              for (e = head[u]; e != INF; e = edge[e].next) {
                v = edge[e].v;
                w = edge[e].w;
                                        if(d[u] + w < d[v]) {           // 註釋4
                    d[v] = d[u] + w;
                                                  if ( !inq[v] ) {
                        inq[v] = true;
                        q.push( (d[v], v) );
                    }
                }
            }
        }
                    return false;
    }

   註釋1:inq[i]表示結點i是否在隊列中,初始時只有s在隊列中;
   註釋2:q.front()爲FIFO隊列的隊列首元素;
   註釋3:判斷是否存在負權圈,如果存在,函數返回true;
   註釋4:和Dijkstra優先隊列優化的算法很相似的鬆弛操作;
      以上僞代碼實現的SPFA算法的最壞時間複雜度爲O(nm),其中n爲點數,m爲邊數,但是一般不會達到這個上界,一般的期望時間複雜度爲O(km), k爲常數,m爲邊數(這個時間複雜度只是估計值,具體和圖的結構有很大關係,而且很難證明,不過可以肯定的是至少比傳統的Bellman-Ford高效很多,所以一般採用SPFA來求解帶負權圈的最短路問題)。

      7、Floyd-Warshall
      最後介紹一個求任意兩點最短路的算法,很顯然,我們可以求n次單源最短路(枚舉起點),但是下面這種方法更加容易編碼,而且很巧妙,它也是基於動態規劃的思想。
      令d[i][j][k]爲只允許經過結點[0, k]的情況下,i 到 j的最短路。那麼利用最優子結構性質,有兩種情況:
      a. 如果最短路經過k點,則d[i][j][k] = d[i][k][k-1] + d[k][j][k-1];
      b. 如果最短路不經過k點,則d[i][j][k] = d[i][j][k-1];
      於是有狀態轉移方程: d[i][j][k] = min{ d[i][j][k-1], d[i][k][k-1] + d[k][j][k-1] }  (0 <= i, j, k < n)
      這是一個3D/0D問題,只需要按照k遞增的順序進行枚舉,就能在O(n^3)的時間內求解,又第三維的狀態可以採用滾動數組進行優化,所以空間複雜度爲O(n^2)。
三、差分約束
      1、數形結合
      介紹完最短路,回到之前提到的那個不等式組的問題上來,我們將它更加系統化。
      如若一個系統由n個變量和m個不等式組成,並且這m個不等式對應的係數矩陣中每一行有且僅有一個1和-1,其它的都爲0,這樣的系統稱爲差分約束( difference constraints )系統。引例中的不等式組可以表示成如圖三-1-1的係數矩陣。
圖三-1-1
      然後繼續回到單個不等式上來,觀察 x[i] - x[j] <= a[k], 將這個不等式稍稍變形,將x[j]移到不等式右邊,則有x[i] <= x[j] + a[k],然後我們令a[k] = w(j, i),再將不等式中的i和j變量替換掉,i = v, j = u,將x數組的名字改成d(以上都是等價變換,不會改變原有不等式的性質),則原先的不等式變成了以下形式:d[u] + w(u, v) >= d[v]。
      這時候聯想到SPFA中的一個鬆弛操作:
    if(d[u] + w(u, v) < d[v]) {
        d[v] = d[u] + w(u, v);
    }
      對比上面的不等式,兩個不等式的不等號正好相反,但是再仔細一想,其實它們的邏輯是一致的,因爲SPFA的鬆弛操作是在滿足小於的情況下進行鬆弛,力求達到d[u] + w(u, v) >= d[v],而我們之前令a[k] = w(j, i),所以我們可以將每個不等式轉化成圖上的有向邊:
      對於每個不等式 x[i] - x[j] <= a[k],對結點 j 和 i 建立一條 j -> i的有向邊,邊權爲a[k],求x[n-1] - x[0] 的最大值就是求 0 到n-1的最短路。
圖三-1-2
      圖三-1-2 展示了 圖三-1-1的不等式組轉化後的圖。

      2、三角不等式
      如果還沒有完全理解,我們可以先來看一個簡單的情況,如下三個不等式:
B - A <= c      (1)
C - B <= a      (2)
C - A <= b      (3)
      我們想要知道C - A的最大值,通過(1) + (2),可以得到 C - A <= a + c,所以這個問題其實就是求min{b, a+c}。將上面的三個不等式按照 -1 數形結合 中提到的方式建圖,如圖三-2-1所示。
圖三-2-1
      我們發現min{b, a+c}正好對應了A到C的最短路,而這三個不等式就是著名的三角不等式。將三個不等式推廣到m個,變量推廣到n個,就變成了n個點m條邊的最短路問題了。

      3、解的存在性
      上文提到最短路的時候,會出現負權圈或者根本就不可達的情況,所以在不等式組轉化的圖上也有可能出現上述情況,先來看負權圈的情況,如圖三-3-1,下圖爲5個變量5個不等式轉化後的圖,需要求得是X[t] - X[s]的最大值,可以轉化成求s到t的最短路,但是路徑中出現負權圈,則表示最短路無限小,即不存在最短路,那麼在不等式上的表現即X[t] - X[s] <= T中的T無限小,得出的結論就是 X[t] - X[s]的最大值 不存在。
圖三-3-1
      再來看另一種情況,即從起點s無法到達t的情況,如圖三-3-2,表明X[t]和X[s]之間並沒有約束關係,這種情況下X[t] - X[s]的最大值是無限大,這就表明了X[t]和X[s]的取值有無限多種
圖三-3-2
      在實際問題中這兩種情況會讓你給出不同的輸出。綜上所述,差分約束系統的解有三種情況:1、有解;2、無解;3、無限多解

      4、最大值 => 最小值
      然後,我們將問題進行一個簡單的轉化,將原先的"<="變成">=",轉化後的不等式如下:
B - A >= c      (1)
C - B >= a      (2)
C - A >= b      (3)
      然後求C - A的最小值,類比之前的方法,需要求的其實是max{b, c+a},於是對應的是圖三-2-1從A到C的最長路。同樣可以推廣到n個變量m個不等式的情況。
      
      5、不等式標準化
      如果給出的不等式有"<="也有">=",又該如何解決呢?很明顯,首先需要關注最後的問題是什麼,如果需要求的是兩個變量差的最大值,那麼需要將所有不等式轉變成"<="的形式,建圖後求最短路;相反,如果需要求的是兩個變量差的最小值,那麼需要將所有不等式轉化成">=",建圖後求最長路。
      如果有形如:A - B = c 這樣的等式呢?我們可以將它轉化成以下兩個不等式:
A - B >= c      (1)
A - B <= c      (2)
       再通過上面的方法將其中一種不等號反向,建圖即可。
       最後,如果這些變量都是整數域上的,那麼遇到A - B < c這樣的不帶等號的不等式,我們需要將它轉化成"<="或者">="的形式,即 A - B <= c - 1。

四、差分約束的經典應用
      1、線性約束
        線性約束一般是在一維空間中給出一些變量(一般定義位置),然後告訴你某兩個變量的約束關係,求兩個變量a和b的差值的最大值或最小值。
     【例題1】N個人編號爲1-N,並且按照編號順序排成一條直線,任何兩個人的位置不重合,然後給定一些約束條件。
       X(X <= 100000)組約束Ax Bx Cx(1 <= Ax < Bx <= N),表示Ax和Bx的距離不能大於Cx。
       Y(X <= 100000)組約束Ay By Cy(1 <= Ay < By <= N),表示Ay和By的距離不能小於Cy。
       如果這樣的排列存在,輸出1-N這兩個人的最長可能距離,如果不存在,輸出-1,如果無限長輸出-2。
      像這類問題,N個人的位置在一條直線上呈線性排列,某兩個人的位置滿足某些約束條件,最後要求第一個人和最後一個人的最長可能距離,這種是最直白的差分約束問題,因爲可以用距離作爲變量列出不等式組,然後再轉化成圖求最短路。
      令第x個人的位置爲d[x](不妨設d[x]爲x的遞增函數,即隨着x的增大,d[x]的位置朝着x正方向延伸)。
      那麼我們可以列出一些約束條件如下:
      1、對於所有的Ax Bx Cx,有 d[Bx] - d[Ax] <= Cx;
      2、對於所有的Ay By Cy,有 d[By] - d[Ay] >= Cy;
      3、然後根據我們的設定,有 d[x] >= d[x-1] + 1 (1 < x <= N)  (這個條件是表示任何兩個人的位置不重合)
     而我們需要求的是d[N] - d[1]的最大值,即表示成d[N] - d[1] <= T,要求的就是這個T。
     於是我們將所有的不等式都轉化成d[x] - d[y] <= z的形式,如下:
      1、d[Bx]  -  d[Ax]    <=    Cx
      2、d[Ay]  -  d[By]    <=  -Cy
      3、d[x-1] -    d[x]    <=    -1
     對於d[x] - d[y] <= z,令z = w(y, x),那麼有 d[x] <= d[y] + w(y, x),所以當d[x] > d[y] + w(y, x),我們需要更新d[x]的值,這對應了最短路的鬆弛操作,於是問題轉化成了求1到N的最短路。
       對於所有滿足d[x] - d[y] <= z的不等式,從y向x建立一條權值爲z的有向邊。
      然後從起點1出發,利用SPFA求到各個點的最短路,如果1到N不可達,說明最短路(即上文中的T)無限長,輸出-2。如果某個點進入隊列大於等於N次,則必定存在一條負環,即沒有最短路,輸出-1。否則T就等於1到N的最短路。

      2、區間約束
     【例題2】給定n(n <= 50000)個整點閉區間和這個區間中至少有多少整點需要被選中,每個區間的範圍爲[ai, bi],並且至少有ci個點需要被選中,其中0 <= ai <= bi <= 50000,問[0, 50000]至少需要有多少點被選中。
      例如3 6 2 表示[3, 6]這個區間至少需要選擇2個點,可以是3,4也可以是4,6(總情況有 C(4, 2)種 )。

      這類問題就沒有線性約束那麼明顯,需要將問題進行一下轉化,考慮到最後需要求的是一個完整區間內至少有多少點被選中,試着用d[i]表示[0, i]這個區間至少有多少點能被選中,根據定義,可以抽象出 d[-1] = 0,對於每個區間描述,可以表示成d[ bi ]  - d[ ai - 1 ] >= ci,而我們的目標要求的是 d[ 50000 ] - d[ -1 ] >= T 這個不等式中的T,將所有區間描述轉化成圖後求-1到50000的最長路。
      這裏忽略了一些要素,因爲d[i]描述了一個求和函數,所以對於d[i]和d[i-1]其實是有自身限制的,考慮到每個點有選和不選兩種狀態,所以d[i]和d[i-1]需要滿足以下不等式:  0 <= d[i] - d[i-1] <= 1   (即第i個數選還是不選)
      這樣一來,還需要加入 50000*2 = 100000 條邊,由於邊數和點數都是萬級別的,所以不能採用單純的Bellman-Ford ,需要利用SPFA進行優化,由於-1不能映射到小標,所以可以將所有點都向x軸正方向偏移1個單位(即所有數+1)。

      3、未知條件約束
      未知條件約束是指在不等式的右邊不一定是個常數,可能是個未知數,可以通過枚舉這個未知數,然後對不等式轉化成差分約束進行求解。
     【例題3】
在一家超市裏,每個時刻都需要有營業員看管,R(i)  (0 <= i < 24)表示從i時刻開始到i+1時刻結束需要的營業員的數目,現在有N(N <= 1000)個申請人申請這項工作,並且每個申請者都有一個起始工作時間 ti,如果第i個申請者被錄用,那麼他會連續工作8小時。
現在要求選擇一些申請者進行錄用,使得任何一個時刻i,營業員數目都能大於等於R(i)。
       i = 0 1 2 3 4 5 6 ... 20 21 22 23 23,分別對應時刻 [i, i+1),特殊的,23表示的是[23, 0),並且有些申請者的工作時間可能會“跨天”。
       a[i] 表示在第i時刻開始工作的人數,是個未知量
       b[i] 表示在第i時刻能夠開始工作人數的上限, 是個已知量
       R[i] 表示在第i時刻必須值班的人數,也是已知量
       那麼第i時刻到第i+1時刻還在工作的人滿足下面兩個不等式(利用每人工作時間8小時這個條件):
       當 i >= 7,        a[i-7] + a[i-6] + ... + a[i] >= R[i]                                     (1)
       當 0 <= i < 7,  (a[0] + ... + a[i]) + (a[i+17] + ... + a[23]) >= R[i]              (2)

       對於從第i時刻開始工作的人,滿足以下不等式:
       0 <= i < 24,    0 <= a[i] <= b[i]                                                            (3)
       令 s[i] = a[0] + ... + a[i],特殊地,s[-1] = 0
       上面三個式子用s[i]來表示,如下:
       s[i] - s[i-8] >= R[i]                               (i >= 7)                                      (1)
       s[i] + s[23] - s[i+16] >= R[i]               (0 <= i < 7)                                  (2)
       0 <= s[i] - s[i-1] <= b[i]                     (0 <= i < 24)                                (3)
       
      仔細觀察不等式(2),有三個未知數,這裏的s[23]就是未知條件,所以還無法轉化成差分約束求解,但是和i相關的變量只有兩個,對於s[23]的值我們可以進行枚舉,令s[23] = T, 則有以下幾個不等式:
      
      s[i] - s[i-8] >= R[i]
      s[i] - s[i+16] >= R[i] - T
      s[i] - s[i-1] >= 0
      s[i-1] - s[i] >= -b[i]
      
      對於所有的不等式 s[y] - s[x] >= c,建立一條權值爲c的邊 x->y,於是問題轉化成了求從原點-1到終點23的最長路。
      但是這個問題比較特殊,我們還少了一個條件,即:s[23] = T,它並不是一個不等式,我們需要將它也轉化成不等式,由於設定s[-1] = 0,所以 s[23] - s[-1] = T,它可以轉化成兩個不等式:
      s[23] - s[-1] >= T
      s[-1] - s[23] >= -T
      將這兩條邊補到原圖中,求出的最長路s[23]等於T,表示T就是滿足條件的一個解,由於T的值時從小到大枚舉的(T的範圍爲0到N),所以第一個滿足條件的解就是答案。
      最後,觀察申請者的數量,當i個申請者能夠滿足條件的時候,i+1個申請者必定可以滿足條件,所以申請者的數量是滿足單調性的,可以對T進行二分枚舉,將枚舉複雜度從O(N)降爲O(logN)。

五、差分約束題集整理

最短路
Shortest Path                      ☆☆     單源最短路
Shortest Path Problem              ☆☆     單源最短路 + 路徑數
HDU Today                          ☆☆     單源最短路
Idiomatic Phrases Game             ★☆☆     單源最短路
Here We Go(relians) Again          ☆☆     單源最短路
find the safest road               ☆☆     單源最短路
Saving James Bond                  ☆☆     單源最短路
A strange lift                     ★☆☆     單源最短路
Free DIY Tour                      ☆☆     單源最短路 + 路徑還原
find the safest road               ★☆☆     單源最短路(多詢問)
Invitation Cards                   ☆☆     單源最短路
Minimum Transport Cost             ☆☆     單源最短路 + 路徑還原
Bus Pass                           ☆☆     單源最短路
In Action                          ☆☆     單源最短路 + 揹包
Choose the best route              ★☆☆     單源最短路 + 預處理
find the longest of the shortest   ★☆☆     二分枚舉 + 最短路
Cycling                            ☆☆     二分枚舉 + 最短路
Trucking                           ★☆☆     二分枚舉 + 最短路
Delay Constrained Maximum Capacity ★☆☆     二分枚舉 + 最短路
The Worm Turns                     ☆☆     四向圖最長路
A Walk Through the Forest          ☆☆     按照規則求路徑數
find the mincost route             ★☆☆     無向圖最小環
Arbitrage                          ☆☆     多源最短路
zz's Mysterious Present            ★☆☆     單源最短路
The Shortest Path                  ★☆☆     多源最短路
Bus System                         ★☆☆     單源最短路
How Many Paths Are There           ☆☆     次短路
WuKong                             ☆☆     兩條最短路的相交點個數爲P,要求最大化P
Shortest Path                      ★☆☆     多詢問的最短路
Sightseeing                        ★☆☆     最短路和次短路的路徑數
Travel                             ★☆☆     最短路徑樹思想
Shopping                           ★☆☆
Transit search                     ★★☆
Invade the Mars                    ★★☆
Circuit Board                      ★★☆
Earth Hour                         ★★☆
Catch the Theves                   ★★☆

差分約束
Layout                             ★☆☆     差分約束系統 - 最短路模型 + 判負環
World Exhibition                   ☆☆☆     差分約束系統 - 最短路模型 + 判負環
House Man                          ☆☆☆     差分約束系統 - 最短路模型 + 判負環
Intervals                          ☆☆☆     差分約束系統 - 最長路模型 邊存儲用鏈式前向星
King                               ☆☆☆     差分約束系統 - 最長路模型 + 判正環
XYZZY                              ☆☆☆     最長路 + 判正環
Integer Intervals                  ☆☆☆     限制較強的差分約束 - 可以貪心求解
THE MATRIX PROBLEM                 ★☆☆     差分約束系統 - 最長路模型 + 判正環
Is the Information Reliable?       ★☆☆     差分約束系統 - 最長路模型 + 判正環      
Advertisement                      ★☆☆     限制較強的差分約束 - 可以貪心求解
Cashier Employment                 ★☆☆     二分枚舉 + 差分約束系統 - 最長路模型
Schedule Problem                   ★☆☆     差分約束系統 - 最長路模型
Subway Chasing                     ★☆☆     差分約束系統 - 最長路模型 - 判正環
Candies                            ★☆☆
Burn the Linked Camp               ★☆
Instrction Arrangement             ★☆☆



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章