基於連通性狀態壓縮的動態規劃問題

基於連通性狀態壓縮的動態規劃問題

基於狀態壓縮的動態規劃問題是一類以集合信息爲狀態且狀態總數爲指數級的特殊的動態規劃問題.在狀態壓縮的基礎上,有一類問題的狀態中必需要記錄若干個元素的連通狀況,咱們稱這樣的問題爲基於連通性狀態壓縮的動態規劃問題,本文着重對這類問題的解法及優化進行探討和研究.html

本文主要從動態規劃的幾個步驟——劃分階段,確立狀態,狀態轉移以及程序實現來介紹這類問題的通常解法,會特別針對到目前爲止信息學競賽中涌現出來的幾類題型的解法做一個探討.結合例題,本文還會介紹做者在減小狀態總數和下降轉移開銷兩個方面對這類問題優化的一些心得.總結自CDQ論文算法

序言

先看一個很是經典的問題——旅行商問題(即TSP問題,Traveling Salesman Problem):一個n(15)個點的帶權徹底圖,求權和最小的通過每一個點剛好一次的封閉迴路.這個問題已經被證實是NP徹底問題,那麼對於這樣一類無多項式算法的問題,搜索算法是否是解決問題的惟一途徑呢? 答案是否認的.不難發現任什麼時候候咱們只須要知道哪些點已經被遍歷過而遍歷點的具體順序對之後的決策是沒有影響的,所以不妨以當前所在的位置i,遍歷過的點的集合S爲狀態做動態規劃:編程

動態規劃的時間複雜度爲,雖然爲指數級算法,可是對於n = 15的數據規模來講已經比樸素的的搜索算法高效不少了.咱們一般把這樣一類以一個集合內的元素信息做爲狀態且狀態總數爲指數級別的動態規劃稱爲基於狀態壓縮的動態規劃或集合動態規劃.基於狀態壓縮的動態規劃問題一般具備如下兩個特色:1.數據規模的某一維或幾維很是小;2.它須要具有動態規劃問題的兩個基本性質:最優性原理無後效性數組

通常的狀態壓縮問題,壓縮的是一個小範圍內每一個元素的決策,狀態中元素的信息相對獨立.而有些問題,僅僅記錄每一個元素的決策是不夠的,不妨再看一個例子:給你一個m * n (m, n≤9) 的矩陣,每一個格子有一個價值,要求找一個連通塊使得該連通塊內全部格子的價值之和最大.按從上到下的順序依次考慮每一個格子選仍是不選,下圖爲一個極端狀況,其中黑色的格子爲所選的連通塊.只考慮前5行的時候,全部的黑色格子造成了三個連通塊,而最後全部的黑色格子造成一個連通塊.若是狀態中只單純地記錄前一行或前幾行的格子選仍是不選,是沒法準確描述這個狀態的,所以壓縮的狀態中咱們須要增長一維,記錄若干個格子之間的連通狀況.咱們把這一類必需要在狀態中記錄若干個元素之間的連通訊息的問題稱爲基於連通性狀態壓縮的動態規劃問題.本文着重對這類問題進行研究.app

 

 

連通是圖論中一個很是重要的概念,在一個無向圖中,若是兩個頂點之間存在一條路徑,則稱這兩個點連通.而基於連通性狀態壓縮的動態規劃問題與圖論模型有着密切的關聯,好比後文涉及到的哈密爾頓迴路、生成樹等等.一般這類問題的自己與連通性有關或者隱藏着連通訊息.框架

全文共有六個章節.ide

第一章,問題的通常解法,介紹解決基於連通性狀態壓縮的動態規劃問題的通常思路和解題技巧;測試

第二章,一類簡單路徑問題,介紹一類基於棋盤模型的簡單路徑問題的狀態表示的改進——括號表示法以及提出廣義的括號表示法;優化

第三章,一類棋盤染色問題,介紹解決一類棋盤染色問題的通常思路;編碼

第四章,一類基於非棋盤模型的問題,介紹解決一類非棋盤模型的連通性狀態壓縮問題的通常思路;

第五章,一類最優性問題的剪枝技巧,本章的重點是優化,探討如何經過剪枝來減小擴展的狀態的總數從而提升算法的效率;

第六章,總結,回顧前文,總結解題方法.

一. 問題的通常解法

基於連通性狀態壓縮的動態規劃問題一般具備一個比較固定的模式,幾乎全部的題目都是在這個模式的基礎上變形和擴展的.本章選取了一個有表明性的例題來介紹這一類問題的通常解法.

【例1】Formula 1[1]

問題描述

給你一個m * n的棋盤,有的格子是障礙,問共有多少條迴路使得通過每一個非障礙格子剛好一次.m, n ≤ 12.



[1] Ural1519, Timus Top Coders : Third Challenge

 

如圖,m = n = 4,(1, 1), (1, 2)是障礙,共有2條知足要求的迴路.

算法分析

劃分階段】 這是一個典型的基於棋盤模型的問題,棋盤模型的特殊結構,使得它成爲連通性狀態壓縮動態規劃問題最多見的「舞臺」.一般來講,棋盤模型有三種劃分階段的方法:逐行,逐列,逐格.顧名思義,逐行即從上到下或從下到上依次考慮每一行的狀態,並轉移到下一行;逐列即從左到右或從右到左依次考慮每一列的狀態,並轉移到下一列;逐格即按必定的順序(如從上到下,從左到右)依次考慮每一格的狀態,並轉移到下一個格子.

對於本題來講,逐行遞推和逐列遞推基本相似[1],接下來咱們會對逐行遞推和逐格遞推的狀態確立,狀態轉移以及程序實現一一介紹.


[1] 有的題目, 逐行遞推和逐列遞推的狀態表示有較大的區別, 好比本文後面會講到的Rocket Mania一題

