《编程之美》的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种状态考虑:

  1. 该格子先空着不放,以备下一行来使用

  2. 根据上一行(row-1,pos)位置上是空着的,则竖着铺一个瓷砖,占领(row-1,pos)和(row,pos)位置。

  3. 在空间许可的前提下,横着铺一个瓷砖,占领(row,pos)和(row,pos+1)位置。


那么,如何记录瓷砖不同的摆放呢??


我们假定,row行pos列上的格子若被占领则为1,没占领则为0。例如,存在一个矩阵:

0110
1001
0110
1111

但是我们不打算构造这样的一个矩阵,而是将每行的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个地板。


算法过程:


  1. 首先初始化第一行,利用之前讨论的3种情况,找出第一行中符合题意的状态。例如,000001就不符合题意,不可能第一行上就只出现一个1。

  2. 对于第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是指上一行的状态。

  3. 其实,该算法是,通过调整相应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]中