我們知道,用DP解決一個問題的時候很重要的一環就是狀態的表示,一般來說,一個數組即可保存狀態。但是有這樣的一些題 目,它們具有DP問題的特性,但是狀態中所包含的信息過多,如果要用數組來保存狀態的話需要四維以上的數組。於是,我們就需要通過狀態壓縮來保存狀態,而 使用狀態壓縮來保存狀態的DP就叫做狀態壓縮DP。
一道例題:
HOJ 2662
有一個n*m的棋盤(n、m≤80,n*m≤80)要在棋盤上放k(k≤20)個棋子,使得任意兩個棋子不相鄰(每個棋子最多和周圍4個棋子相鄰)。求合法的方案總數。
直接考慮解決這個問題並不容易,我們先來考慮這個問題的退化形式:
現在我們令n=1。則我們可以很容易的想到狀態轉移方程:
設dp[i][j][0]表示當前到達第i列,一共使用了j個旗子,且當前格子的狀態為不放的狀態總數,類似的 dp[i][j][1]就是當前格子的狀態為放的狀態總數。
那么狀態轉移方程就是
dp[i][j][0]=dp[i-1][j][1]+dp[i-1][j][0];
dp[i][j][1]=dp[i-1][j-1][0];
當n=1的時候這個問題無疑是非常簡單的,但是如果我們想模仿這種做法來解決原問題的話,就會遇到這樣的問題:如何來表示當前行的狀態?
昨天已經提到了一些狀態壓縮的知識,如果看懂了的話,應該已經明白怎么做了。
對於每一行,如果把沒有棋子的地方記為0,有棋子的地方記為1,那么每一行的狀態都可以表示成一個2進制數,進而將其轉化成10進制。
那么這個問題的狀態轉移方程就變成了
設dp[ i ] [ j ][k ]表示當前到達第i列,一共使用了j個棋子,且當前行的狀態在壓縮之后的十進制數為k 時的狀態總數。那么我們也可以類似的寫出狀態轉移方程:
dp[ i ][ j ][ k ]=sum( dp[ i-1][ j-num(k) ][ w ] ) num(k)表示 k狀態中棋子的個數,w表示前一行的狀態。
雖然寫出了狀態轉移方程,但是還是有很多細節問題需要解決:比如,如何保證當前狀態是合法的?
最基本的做法是:首先判斷k狀態是否合法,也就是判斷在這一行中是否有2個旗子相鄰,然后枚舉上一行的狀態w,判斷w狀態是否合法,然后判斷k狀態和w狀態上下之間是否有相鄰的棋子。
當然這樣做的時間復雜度是很高的,也就是說有很多地方可以優化,比如:判斷每一行狀態是否合法,可以在程序一開始判斷然后保存結果,判斷k狀態和w狀態上下之間是否有相鄰的棋子,可以利用位運算,if(k&w)說明上下之間有相鄰的棋子等等。
講到這里,這道題目的做法已經很明確了,請大家自行完成。
另一道例題:
TSP問題
給你n個城市和城市之間的通路的長度,請你找出一條經過所有城市一次且僅經過一次的路線,使得這條路線的長度最短。
問題分析,如果要設計一個狀態的話,顯然狀態與已經走過的城市和你當前所在的城市有關,現在,按照一定的順序給每個城市一個編號,如果已經走過的城市記為1,沒走過的城市記為0,那么已經走過的城市的狀態就可以壓縮成一個數。所以,該題目的狀態表示為:
Dp[i][j]表示已經走過的城市為i,當前所在的城市為j的最短路程。
相應的狀態轉移方程為dp[ i ][ j]=min( dp[ i ^ (1<<j) ][ k ] + dis[ k ][ j ] ); i ^ (1<<j)的意思是將j這個城市從i狀態中去掉。 dis[ k][ j ] 是k和j之間的距離。
HOJ2665 雖然題意與之相去甚遠,但是本質上是一樣的,希望大家能夠完成這道題目。
狀態壓縮DP的特點:
狀態中的某一維會比較小,一般不會超過15,多了的話狀態數會急劇上升而無法壓縮,一般來說需要狀態壓縮的也就是這一維。
狀態壓縮DP的常見優化:
預處理是最常見的優化,尤其是在棋盤類問題上,比如說例題1,如果我們想進一步提高效率,我們還可以預處理出狀態之間是否可以轉移而不用在每一次轉移中判斷。
靈活運用位運算,例題1中if(k&w)就是一個很好的例子。
推薦題目:
除了上述2道題目以外,我還推薦:
HOJ
• 2188 WordStack
• 2798 Globulous gumdrops
• 2800 Artillery Assignment
這一類DP對於編碼能力要求比較高,請大家盡力而為。