確立狀態】 先提出一個很是重要的概念——「插頭」.對於一個4連通的問題來講,它一般有上下左右4個插頭,一個方向的插頭存在表示這個格子在這個方向能夠與外面相連.本題要求迴路的個數,觀察能夠發現全部的非障礙格子必定是從一個格子進來,另外一個格子出去,即4個插頭剛好有2個插頭存在,共6種狀況.

逐行遞推不妨按照從上到下的順序依次考慮每一行.分析第i 行的哪些信息對第i + 1行有影響:咱們須要記錄第i行的每一個格子是否有下插頭,這決定了第i+1行的每一個格子是否有上插頭.僅僅記錄插頭是否存在是不夠的,可能致使出現多個迴路 (如圖),而本題要求一個迴路,也就隱含着最後全部的非障礙格子經過插頭鏈接成了一個連通塊,所以還須要記錄第i行的n個格子的連通狀況

咱們稱圖中的藍線爲輪廓線,任什麼時候候只有輪廓線上方與其直接相連的格子和插頭纔會對輪廓線如下的格子產生直接的影響.經過上面的分析,能夠寫出動態規劃的狀態:表示前i行,第i行的n個格子是否具備下插頭的一個n位的二進制數爲,第i行的n個格子之間的連通性爲的方案總數.

如何表示n個格子的連通性呢? 一般給每個格子標記一個正數,屬於同一個的連通塊的格子標記相同的數.好比{1,1,2,2}和{2,2,1,1}都表示第1,2個格子屬於一個連通塊,第3,4個格子屬於一個連通塊.爲了不出現同一個連通訊息有不一樣的表示,通常會使用最小表示法

一種最小表示法爲:全部的障礙格子標記爲0,第一個非障礙格子以及與它連通的全部格子標記爲1,而後再找第一個未標記的非障礙格子以及與它連通的格子標記爲2,……,重複這個過程,直到全部的格子都標記完畢.好比連通訊息((1,2,5),(3,6),(4))表示爲{1,1,2,3,1,2}.還有一種最小表示法,即一個連通塊內全部的格子都標記成該連通塊最左邊格子的列編號,好比上面這個例子,咱們表示爲{1,1,3,4,1,3}.兩種表示方法在轉移的時候略有不一樣,本文後面將會提到[1].如上圖三個狀態咱們能夠依次表示爲,,.

狀態表示的優化 經過觀察能夠發現若是輪廓線上方的n個格子中某個格子沒有下插頭,那麼它就不會再與輪廓線如下的格子直接相連,它的連通性對輪廓線如下的格子不會再有影響,也就成爲了「冗餘」信息.不妨將記錄格子的連通性改爲記錄插頭的連通性,若是這個插頭存在,那麼就標記這個插頭對應的格子的連通標號,若是這個插頭不存在,那麼標記爲0.這樣狀態就從精簡爲,上圖三個狀態表示爲,,.

優化後不只狀態表示更加簡單,並且狀態總數將會大大減小.



[1]由於第一種表示法更加直觀, 本文若是不做特殊說明, 默認使用第一種最小表示法

逐格遞推 按照從上到下,從左到右的順序依次考慮每一格.分析轉移完(i, j)這個格子後哪些信息對後面的決策有影響:一樣咱們能夠刻畫出輪廓線,即輪廓線上方是已決策格子,下方是未決策格子.由圖可知與輪廓線直接相連的格子有n個,直接相連的插頭有n+1個,包括n個格子的下插頭以及(i, j)的右插頭.爲了保持輪廓線的「連貫性」,不妨從左到右依次給n個格子標號,n+1個插頭標號.相似地,咱們須要記錄與輪廓線直接相連的n+1個插頭是否存在以及n個格子的連通狀況.

經過上面的分析,很容易寫出動態規劃的狀態:表示當前轉移完(i, j)這個格子,n+1個插頭是否存在表示成一個n+1位的二進制數S0,以及n個格子的連通性爲S1的方案總數.

逐行遞推的時候咱們提到了狀態的優化,一樣地,咱們也能夠把格子的連通性記錄在插頭上,新的狀態爲,上圖3個狀態依次爲

 

,,.

 

轉移狀態

狀態的轉移開銷主要包含兩個方面:每一個狀態轉移的狀態數,計算新的狀態的時間.

逐行遞推 假設從第i行轉移到第i+1行,咱們須要枚舉第i+1行的每一個格子的狀態(共6種狀況),對於任何一個非障礙格子,它是否有上插頭和左插頭已知,所以最多隻有2種狀況,狀態的轉移數≤2n

枚舉完第i+1行每一個格子的狀態後,須要計算第i+1行n個格子之間的連通性的最小表示,一般可使用並查集的Father數組對其從新標號或者從新執行一次BFS/DFS,時間複雜度爲O(n),最後將格子的連通性轉移到插頭的連通性上.

特別須要注意的是在轉移的過程當中,爲了不出現多個連通塊,除了最後一行,任什麼時候候一個連通份量內至少有一個格子有下插頭.

逐格遞推 仔細觀察下面這個圖,當轉移到時,輪廓線上n個格子只有(i-1, j)被改爲(i, j),n+1個插頭只有2個插頭被改動,即(i, j-1)的右插頭修改爲(i, j)的下插頭和(i-1,j)的下插頭修改爲(i, j)的右插頭.轉移的時候枚舉(i, j)的狀態分狀況討論.通常棋盤模型的逐格遞推轉移有3類狀況:新建一個連通份量,合併兩個連通份量,以及保持原來的連通份量.

下面針對本題進行分析:

 

狀況1  新建一個連通份量,這種狀況出如今(i, j)有右插頭和下插頭.新建的兩個插頭連通且不與其它插頭連通,這種狀況下須要將這兩個插頭連通份量標號標記成一個未標記過的正數,從新O(n)掃描保證新的狀態知足最小表示.

