《編程之美》的4.2中有一個瓷磚覆蓋地板的問題:
某年夏天,位於希格瑪大廈四層的微軟亞洲研究院對辦公樓的天井進行了一次大 規模的裝修.原來的地板鋪有 N×M 塊正方形瓷磚,這些瓷磚都已經破損老化了,需要予以 更新.裝修工人們在前往商店選購新的瓷磚時,發現商店目前只供應長方形的瓷磚,現在的 一塊長方形瓷磚相當於原來的兩塊正方形瓷磚, 工人們拿不定主意該買多少了, 讀者朋友們 請幫忙分析一下:能否用 1×2 的瓷磚去覆蓋 N×M 的地板呢?
單看這個問題,其實挺簡單的,很明顯,只要N、M至少有一個能被2整除,便可以使問題成立。
但是在此,書中又提出了一個擴展問題:用 1×2 的瓷磚去覆蓋 8×8 的地板,有多少種方式?如果是 N×M 的地板呢?
其實,不難想到,只要解決了 N×M 的地板的一般性問題,前面的 8×8 的地板也就迎刃而解。但是,在此之前,還必須判斷原問題(能否用 1×2 的瓷磚去覆蓋 N×M 的地板),因爲在此前提下,右面的擴展問題纔有意義。
那好,下面就討論擴展問題的解題思路:
其實,對於 N×M 的地板中第row行pos列上的某個1×1的格子而言,在鋪瓷磚的時候會有以下3種狀態考慮:
該格子先空着不放,以備下一行來使用
根據上一行(row-1,pos)位置上是空着的,則豎着鋪一個瓷磚,佔領(row-1,pos)和(row,pos)位置。
在空間許可的前提下,橫着鋪一個瓷磚,佔領(row,pos)和(row,pos+1)位置。
那麼,如何記錄瓷磚不同的擺放呢??
我們假定,row行pos列上的格子若被佔領則爲1,沒佔領則爲0。例如,存在一個矩陣:
0 | 1 | 1 | 0 |
1 | 0 | 0 | 1 |
0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 |
但是我們不打算構造這樣的一個矩陣,而是將每行的0、1二進制值構成的數值作爲列值構造矩陣dp[n][2^(m-1)+1]。2^(m-1)可通過1<<m快速求出。矩陣dp[i][j]的存儲的值是第i行的j狀態時的擺放方式數目。例如,上面矩陣中的第一行0110,可使用dp[1][0110]即dp[1][6]來表示。
但是,如何來求矩陣dp[n][2^(m-1)+1]??
我們再看回上面的01矩陣,每一行可以有很多狀態,其對應的數值可以是0~2^(m-1),可能某一行的一個狀態可以與上一行的多個狀態兼容,例如:
矩陣一: 001111 矩陣二: 001100
110011 110011
111111 111111
由上面可以看出,兩個矩陣中,第二行的狀態同樣是110011,但可以分別與上一行的001111和001100兼容,當然,此時的擺放方式已經不一樣了。
由上面的例子不難想到,第二行的110011狀態對應的鋪排方式數目應該是能和其兼容的上一行的所有狀態所對應的鋪排方式數目之和,所以可以使用累加的方式計算dp,dp[row][state]=dp[row][state] + dp[row-1][x],其中x是指上一行的狀態,如001111、001100。
爲什麼是累加呢??
首先,非常重要的一點是,dp[row][state]含義是第row行的某個狀態與第1到row-1行中符合題意的狀態的組合數。譬如說,第一行符合題意的狀態有狀態11、狀態12、狀態13、狀態14、狀態15,第二行中與狀態11、狀態12、狀態13兼容的是狀態21,第二行中與狀態14、狀態15兼容的是狀態22,而第三行與狀態21、狀態22兼容的是狀態31。用圖就可將地板表示爲:
狀態11 狀態12 狀態13 狀態14 狀態15
狀態21 狀態21 狀態21 狀態22 狀態22
狀態31 狀態31 狀態31 狀態31 狀態31
初始化:dp[1][狀態11]=1,dp[1][狀態12]=1,dp[1][狀態13]=1,dp[1][狀態14]=1,dp[1][狀態15]=1,
dp[2][狀態21]=0,dp[2][狀態22]=0,dp[3][狀態31]=0。
計算過程:(累加)
(1)計算dp[2][狀態21]
dp[2][狀態21] = dp[2][狀態21] + dp[1][狀態11] = 0+1 = 1,
dp[2][狀態21] = dp[2][狀態21] + dp[1][狀態12] = 1+1 = 2,
dp[2][狀態21] = dp[2][狀態21] + dp[1][狀態13] = 2+1 = 3。
(2)計算dp[2][狀態22]
dp[2][狀態22] = dp[2][狀態22] + dp[1][狀態14] = 0+1 = 1,
dp[2][狀態22] = dp[2][狀態22] + dp[1][狀態15] = 1+1 = 2。
(3)計算dp[3][狀態31]
dp[3][狀態31] = dp[3][狀態31] + dp[2][狀態22] = 0+3 = 3,
dp[3][狀態31] = dp[3][狀態31] + dp[2][狀態22] = 3+2 = 5。
實際上,dp[3][狀態31]的數值就是鋪排的方式的數目,正如上面圖上的5個地板。
算法過程:
首先初始化第一行,利用之前討論的3種情況,找出第一行中符合題意的狀態。例如,000001就不符合題意,不可能第一行上就只出現一個1。
對於第k行(2<=k<=n),從pos位置上(0<=pos<=M-1),考慮之前討論的3種情況
(1)該位置寫0(意義是空着,以備下一行使用)
(2)如果上一行(第k-1行)該位置上是0,則考慮下一位置(相當於一個子問題);
(3)在pos<=M-2的前提下,將該位置和下一位置即是pos、pos+1上寫1,然後再考率pos+2位置;
在考慮以上3個情況之前,首先判斷pos是否等於M(等於M說明之前的位置討論已經到達M-1),這時,該行的狀態已經確定,那麼就按dp[row][state]=dp[row][state] + dp[row-1][x]求解,其中x是指上一行的狀態。
其實,該算法是,通過調整相應pos位置上的01值,考慮當前一行哪個狀態可以與上一行的狀態兼容,而累計當前一行的可以兼容的狀態的dp上。但是可以看出,對於最後一行來說,肯定要全部位置寫1才行,因爲無論是豎着放、還是橫着放,最後一行的位置上必須是1,因爲最後一行已經不容許再空着了。所以,最後一行的狀態是1111...111,也就是該狀態記錄了整個地板的dp。
具體代碼如下:轉自http://blog.csdn.net/limchiang/article/details/8619611
#include <stdio.h> #include <string.h> /** n * m 的地板 */ int n,m; /** dp[i][j] = x 表示使第i 行狀態爲j 的方法總數爲x */ __int64 dp[12][2049]; /* 該方法用於搜索某一行的橫向放置瓷磚的狀態數,並把這些狀態累加上row-1 行的出發狀態的方法數 * @name row 行數 * @name state 由上一行決定的這一行必須放置豎向瓷磚的地方,s的二進制表示中的1 就是這些地方 * @name pos 列數 * @name pre_num row-1 行的出發狀態爲~s 的方法數 */ void dfs( int row, int state, int pos, __int64 pre_num ) { /** 到最後一列 */ if( pos == m ){ dp[row][state] += pre_num; return; } /** 該列不放 */ dfs( row, state, pos + 1, pre_num ); /** 該列和下一列放置一塊橫向的瓷磚 */ if( ( pos <= m-2 ) && !( state & ( 1 << pos ) ) && !( state & ( 1 << ( pos + 1 ) ) ) ) dfs( row, state | ( 1 << pos ) | ( 1 << ( pos + 1 ) ), pos + 2, pre_num ); } int main() { while( scanf("%d%d",&n,&m) && ( n || m ) ){ /** 對較小的數進行狀壓,已提高效率 */ if( n < m ){ n=n^m; m=n^m; n=n^m; } memset( dp, 0, sizeof( dp ) ); /** 初始化第一行 */ dfs( 1, 0, 0, 1 ); for( int i = 2; i <= n; i ++ ) for( int j = 0; j < ( 1 << m ); j ++ ){ if( dp[i-1][j] ){ __int64 tmp = dp[i-1][j]; /* 如果i-1行的出發狀態某處未放,必然要在i行放一個豎的方塊, * 所以我對上一行狀態按位取反之後的狀態就是放置了豎方塊的狀態 */ dfs( i, ( ~j ) & ( ( 1 << m ) - 1 ), 0, tmp ) ; } else continue; } /** 注意並不是循環i 輸出 dp[n][i]中的最大值 */ printf( "%I64d\n",dp[n][(1<<m)-1] ); } return 0; }
程序說明
1.初始化第一行,dfs(1,0,0,1),因爲第一行沒有上一行了,而在累加的時候實際上加的就是符合題意的狀態數目。
2.接下來的兩層for循環,就是遍歷所有行上的所有狀態,並根據上一行的情況來考慮,可以看到其代碼是
dfs( i, ( ~j ) & ( ( 1 << m ) - 1 ), 0, tmp ) ;
對上一行的狀態取反,其實是把上一行爲0的位置,也就是空着的位置所對應的下一行的該位置寫1,因爲該上一行該位置空着是想填一個豎着的瓷磚,所以可以直接確定該位置。而後面與上了( ( 1 << m ) - 1 )是爲了j在超過m的位置上都爲0。還有dp[][]的某個值爲0時,說明該狀態不可能出現,所以不用考慮。
3.在dfs函數中,就是根據之前討論的3種情況來實現。
對與第二部分 dfs( row, state, pos + 1, pre_num ); 其實是該pos位置不管了,原來是1就是1,是0就是0。直接考慮下一位置。爲什麼這樣呢?因爲如果該位置是0的話,那就是默認的0,考慮的就是該位置空着的情況,但如果該位置是1的話,那就是上一行該位置是0,已經空着了,而這一行該位置必須爲1,在調用之前經過取反操作已經置爲1,我們不用管了。
對於第三部分,考慮在改行上橫放一個瓷磚
if
(( pos <= m-2 ) && !( state & ( 1 << pos ) ) && !( state & ( 1 << ( pos + 1 ) ) ))
dfs(row, state | ( 1 << pos ) | ( 1 << ( pos + 1 ) ), pos + 2, pre_num);
根據之前的討論,肯定要判斷夠不夠位置橫放;其次還要判斷pos、pos+1位置上原來是不是1,因爲有些位置是因爲豎着放的緣故,已在取反操作中寫1了。如果不是1就可以橫放瓷磚了。
4.因爲最後一行所有位置上肯定都是1,所以最後結果在dp[n][(1<<m)-1]中