>>最近了解了一些題目,其中對棋盤或者是漢諾塔的表示一般都用到了狀態壓縮的方法,配合BFS或者是DP來做。
題目鏈接:
漢諾塔移動 http://stackoverflow.com/questions/16601701/facebook-sample-puzzle-towers-of-hanoi
黑白棋游戲 http://www.wikioi.com/problem/2743/
狀態壓縮是信息學競賽中一個很常見的方法,最最常見的是二進制壓位。
做過USACO的同學會知道有很多搜索和DP都可以用狀態壓縮優化。
一般來講,如果狀態的夠成非常多,但每個構成相對簡單,就可以狀態壓縮,比如巨大的0/1矩陣等。
當然如果狀態壓縮,提取每個數據起來就會耗費更多的時間,所以一般運用在狀態復制量較大,比較量較小的情況下。
舉例:
多關鍵字排序可以用狀態壓縮,如A,B,C三個關鍵字,均小於100,可以壓縮成A*10000+B*100+C,直接比較即可,非常方便。
N皇后的狀態壓縮,配合位運算,神速。
相鄰兩行之間的匹配關系,壓成二進制,做DP。
其實只要想節約空間都可以用狀態壓縮。
壓縮通式:0<a<A 0<b<B 0<c<C (關鍵程度a>=b>=c) <==> T=a*B*C+b*C+c ,A*B*C<maxstruct
為了方便一般使A=B=C,轉換成A進制數即可
(*)狀態壓縮的效率:
1.空間利用率:顯然按十進制壓縮的空間效率太低,可以考慮按50為一個單元壓縮,
2.時間效率:提取時大量的取模和除法運算使效率太低,可以考慮換成64進制的數,進行壓縮。
這里的空間又有了一定程度上的浪費,也體現了時間與空間的辯證關系。
關於狀態壓縮動態規划:
引入
首先來說說“狀態壓縮動態規划”這個名稱,顧名思義,狀態壓縮動態規划這個算法包括兩個特點,第一是“狀態壓縮”,第二是“動態規划”。
狀態壓縮:
從狀態壓縮的特點來看,這個算法適用的題目符合以下的條件:
1.解法需要保存一定的狀態數據(表示一種狀態的一個數據值),每個狀態數據通常情況下是可以通過2進制來表示的。這就要求狀態數據的每個單元只有兩種狀態,比如說棋盤上的格子,放棋子或者不放,或者是硬幣的正反兩面。這樣用0或者1來表示狀態數據的每個單元,而整個狀態數據就是一個一串0和1組成的二進制數。
2.解法需要將狀態數據實現為一個基本數據類型,比如int,long等等,即所謂的狀態壓縮。狀態壓縮的目的一方面是縮小了數據存儲的空間,另一方面是在狀態對比和狀態整體處理時能夠提高效率。這樣就要求狀態數據中的單元個數不能太大,比如用int來表示一個狀態的時候,狀態的單元個數不能超過32(32位的機器)。
這里舉一個可以狀態壓縮的例子,比如poj上的第1753題Flip Game(見poj1753解題報告),雖然這道題目不是用動態規划做,但是可以用狀態壓縮,4×4的格子,每個格子的狀態為黑或者白,這樣就可以用一個16位的二進制數來表示,在實現的時候可以用一個int類型來表示這個二進制數。在狀態和狀態對比和轉換的時候可以用位操作來完成。
位操作實現技巧:
如果要獲得第i位的數據,判斷((data&(0X1<<i))==0),若真,為0,假,為1;
如果要設置第i位為1,data=(data|(0X1<<i));
如果要設置第i位為0,data=(data&(~(0X1<<i)));
如果要將第i位取反,data=(data^(0X1<<i);
如果要取出一個數的最后一個1(lowbit):(data&(-data)) (這里利用的是負數取反加1實際上改變的是二進制最低位的1這個性質)
插一句:負數的補碼表示:(負數的補碼等於對應的正數二進制表示取反加1)
在從4中減去12時,究竟進行了什么操作?實際上是對負二進制數值采用了2的補碼形式。這里需要做一個約定,以避免解釋它為什么有效。下面看看如何從正數中構建負數的2的補碼形式,讀者也可以自己證明這是有效的。現在回到前面的例子,給-8構建2的補碼形式。首先把+8轉換為二進制:
0000 1000
現在反轉每個二進制數字,即把0變成1,把1變成0: 1111 0111
這稱為1的補碼形式,如果給這個數加上1,就得到了2的補碼形式: 1111 1000
這就是從+4中減去+12,得到的–8的二進制表示。為了確保正確,下面對–8和+12進行正常的相加操作:
+12轉換為二進制 0000 1100 –8轉換為二進制 1111 1000
把這兩個數加在一起,得到: 0000 0100
動態規划:
如果說狀態壓縮是數據結構的話,那么動態規划應該是算法了。題目通過動態規划來解通常有兩個動機,第一是利用遞歸的重疊子問題,進行記憶話求解,即遞歸法的優化。第二是把問題看作多階段決策的過程來求解問題。在狀態壓縮動態規划中我們討論的是第二種動機。
多階段決策過程求解問題的動態規划最重要的是划分階段和找到狀態轉移方程。對於划分階段,是根據不同階段之間的獨立性來划分,通常會用狀態數組的第一個下標來記錄這個階段的標記(比如01背包問題中的狀態數組第一個下標為物品的個數,棋盤放棋子問題中的狀態數組的第一個下標為棋盤的行數等等)。另一個重要的便是狀態轉移方程,狀態轉移方程是遞推時得到一個狀態數據的重要根據。通常情況下狀態數組的除了第一個下標以外都是表示狀態數據的,而狀態數組的值是和所求結果緊密結合的。在后面的幾個例題中會重點說明狀態轉移方程。
當狀態壓縮和動態規划結合的時候便形成了一類問題的一種算法,即狀態壓縮動態規划的算法。這種算法最常見在棋盤問題上或者網格問題上,因為這一類問題的狀態數據的單元較少,可以通過狀態壓縮來對當前棋盤或者網格的狀態進行處理。
例題解析
例:在n*n(n≤20)的方格棋盤上放置n 個車,某些格子不能放,求使它們不能互相攻擊的方案總數。
分析:
首先看到這道題目,我們可能會想到8皇后問題,用深度優先搜索。但是這里放這道題目是用來說明此題可以狀態壓縮動態規划,而且狀態壓縮動態規划相比於深度優先搜索可以應對更多的變化情況和擁有更高的效率。
用狀態壓縮動態規划算法
1.划分階段,本題比較簡單,以行來划分階段,即一行一行的放車。
2.找狀態轉移方程,因為前i行的狀態是根據前i-1行的狀態來確定的,所以,在狀態數組中要記錄多行狀態。故設計狀態數組為f[i][x],i表示這是前i行的狀態,x在這里是就是一個壓縮的狀態,記錄一個二進制數從而來表示前i行的狀態,f的值記錄形成前i行的x狀態有多少種方案。狀態轉移方程是f[i][a]=SUM{f[i-1][b]},下面討論a狀態與b狀態的關系。先舉個例子,如果a狀態為01011,表明前i行中的第0,1,3列都放置了一個車(從右往左看)。於是b的狀態就可能是01010,01001,00011三種狀態,於是f[3][01011]=f[2][01010]+f[2][01001]+f[2][00011]。再看普遍的情況,a-b的值的二進制表示中只有一個1。而且要保證a狀態,b狀態不能和非可用的格子沖突。綜上,狀態轉移方程為:
f[i][a]=SUM{f[i-1][b]} (a-b的值的二進制表示中只有一個1; a,b不與題目中的約束條件沖突)
這樣通過遞推可以得到f[n][1…1]的方案數即為最后的方案總數。
算法總結:
1.斷定這道題目可以用數據壓縮動態規划來做。重點是看它的特點,是否符合狀態壓縮動態規划的條件。
2.划分階段。像棋盤和網格問題大多數是一行一行的進行操作,故以行來划分階段,當然也有其他的階段划分方法,具體問題具體分析。
3.找狀態轉移方程
3.1設計狀態數組。通常數組的第一個參數為階段的標志,其他幾個參數為記錄狀態用(如果第i行的狀態可以通過第i-1行的狀態來確定,則需要一個參數,即第i行的狀態;如果像炮兵陣地那題一樣,第i行的狀態需要根據第i-1行和第i-2行來確定,則需要兩個參數,分別為第i行的狀態和第i-1行的狀態,具體見附錄)。數組的值與結果掛鈎,通常有以下幾種情況:a.題目要求一共有多少的方案,這時數組的值為當前狀態的方案數。(比如poj2411以及例題)b.題目要求最佳方案,這是數組的值為最佳方案的值。(比如poj1185炮兵陣地)。
3.2列出狀態轉移方程,對應與上面的a,b兩種情況,a情況時,狀態轉移方程為f[i][a]=sum{f[i-1][b]}; b情況時,狀態轉移方程為f[i][a]=max{f[i][b]}+sth.
3.3找出狀態轉移方程中a,b之間的關系。即為狀態轉移方程添加約束條件。
4.在很多情況下需要對每一個階段的可能值進行dfs來找出所有的可能值存儲起來,以便在對每一個階段處理的時候能夠很快的運用以提高效率。