狀況2  合併兩個連通份量,這種狀況出如今(i, j)有上插頭和左插頭.若是兩個插頭不連通,那麼將兩個插頭所處的連通份量合併,標記相同的連通塊標號,O(n)掃描保證最小表示;若是已經連通,至關於出現了一個迴路,這種狀況只能出如今最後一個非障礙格子.

狀況3  保持原來的連通份量,這種狀況出如今(i, j)的上插頭和左插頭剛好有一個,下插頭和右插頭也剛好有一個.下插頭或右插頭至關因而左插頭或上插頭的延續,連通塊標號相同,而且不會影響到其餘的插頭的連通塊標號,計算新的狀態的時間爲O(1)

注意當從一行的最後一個格子轉移到下一行的第一個格子的時候,輪廓線須要特殊處理.值得一提的是,上面三種狀況計算新的狀態的時間分別爲O(n), O(n), O(1),若是使用前面提到的第二種最小表示方法,狀況1只須要O(1),可是狀況3可能須要O(n)從新掃描.

比較一下逐行遞推和逐格遞推的狀態的轉移,逐行遞推的每個轉移的狀態總數爲指數級,而逐格遞推爲O(1),每次計算新的狀態的時間二者最壞狀況都爲O(n),可是逐行遞推的常數要比逐格遞推大,從轉移開銷這個角度來看,逐格遞推的優點是毋庸置疑的.

程序實現

逐行遞推和逐格遞推的程序實現基本一致,下面以逐格遞推爲例來講明.首先必須解決的一個問題是,對於像這樣的一個狀態咱們該如何存儲,能夠開一個長度爲n+1的數組來存取n+1個插頭的連通性,可是數組判重並不方便,並且空間較大.不妨將n+1個元素進行編碼,用一個或幾個整數來存儲,當咱們須要取一個狀態出來對它進行修改的時候再進行解碼

編碼最簡單的方法就是表示成一個n+1位的p進制數,p能夠取可以達到的最大的連通塊標號加1[1],對本題來講,最多出現個連通塊,不妨取p = 7.在不會超過數據類型的範圍的前提下,建議將p改爲2的冪,由於位運算比普通的運算要快不少,本題最好採用8進制來存儲.

如需大範圍修改連通塊標號,最好將狀態O(n) 解碼到一個數組中,修改後再O(n)計算出新的p進制數,而對於只須要局部修改幾個標號的狀況下,能夠直接用(x div pi-1) mod p來獲取第i位的狀態,用直接對第i位進行修改.

最後咱們探討一下實現的方法,通常有兩種方法:

1.對全部可能出現的狀態進行編碼,枚舉編碼方式:預處理將全部可能的

連通性狀態搜索出來,依次編號1, 2, 3, ,Tot,那麼狀態爲表示轉移完(i, j)後輪廓線狀態編號爲k的方案總數.將全部狀態存入Hash表中,使得每一個狀態與編號一一對應,程序框架以下:

1 For  i ← 1  to  m
2 For  j ←1  to  n
3 For  k ← 1  to  Tot
4       For  x ← (i, j, State[k]) 的全部轉移後的狀態
5  ← 狀態x的編號
6  , 爲 的後繼格子. 
7       End For

[1]由於還要把0留出來存沒有插頭的狀況

 2.記憶化寬度優先搜索:將初始狀態放入隊列中,每次取隊首元素進行擴展,並用Hash對擴展出來的新的狀態判重.程序框架以下:

 1 Queue.Push(全部初始狀態) 
 2 While not Empty(Queue)
 3    p ← Queue.Pop()
 4        For  x ← p的全部轉移後的狀態
 5 If  x以前擴展過 Then 
 6 Sum [x] ← Sum[x] + Sum[p]
 7 Else     
 8 Queue.Push(x)
 9 Sum[x] ← Sum[p] 
10      End If
11        End For
12 End While

比較上述兩種實現方法,直接編碼的方法實現簡單,結構清晰,可是有一個很大的缺點:無效狀態可能不少,致使了不少次空循環,而大大影響了程序的效率.下面是一組實驗的比較數據:

表1.直接編碼與寬度優先搜索擴展狀態總數比較

 

 

能夠看出直接編碼擴展的無效狀態的比率很是高,對於障礙較多的棋盤其對比更加明顯,所以一般來講寬度優先搜索擴展比直接編碼實現效率要高.

Hash判重的優化:使用一個HashSize較小的Hash表,每轉移一個(i, j)清空一次,每次判斷狀態x是否擴展過的程序效率比用一個HashSize較大的Hash表每次判斷狀態(i, j, x)高不少.相似地,在不須要記錄路徑的狀況下,也可使用滾動的擴展隊列來代替一個大的擴展隊列.

最後咱們比較一下,不一樣的實現方法對程序效率的影響[1]

Program 1 :8-Based,枚舉編碼方式.

Program 2 :8-Based,隊列擴展,HashSize = 3999997.

Program 3 :8-Based,隊列擴展,HashSize = 4001,Hash表每次清空.

Program 4 :7-Based,隊列擴展,HashSize = 4001,Hash表每次清空.

表2.不一樣的實現方法的程序效率的比較


[1] 測試環境: Intel Core2 Duo T7100, 1.8GHz, 1G內存

小結

本章從劃分階段,確立狀態,狀態轉移以及程序實現四個方面介紹了基於連通性狀態壓縮動態規劃問題的通常解法,並在每一個方面概括了一些不一樣的方法,最後對不一樣的算法的效率進行比較.在平時的解題過程當中咱們要學會針對題目的特色和數據規模「對症下藥」,選擇最合適的方法而達到最好的效果.

因爲逐格遞推的轉移開銷比逐行遞推小不少,下文若是不做特殊說明,咱們都採用逐格的階段劃分.

二. 一類簡單路徑問題

這一章咱們會針對一類基於棋盤模型的簡單迴路和簡單路徑問題的解法做一個探討.簡單路徑,即除了起點和終點可能相同外,其他頂點均不相同的路徑,而簡單迴路爲起點和終點相同的簡單路徑.Formula 1是一個典型的棋盤模型的簡單迴路問題,這一章咱們繼續以這個題爲例來講明.

首先咱們分析一下簡單迴路問題有什麼特色:

 

 

仔細觀察上面的圖,能夠發現輪廓線上方是由若干條互不相交的路徑構成的,而每條路徑的兩個端口剛好對應了輪廓線上的兩個插頭! 一條路徑上的全部格子對應的是一個連通塊,而每條路徑的兩個端口對應的兩個插頭是連通的並且不與其餘任何一個插頭連通.

在上一章咱們提到了逐格遞推轉移的時候的三種狀況:新建一個連通份量,合併兩個連通份量,保持原來的連通份量,它們分別等價於兩個插頭成爲了一條新的路徑的兩端,兩條路徑的兩個端口鏈接起來造成一條更長的路徑或一條路徑的兩個端口鏈接起來造成一個迴路以及延長原來的路徑.

經過上面的分析咱們知道了簡單迴路問題必定知足任什麼時候候輪廓線上每個連通份量剛好有2個插頭,那麼這些插頭之間有什麼性質呢? 

 

【性質】輪廓線上從左到右4個插頭a, b, c, d,若是a, c連通,而且與b不連通,那麼b, d必定不連通.

證實:反證法,若是a, c連通,b, d連通,那麼輪廓線上方必定至少存在一條ac的路徑和一條bd的路徑.如圖,兩條路徑必定會有交點,不妨設兩條路徑相交於格子P,那麼P既與a, c連通,又與b, d連通,能夠推出a, cb, d連通,矛盾,得證.

這個性質對全部的棋盤模型的問題都適用.

「兩兩匹配」,「不會交叉」這樣的性質,咱們很容易聯想到括號匹配.將輪廓線上每個連通份量中左邊那個插頭標記爲左括號,右邊那個插頭標記爲右括號,因爲插頭之間不會交叉,那麼左括號必定能夠與右括號一一對應.這樣咱們就可使用3進制——0表示無插頭,1表示左括號插頭,2表示右括號插頭記錄下全部的輪廓線信息.不妨用#表示無插頭,那麼上面的三幅圖分別對應的是(())#(),(()#)(),(()###),即,咱們稱這種狀態的表示方法爲括號表示法

依然分三類狀況來討論狀態的轉移:

爲了敘述方便,不妨稱(i,j-1)的右插頭爲p,(i-1, j)的下插頭爲q,(i, j)的下插頭爲p',右插頭爲q',那麼每次轉移至關於輪廓線上插頭p的信息修改爲的信息p',插頭q的信息修改爲的信息q',設W(x) = 0, 1, 2表示插頭x的狀態.

狀況新建一個連通份量,這種狀況下W(p) = 0,W(q) = 0,p',q'兩個插頭構建了一條新的路徑,p'至關於爲左括號,q'爲右括號,即W(p')← 1,W(q')← 2,計算新的狀態的時間爲O(1).

狀況合併兩個連通份量,這種狀況下W(p) > 0,W(q) > 0,W(p')← 0,W(q')← 0,根據p, q爲左括號仍是右括號分四類狀況討論:

  狀況2.1  W(p) = 1,W(q) = 1.那麼須要將q這個左括號與之對應的右括號v修改爲左括號,即W(v) ← 1.

  狀況2.2  W(p) = 2,W(q) = 2.那麼須要將p這個右括號與之對應的左括號v修改爲右括號,即W(v)← 2.

  狀況2.3  W(p) = 1,W(q) = 2,那麼pq是相對應的左括號和右括號,鏈接p, q至關於將一條路徑的兩端鏈接起來造成一個迴路,這種狀況下只能出如今最後一個非障礙格子.

 狀況2.4  W(p) = 2,W(q) = 1,那麼pq鏈接起來後,p對應的左括號和q對應的右括號剛好匹配,不須要修改其餘的插頭的狀態.

 

狀況2.1, 2.2須要計算某個左括號或右括號與之匹配的括號,這個時候須要對三進制狀態解碼,利用相似模擬棧的方法.所以狀況2.1, 2.2計算新的狀態的時間複雜度爲O(n),2.3, 2.4時間複雜度爲O(1).

狀況保持原來的連通份量,W(p),W(q)中剛好一個爲0,p',q'中也剛好一個爲0.那麼不管p',q'中哪一個插頭存在,都至關因而p, q中那個存在的插頭的延續,括號性質同樣,所以W(p')← W(p) + W(q),W(q')← 0或者W(q')← W(p) + W(q),W(p')← 0.計算新的狀態的時間複雜度爲O(1).

經過上面的分析能夠看出,括號表示法利用了簡單迴路問題的「一個連通份量內只有2個插頭」的特殊性質巧妙地用3進制狀態存儲下完整的連通訊息,插頭的連通性標號相對獨立,再也不須要經過O(n)掃描大範圍修改連通性標號.實現的時候,咱們能夠用4進制代替3進制而提升程序運算效率,下面對最小表示法與括號表示法的程序效率進行比較:

表3.不一樣的狀態表示的程序效率的比較

能夠看出,括號表示法的優點很是明顯,加上它的思路清晰天然,實現也更加簡單,所以對於解決這樣一類簡單迴路問題是很是有價值的.

相似的問題還有:NWERC 2004 Pipes,Hnoi2004 Postman,Hnoi2007 Park,還有一類非迴路問題也能夠經過棋盤改造後用簡單迴路問題的方法解決,好比 POJ 1739 Tony’s Tour:給一個m * n棋盤,有的格子是障礙,要求從左下角走到右下角,每一個格子剛好通過一次,問方案總數.(m, n ≤ 8)

只須要將棋盤改造一下,問題就等價於Formula 1了.

                  .......

#..  改形成  .#####.

       ...          .##..#.

                .......

介紹完簡單迴路問題的解法,那麼通常的簡單路徑問題又如何解決呢?

【例2】Formula 2[1]

問題描述

給你一個m * n的棋盤,有的格子是障礙,要求從一個非障礙格子出發通過每一個非障礙格子剛好一次,問方案總數.m, n ≤ 10.


[1] 改編自Formula 1

如圖,一個2 * 2的無障礙棋盤,共有4條知足要求的路徑.

算法分析

確立狀態:按照從上到下,從左到右依次考慮每個格子,設表示轉移完(i, j)這個格子,輪廓線狀態爲S的方案總數.若是用通常的最小表示法,不只須要記錄每一個插頭的連通狀況,還須要額外記錄每一個插頭是否鏈接了路徑的一端,狀態表示至關複雜.依然從括號表示法這個角度來思考如何來存儲輪廓線的狀態:

這個問題跟簡單迴路問題最大的區別爲:不是全部的插頭都兩兩匹配,有的插頭鏈接的路徑的另外一端不是一個插頭而是整條路徑的一端,咱們稱這樣的插頭爲獨立插頭.不妨將原來的3進制狀態修改爲4進制——0表示無插頭,1表示左括號插頭,2表示右括號插頭,3表示獨立插頭,這樣咱們就能夠用4進制完整地記錄下輪廓線的信息,圖中狀態表示爲(1203)4

狀態轉移:依然設(i, j-1)的右插頭爲p,(i-1, j)的下插頭爲q,(i, j)的下插頭爲p',右插頭爲q'.部分轉移同簡單迴路問題徹底同樣,這裏再也不贅述,下面分三類狀況討論與獨立插頭有關的轉移:

狀況W(p) = 0,W(q) = 0.當前格子可能成爲路徑的一端,即右插頭或下插頭是獨立插頭,所以W(p')← 3,W(q')← 0或者W(q')← 3,W(p')← 0.

狀況W(p) > 0,W(q) > 0,那麼W(p')← 0,W(q')← 0

  狀況2.1  W(p) =3,W(q) = 3,將插頭pq鏈接起來就至關於造成了一條完整的路徑,這種狀況只能出如今最後一個非障礙格子.

  狀況2.2  W(p) ,W(q) 中有一個爲3,若是p爲獨立插頭,那麼不管q是左括號插頭仍是右括號插頭,與q相匹配的插頭v成爲了獨立插頭,所以,

W(v)←3.若是q爲獨立插頭,相似處理.

狀況3 W(p) ,W(q) 中有一個>0,即p, q中有一個插頭存在.

  狀況3.1  若是這個插頭爲獨立插頭,若在最後一個非障礙格子,這個插頭能夠成爲路徑的一端,不然能夠用右插頭或下插頭來延續這個獨立插頭.

  狀況3.2  若是這個插頭是左括號或右括號,那麼咱們以將這個插頭「封住」,使它成爲路徑的一端,須要將這個插頭所匹配的另外一個插頭的狀態修改爲爲獨立插頭.

狀況2.2, 3.2須要計算某個左括號或右括號與之匹配的括號,計算新的狀態的時間複雜度爲O(n),其他狀況計算新的狀態的時間複雜度爲O(1).

特別須要注意,任什麼時候候輪廓線上獨立插頭的個數不能夠超過2個.至此問題完整解決,m = n = 10的無障礙棋盤,擴展的狀態總數爲3493315,徹底能夠承受.

上面兩類題目咱們用括號表示法取得了很不錯的效果,可是它存在必定的侷限性,即插頭必須知足兩兩匹配.那麼對於更加通常的問題,一個連通份量內出現大於2個插頭,上述的括號表示方法顯得一籌莫展.下面將介紹一種括號表示法的變形,它能夠適用於出現連通塊內大於2個插頭的問題,咱們稱之爲廣義的括號表示法:

假設一個連通份量從左到右有多個插頭,不妨將最左邊的插頭標記爲「(」,最右邊的插頭標記爲「)」,中間的插頭所有標記爲「)(」,那麼可以匹配的括號對應的插頭連通.若是問題中可能出現一個連通份量只有一個插頭,那麼這個插頭標記爲「( )」,這樣插頭之間的連通性可用括號序列完整地記錄下來,好比對於一個連通性狀態爲{1,2,2,3,4,3,2,1},咱們能夠用(-(-)(-(-()-)-)-)記錄.

這種廣義的括號表示方法須要用4進制甚至5進制存儲狀態,並且直接對狀態連通性進行修改狀況很是多,最好仍是將狀態進行解碼,修改後再從新編碼.下文咱們將會運用廣義的括號表示法解決一些具體的問題.

小結

本章針對一類簡單路徑問題,提出了一種新的狀態表示方法——括號表示法,最後提出了廣義的括號表示方法.相比普通的最小表示法,括號表示法巧妙地把連通塊與括號匹配一一對應,使得狀態更加簡單明瞭,雖然不會減小擴展的狀態總數,可是轉移開銷的常數要小不少,是一個不錯的方法.

三. 一類棋盤染色問題

有一類這樣的問題——給你一個m * n的棋盤,要求給每一個格子染上一種顏色(共k種顏色),每種顏色的格子相互連通 (4連通).本章主要對這類問題的解法進行探討,咱們從一個例題提及:

【例3】Black & White[1]

問題描述

一個m * n的棋盤,有的格子已經染上黑色或白色,如今要求將全部的未染色格子染上黑色或白色,使得知足如下2個限制:

1)      全部的黑色的格子是連通的,全部的白色格子也是連通的.

2)      不會有一個2 * 2的子矩陣的4個格子的顏色所有相同.

問方案總數.(m, n ≤ 8)

  以下圖,m = 2,n = 3,灰色格子爲未染色格子,共有9種染色方案.

 



[1] Source : Uva10572

算法分析

這是一個典型的棋盤染色問題,着色規則有:

1) 只有黑白兩種顏色,即k = 2,而且同色的格子互相連通.

2) 沒有同色的2 * 2的格子.

對於簡單路徑問題來講,相鄰的格子是否連通取決於它們之間的插頭是否存在,狀態記錄輪廓線上每一個插頭是否存在以及插頭之間的連通性;而棋盤染色問題相鄰的格子是否連通取決於它們的顏色是否相同,這就須要記錄輪廓線上方n個格子的顏色以及格子之間的連通性.

確立狀態 設當前轉移完Q(i, j)這個格子,對之後的決策產生影響的信息有:輪廓線上方n個格子的染色狀況以及它們的連通性,由第2條着色規則「沒有同色的2 * 2的格子」可知P(i-1, j)的顏色會影響到(i, j+1)着色,所以咱們還須要額外記錄格子的顏色.動態規劃的狀態爲:表示轉移完(i, j),輪廓線上從左到右n個格子的染色狀況爲S0 (0 ≤ S0 < 2n),連通性狀態爲S1,格子的顏色爲cp(0或1)的方案總數.

狀態的精簡 若是相鄰的2個格子不屬於同一個連通塊,那麼它們必然不一樣色,所以只須要記錄(i, 1)和(i-1, j+1)兩個格子的顏色,利用S1就能夠推出n個格子的顏色.這個精簡不會減小狀態的總數,仍然須要一個變量來記錄兩個格子的顏色,所以意義並不大,這裏只是提一下.

狀態轉移 枚舉當前格子(i, j)的顏色,計算新的狀態:S0cp都很容易O(1)計算出來.考慮計算S1:輪廓線的變化至關於將記錄(i-1, j)的連通性改爲記錄(i, j)的連通性.根據當前格子與上面的格子和左邊的格子是否同色分四類狀況討論.應當注意的是若是(i, j)和(i-1, j)不一樣色,而且(i-1, j)在輪廓線上爲一個單獨的一個連通塊,那麼(i-1, j)之後都不可能與其餘格子連通,即剩餘的格子都必須染上與(i-1, j)相反的顏色,須要特殊判斷.轉移的時間複雜度爲O(n).計算新狀態的S1程序框架以下:

 1 將前一個狀態的S1解碼,連通性存入c[1],c[2],…,c[n].
 2 If  (i, j) 與 (i-1, j) 不一樣色而且 (i-1, j) 爲一個單獨的連通塊Then
 3 特殊判斷
 4     Else
 5 If  (i, j) 與 (i-1, j) 和 (i, j-1) 均同色Then 
 6 For k ← 1 to n
 7  If c[k] = c[j] Then
 8      c[k] ← c[j-1]  // 合併兩個連通塊
 9                EndIf
10 Else 
11 If  (i, j) 與 (i-1, j) 和 (i, j-1) 均不一樣色Then  
12 c[j] ← 最大可能出現的連通塊標號 // (i, j) 新建一個連通塊.
13 Else 
14 If  (i, j) 與 (i, j-1) 同色與 (i-1, j) 不一樣色 Then 
15 c[j] ← c[j-1]  // (i, j) 的連通性標號跟 (i, j-1)相同.
16              EndIf
17           EndIf
18        EndIf
19     EndIf

c[] O(n)掃描,修改爲最小表示,利用c[]編碼計算出新的S1

對於m = n = 8的一個所有未染色的棋盤,擴展出來的狀態總數爲122395,轉移須要時間爲O(n),所以總的時間複雜度爲O(TotalState * n) = 979160,運行時間<0.1s.至此問題完整解決.相似能夠解決的問題還有2007年重慶市選拔賽 Rect和IPSC 2007 Delicious Cake.

擴展上面提到的是4連通問題,若是要求8連通呢?

4連通問題是兩個格子至少有一條邊重合爲連通,而8連通問題是兩個格子至少有一個頂點重合爲連通,所以須要記錄全部至少有一個頂點在輪廓線上的格子的連通和染色狀況,即包括(i-1, j)在內的n+1個格子.

一個優化的方向 擴展的狀態中無效狀態的總數很大程度上決定了算法的效率.好比Black & White中若是出現右圖的狀態,那麼不管以後如何決策,都不可能知足同色的格子互相連通的性質,所以它是一個無效狀態.對於任何一個k染色棋盤問題,若是從左到右有4個相互不嵌套[1]的連通塊a,b,c,da, c同色, b, d同色且與a, c不一樣色,那麼這個狀態爲無效狀態.

 

小結

本章介紹瞭解決一類棋盤染色問題的通常思路.不管染色規則多麼複雜,咱們只要在基本狀態即「輪廓線上方與其相連的格子的連通性以及染色狀況」的基礎上,根據題目的須要在狀態中增長對之後的決策可能產生影響的信息,問題均可以迎刃而解了.


[1]「嵌套」的概念能夠用廣義的括號匹配的表示方法來理解

 

四. 一類基於非棋盤模型的問題

本章將會介紹一類基於非棋盤模型的連通性狀態壓縮動態規劃問題,它雖然不具備棋盤模型的特殊結構,可是解法的核心思想又跟棋盤模型的問題有着殊途同歸之處.

【例4】生成樹計數[1]

問題描述

給你一個n個點的無向連通圖,其邊集爲:任何兩個不一樣的點i, j(1 ≤ i, j n),若是|i - j| ≤ k,那麼有一條無向邊<i, j>.已知nk,求這個圖的生成樹個數.

    n ≤ 1015,2 ≤ k ≤ 5.

算法分析

這個題給咱們的第一印象是:n很是大,k卻很是小.

    生成樹最重要的兩個性質:無環,連通.那麼若是按照1,2,…,n的順序依次考慮每個點與前面的哪些點相連,而且保證任什麼時候候都不會出現環,最後統計全部的點所有在一個連通份量內的方案總數即爲最終的答案.

在棋盤模型的問題中,咱們提出了輪廓線這個概念,任什麼時候候只有輪廓線上方與其直接相連的格子對之後的決策會產生影響.相似地咱們分析一下這個問題,當咱們肯定了1~i的全部點的連邊狀況後,哪些信息對之後的決策會產生影響:1~i–k這些點與i以後的點必定沒有邊相連,那麼對i之後的點的決策不會產生直接的影響,所以咱們須要記錄的僅僅是i-k+1~ik個點的連通訊息!

以下圖,咱們不妨也稱藍線爲輪廓線,由於只有輪廓線上的點的信息會對輪廓線右邊的點的決策產生直接的影響.這樣咱們就很容易確立狀態


[1] Source : Noi2007 Day2 生成樹計數, Count

 

表示考慮完前i個點的連邊狀況後,i-k+1 .. ik個點的連通狀況爲S.

轉移狀態O(2k)依次枚舉點ii-1,…,i-kk個點是否相連.轉移的時候須要注意:i-1, …, i-kk個點,任何一個連通塊,i最多隻能與其中的一個點相連,這樣能夠避免環的出現.若是i-k在輪廓線上爲一個單獨的連通塊,那麼i必然與i-k相連,這樣能夠避免出現孤立的連通塊.好比對於一個k = 5的狀態來講,若是點ii-2和i-1相連,那麼新的狀態爲.這樣咱們就能夠在O(2k*k)的時間複雜度內完成狀態的轉移.

算法實現:Tk表示k個點的本質不一樣的連通狀況的個數,搜索可知T5=52.動態規劃的時間複雜度爲O(n * Tk * 2k * k),依然太大.能夠發現當ik,狀態是否能夠轉移到只與有關,這樣咱們就能夠用矩陣乘法實現動態規劃加速,因爲這不是本文的重點,這裏再也不詳細介紹.最終的時間複雜度爲O(Tk3*log2n),對於k = 5, Tk = 52的數據規模來講已經徹底能夠承受了,至此問題完整解決.

本題中的無向圖很是特殊,每一個點只和距離它不超過k的點有邊相連,而且k很是小.對於棋盤模型的問題,能夠抽象成一個特殊的無向圖——m * n個點,每一個點只與它上下左右四個點有邊相連.那麼對於一個與連通性有關的無向圖問題,無向圖具有怎樣的特色才能夠用基於狀態壓縮的動態規劃來解決? 分析以上幾個問題,不難發現它們有一個共同點:給無向圖中的點找一個序,在這個序中有邊相連的兩個點的距離不超過p(p很小),這樣咱們就能夠以當前決策完序中前i個,最後p個點的連通性爲狀態做動態規劃.棋盤模型的問題中序即爲從上到下,從左到右或從左到右,從上到下,pmn,所以棋盤模型的問題mn中至少有一個數會很是小.

小結

本章寫得比較簡略,可是依然可以給咱們不少的啓示.處理這樣的一類非棋盤模型的問題,通常的思路是尋找某一個序依次考慮每一個點的決策,並分析哪些信息對之後的決策會產生影響,找到問題中的「輪廓線」,以輪廓線的信息來確立動態規劃的狀態.一般來講,輪廓線上的信息比較少,這也是可以做狀態壓縮動態規劃的基礎,像本題中k≤5這樣的條件每每能成爲解決問題的突破口.

五. 一類最優性問題的剪枝技巧

基於連通性狀態壓縮的動態規劃問題的算法的效率主要取決於狀態的總數和轉移的開銷,減小狀態總數和下降轉移開銷成爲了優化的核心內容.前面的章節咱們提到了一些優化的技巧,這一章咱們選取了一個很是有趣的題目Rocket Mania來介紹針對這樣的一類最優性問題,如何經過剪枝使狀態總數大大減小而提升算法效率.

【例5】Rocket Mania[1]

問題描述

這個題目的背景是幻想遊戲的「中國煙花」:

給你一個9 * 6的棋盤,棋盤的左邊有9根火柴,右邊有9個火箭.棋盤中的每個格子多是一個空格子也多是一段管道,管道的類型有4種:


[1] Source : Zju 2125, Online Contest of Fantasy Game

 

一個火箭可以被髮射當且僅當存在一條由管道組成的從一根點燃的火柴到這個火箭的路徑.

給你棋盤的初始狀態以及X,你的目標是旋轉每一個格子內的管道0,90,180或270度,使得當點燃左邊第X根火柴後,被髮射的火箭個數儘量多.

算法分析

確立狀態:按照從左到右,從上到下的順序依次考慮每個格子,咱們須要記錄每一個插頭是否已經點燃以及它們之間的連通狀況.所以狀態爲

表示轉移完(i, j),輪廓線上10個插頭的連通性爲S(把每一個插頭是否存在記錄在S中), 10個插頭是否被點燃的2進制數fired的狀態可否達到.

那麼最後的答案爲全部能夠達到的狀態 Ones[fired]的最大值,其中Ones[x]表示二進制數x的1的個數.

狀態轉移:依次枚舉每個格子的旋轉方式(最多4種),根據當前格子是否能夠與上面的格子和左邊的格子經過插頭鏈接起來分狀況討論,O(m)掃描計算出新的狀態.前面的題目咱們已經很詳細地介紹過棋盤模型的問題的轉移方法,這裏再也不贅述.

若是直接按照上面的思路做動態規劃,Sample也須要運行> 60s,實在使人沒法滿意.優化,勢在必行.如何經過剪枝優化來減小擴展的狀態總數,儘量捨去無效狀態成爲了如今所面臨的問題:

剪枝一般能夠分爲兩類:一.可行性剪枝,即將不管以後如何決策,都不可能知足題目要求的狀態剪掉;二.最優性剪枝,即對於最優性問題,將不可能成爲最優解的狀態給剪掉.咱們從這兩個角度入手來考慮問題:

剪枝一:若是輪廓線上全部的插頭所有都未被點燃,那麼最後全部的火箭都不可能發射,因此這個狀態能夠捨去.這個剪枝看上去很是顯然,對於大部分數據卻能夠剪掉近乎一半的狀態.

剪枝二:若是輪廓線上有一個插頭p,它沒有被火柴點燃且沒有其它的插頭與它連通,那麼這個插頭能夠認爲是「無效」插頭.由於即便這個插頭所在的路徑之後會被點燃而能夠發射某個火箭,那麼必定存在另外一條路徑能夠不通過這個插頭而發射火箭,如圖.這種狀況下將插頭設置爲不存在.這是最重要的一個剪枝,大部分數據的狀態總數能夠縮小七八倍,甚至十幾倍.

剪枝三:這是一個最優性問題,咱們考慮最優性剪枝:對於一個格子(i, j)的兩個狀態,,若是第一個狀態的每個存在的插頭在第二個狀態中不只存在並且都被點燃,那麼不管之後如何決策,第二個狀態點燃的火箭個數不會少於第一個狀態,這樣咱們就能夠果斷地捨去第一個狀態.對於每個(i, j),選擇Ones[Fired]最多的一個狀態Best,若是一個狀態必定不比Best好,就能夠捨去.

剪枝四:從邊界狀況入手,邊界狀態很是特殊,也很是容易致使產生無效狀態.分析一下,轉移完最後一列的某個格子(i, 6)後,若是I類插頭中某個插頭p沒有被點燃,而且II類插頭中沒有插頭與它連通,那麼這個插頭就成了「無效」插頭.

 

比較以上四種剪枝的效果,因爲不一樣的棋盤初始狀態擴展的狀態總數差別較大,所以選取10組不一樣的棋盤初始狀態來測試擴展狀態的總數.10組數據大體分佈以下:Test 1~4依次爲所有「—」,「L」,「T」,「+」,Test 5爲奇數行「L」,偶數行爲空,Test 6爲「L」,「T」交替.Test 7~10爲隨機數據,「L」,「T」分佈較多,Test 10的「—」較多[1]

表4.四種剪枝擴展的狀態總數的比較

由上表能夠看出,優化後擴展的狀態總數已經很是少了,剪枝的效果很是明顯.咱們從可行性和最優性兩個角度,從通常狀況和邊界狀況入手提出了4種剪枝方法,雖然有的剪枝看上去微不足道,可是它產生的效果確是驚人的.固然剪枝方法遠遠不止這4種,只要抓住問題的特徵不斷分析,就能夠提出更多更好的剪枝方法.

值得一提的是S的狀態表示,若是用普通的最小表示法,須要用10進制存儲狀態.由剪枝2可知若是一個插頭屬於一個單獨的連通塊,那麼它必定被火柴點燃.若是使用廣義的括號表示法,能夠將無插頭狀態和單獨的連通塊插頭都有「( )」表示,利用fired來區別,這樣就能夠用「(」,「)」,「)(」,「( )」——4進制完整記錄下n個格子的連通性,相比10進制有必定的常數優點.

至此,問題完整解決,60多組測試數據運行時間<1.5s,實際效果確實不錯.

這個問題還有一個增強版[1]:跟本題惟一不一樣的是,左邊全部的火柴所有點燃,那麼只要把初始狀態中9個右插頭所有設置爲點燃,且爲一個連通塊便可.



[1]Zju 2126 Rocket Mania Plus


小結

本章咱們以RocketMania一題爲例介紹瞭解決一類最優性的連通性狀態壓縮動態規劃問題的剪枝技巧,從可行性和最優性這兩個角度出發而達到減小狀態總數的目的.在解題的過程當中,要抓住問題的主要特徵,多思考,多嘗試,才能作的愈來愈好,優化是無止境的

六.總結

本文立足於基於連通性狀態壓縮動態規劃問題的解法和優化兩個方面.

全文介紹了基於連通性狀態壓縮的動態規劃問題的通常解法及其相關概念;針對一類特殊的問題——簡單迴路和簡單路徑問題,提出了括號表示法以及括號表示法的改進,最後從特殊問題迴歸到通常問題,提出了廣義的括號表示法,這是文章的核心內容;接着對於一類棋盤染色問題和基於非棋盤模型的問題的解法做一個探討;最後咱們把重點放在了剪枝優化上,結合一個很是有趣的例題談針對這類動態規劃問題剪枝的重要性.

固然本文不可能涵蓋基於連通性狀態壓縮動態規劃問題的方方面面,所以關鍵是要掌握解決問題的思路,在解題的過程當中抓住問題的特徵,深刻分析,靈活運用.從上面的例題中能夠發現,細節是不可忽略的因素,它很大程度上決定了算法的效率.所以平時咱們要養成良好的編程習慣,注意細節,注重常數優化.作到多思考,多分析,多實驗,不斷優化,精益求精.讓咱們作得愈來愈好!

【參考文獻】

【1】    《算法藝術與信息學競賽》 劉汝佳、黃亮

【2】    金愷 2004年國家集訓隊做業 《Black & White》解題報告 

【3】    毛子青 2001年國家集訓隊論文《動態規劃算法的優化技巧》 

【4】    Uva在線題庫:http://icpcres.ecs.baylor.edu/onlinejudge

【5】    Ural在線題庫:http://acm.timus.ru

【6】    Zju在線題庫:http://acm.zju.edu.cn

相關文章
相關標籤/搜索