1. 程式人生 > >常見的動態規劃問題分析與求解

常見的動態規劃問題分析與求解

  動態規劃(Dynamic Programming,簡稱DP),雖然抽象後進行求解的思路並不複雜,但具體的形式千差萬別,找出問題的子結構以及通過子結構重新構造最優解的過程很難統一,並不像回溯法具有解決絕大多數問題的銀彈(全面解析回溯法:演算法框架與問題求解)。為了解決動態規劃問題,只能靠多練習、多思考了。本文主要是對一些常見的動態規劃題目的收集,希望能有所幫助。難度評級受個人主觀影響較大,僅供參考。

目錄(點選跳轉)

備忘錄法

參考資料

動態規劃求解的一般思路: 

  判斷問題的

子結構(也可看作狀態),當具有最優子結構時,動態規劃可能適用。

  求解重疊子問題。一個遞迴演算法不斷地呼叫同一問題,遞迴可以轉化為查表從而利用子問題的解。分治法則不同,每次遞迴都產生新的問題。

  重新構造一個最優解。

備忘錄法:

  動態規劃的一種變形,使用自頂向下的策略,更像遞迴演算法。

  初始化時表中填入一個特殊值表示待填入,當遞迴演算法第一次遇到一個子問題時,計算並填表;以後每次遇到時只需返回以前填入的值。

  例項可以參照矩陣鏈乘法部分。 

1.硬幣找零

難度評級:★

  假設有幾種硬幣,如1、3、5,並且數量無限。請找出能夠組成某個數目的找零所使用最少的硬幣數。 

解法:

  用待找零的數值k描述子結構/狀態,記作sum[k],其值為所需的最小硬幣數。對於不同的硬幣面值coin[0...n],有sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1。對應於給定數目的找零total,需要求解sum[total]的值。

typedef struct {
    int nCoin; //使用硬幣數量
    //以下兩個成員是為了便於構造出求解過程的展示
    int lastSum;//上一個狀態
    int addCoin;//從上一個狀態達到當前狀態所用的硬幣種類
} state;
state *sum = malloc(sizeof
(state)*(total+1)); //init for(i=0;i<=total;i++) sum[i].nCoin = INF; sum[0].nCoin = 0; sum[0].lastSum = 0; for(i=1;i<=total;i++) for(j=0;j<n;j++) if(i-coin[j]>=0 && sum[i-coin[j]].nCoin+1<sum[i].nCoin) { sum[i].nCoin = sum[i-coin[j]].nCoin+1; sum[i].lastSum = j; sum[i].addCoin = coin[j]; } if(sum[total].nCoin == INF) { printf("can't make change.\n"); return 0; } else //output
    ;

  通過sum[total].lastSum和sum[total].addCoin,很容易通過迴圈逆序地或者編寫遞迴呼叫的函式正序地輸出從結束狀態到開始狀態使用的硬幣種類。以下各題輸出狀態轉換的方法同樣,不再贅述。下面為了方便起見,有的題沒有在構造子結構的解時記錄狀態轉換,如果需要請類似地完成。

擴充套件:

(1)一個矩形區域被劃分為N*M個小矩形格子,在格子(i,j)中有A[i][j]個蘋果。現在從左上角的格子(1,1)出發,要求每次只能向右走一步或向下走一步,最後到達(N,M),每經過一個格子就把其中的蘋果全部拿走。請找出能拿到最多蘋果數的路線。

難度評級:★

分析:

  這道題中,當前位置(i,j)是狀態,用M[i][j]來表示到達狀態(i,j)所能得到的最多蘋果數,那麼M[i][j] = max(M[i-1][j],M[i][j-1]) + A[i][j] 。特殊情況是M[1][1]=A[1][1],當i=1且j!=1時,M[i][j] = M[i][j-1] + A[i][j];當i!=1且j=1時M[i][j] = M[i-1][j] + A[i][j]。

  求解程式略。

(2)裝配線排程(《演算法導論》15.1)

難度評級:★

  

2.字串相似度/編輯距離(edit distance)

難度評級:★

  對於序列S和T,它們之間距離定義為:對二者其一進行幾次以下的操作(1)刪去一個字元;(2)插入一個字元;(3)改變一個字元。每進行一次操作,計數增加1。將S和T變為同一個字串的最小計數即為它們的距離。給出相應演算法。

解法:

  將S和T的長度分別記為len(S)和len(T),並把S和T的距離記為m[len(S)][len(T)],有以下幾種情況:

如果末尾字元相同,那麼m[len(S)][len(T)]=m[len(S)-1][len(T)-1];

如果末尾字元不同,有以下處理方式

  修改S或T末尾字元使其與另一個一致來完成,m[len(S)][len(T)]=m[len(S)-1][len(T)-1]+1;

  在S末尾插入T末尾的字元,比較S[1...len(S)]和S[1...len(T)-1];

  在T末尾插入S末尾的字元,比較S[1...len(S)-1]和S[1...len(T)];

  刪除S末尾的字元,比較S[1...len(S)-1]和S[1...len(T)];

  刪除T末尾的字元,比較S[1...len(S)]和S[1...len(T)-1];

  總結為,對於i>0,j>0的狀態(i,j),m[i][j] = min( m[i-1][j-1]+(s[i]==s[j])?0:1 , m[i-1][j]+1, m[i][j-1] +1)。

  這裡的重疊子結構是S[1...i],T[1...j]。

  以下是相應程式碼。考慮到C語言陣列下標從0開始,做了一個轉化將字串後移一位。

#include <stdio.h>
#include <string.h>
#define MAXLEN 20
#define MATCH 0
#define INSERT 1
#define DELETE 2

typedef struct {
    int cost;
    int parent;
} cell;

cell m[MAXLEN+1][MAXLEN+1];

int match(char a,char b)
{
    //cost of match
    //match:    0
    //not match:1
    return (a==b)?0:1;
}

int string_compare(char *s,char *t)
{
    int i,j,k;
    int opt[3];
    
    //row_init(i);
    for(i=0;i<=MAXLEN;i++) {
        m[i][0].cost = i;
        if(i==0)
            m[i][0].parent = -1;
        else
            m[i][0].parent = INSERT;
    }

    //column_init(i);
    for(i=0;i<=MAXLEN;i++) {
        m[0][i].cost = i;
        if(i==0)
            continue;
        else
            m[0][i].parent = INSERT;
    }
    
    char m_s[MAXLEN+1] = " ",m_t[MAXLEN+1] =" ";
    strcat(m_s,s);
    strcat(m_t,t);

    for(i=1;i<=strlen(s);i++)
    {
        for(j=1;j<=strlen(t);j++) {
            opt[MATCH] = m[i-1][j-1].cost + match(m_s[i],m_t[j]);
            opt[INSERT] = m[i][j-1].cost + 1;
            opt[DELETE] = m[i-1][j].cost + 1;
            m[i][j].cost = opt[MATCH];
            m[i][j].parent = MATCH;
            for(k=INSERT;k<=DELETE;k++)
                if(opt[k]<m[i][j].cost)
                {
                    m[i][j].cost = opt[k];
                    m[i][j].parent = k;
                }
        }
    }
    i--,j--;
    //goal_cell(s,t,&i,&j);
    return m[i][j].cost;
}

int main() {
    char t[] = "you should not";
    char p[] = "thou shalt not";

    int n = string_compare(t,p);
    printf("%d\n",n);
}
字串相似度/edit distance

應用:

  (1)子串匹配

  難度評級:★★

  修改兩處即可進行子串匹配:

row_init(int i)
{
    m[0][i].cost = 0; /* note change */
    m[0][i].parent = -1; /* note change */
}

goal_cell(char *s, char *t, int *i, int *j)
{
    int k; /* counter */
    *i = strlen(s) - 1;
    *j = 0;
    for (k=1; k<strlen(t); k++)
        if (m[*i][k].cost < m[*i][*j].cost) *j = k;
}
修改部分

  如果j= strlen(S) - strlen(T),那麼說明T是S的一個子串。

  (這部分是根據《演算法設計手冊》8.2.4和具體例項Skiena與Skienaa、Skiena與somta的分析獲得的,解釋不夠全面,可能有誤,請注意)

  (2)最長公共子序列

  難度評級:★★

  將match時不匹配的代價轉化為最大長度即可:

int match(char c, char d)
{
    if (c == d) return(0);
    else return(MAXLEN);
}
match()

  此時,最小值是兩者不同部分的距離。

  (這部分同樣也不好理解,對於最長公共子序列,建議直接使用下一部分中的解法)

擴充套件:

  如果在編輯距離中各個操作的代價不同,如何尋找最小代價? 

3.最長公共子序列(Longest Common Subsequence,lcs)

難度評級:★

  對於序列S和T,求它們的最長公共子序列。例如X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A}則它們的lcs是{B,C,B,A}和{B,D,A,B}。求出一個即可。

解法:

  和2類似,對於X[1...m]和Y[1...n],它們的任意一個lcs是Z[1...k]。

  (1)如果X[m]=Y[n],那麼Z[k]=X[m]=Y[n],且Z[1...k-1]是X[1...m-1]和Y[1...n-1]的一個lcs;

  (2)如果X[m]!=Y[n],那麼Z[k]!=X[m]時Z是X[1...m-1]和Y的一個lcs;

  (3)如果X[m]!=Y[n],那麼Z[k]!=Y[n]時Z是X和Y[1...n-1]的一個lcs;

  下面是《演算法導論》上用偽碼描述的lcs演算法。其中c[i][j]記錄當前lcs長度,b[i][j]記錄到達該狀態的上一個狀態。

擴充套件1:

  如何輸出所有的LCS?

難度評級:★

分析:

  根據上面c[i,j]和b[i,j]的構造過程可以發現如果c[i-1,j]==c[i,j-1],那麼分別向上和向左返回的上一個狀態都是可行的。如果將其標記為“左/上”並通過遞迴呼叫來生成從c[m,n]到c[1,1]的所有路徑,就能找出所有的LCS。時間複雜度上界為O(mn)。

擴充套件2:

  通過LCS獲得最長遞增自子序列。

分析:

  對於1個序列,如243517698,最大值9,最小值1,那麼通過將它與123456789求LCS得到的就是最長連續遞增子序列23568。

  這種做法不適用於最長連續非遞減子序列,除非能獲得重複最多的元素數目,如2433517698,那麼可以用112233445566778899與之比較。

  使用專門的最長遞增子序列演算法可以進行優化,詳見下一部分。

4.最長遞增子序列(Longest Increasing Subsequence,lis)

難度評級:★

   對於一個序列如1,-1,2,-3,4,-5,6,-7,其最長第增子序列為1,2,4,6。

解法:

  除了利用3中lcs來求解,這裡使用求解lis問題的專門方法。

  先看看如何確定子結構的表示。對於長度為k的序列s[1...k],如果用lis[k]記錄這個序列中最長子序列似乎沒什麼用,因為在構造lis[k+1]時,需要比較s[k]與前面長度為lis[k]的lis的最後一個元素、s[1...k]中長度為lis[k]-1的序列的最後一個元素等等,沒有提供什麼便利,這個方案被否決。

  為了將每個lis[k]轉化為構造lis[k+1]時有用的資料,把子結構記為以s[k]為結尾的lis的長度,那麼對於s[k+1],需要檢查所有在它前面且小於它的元素s[i],並令lis[k+1] = max(lis[i]+1),(i=1 to k,s[k+1]>s[i])。這樣,一個O(n2)的演算法便寫成了。為了在處理完成後不必再一次遍歷lis[1...n],可以使用一個MaxLength變數儲存當前記錄中最長的lis。

typedef struct {
    int length;
    int prev;
} state;

//演算法核心
state *a = malloc(sizeof(state) * n);
for(i=0;i<n;i++) {
    a[i].length = 1;
    a[i].prev = -1;
}

for(i=1;i<n;i++)
    for(j=1;j<i;j++)
        if(array[i]>array[j] && a[i].length < a[j].length + 1)
        {
            a[i].length = a[j].length + 1;
            a[i].prev = j;
            if(a[i].length > max_length) {
                max_length = a[i].length;
                max_end = i;
            }
        }

擴充套件:

  求解lis的加速

 難度評級:★★

分析:

  在構造lis[k+1]的時候可以發現,對於s[k+1],真正有用的元素s[i]<s[k+1]且lis[i]最大。如果記錄了不同長度的lis的末尾元素,那麼對於新加入的元素s[k+1],找出前面比它小的且對應lis最長的,就是以s[k+1]為結尾的lis[k+1]的長度。

  可以發現使用陣列MaxV[1...MAXLENGTH]其中MaxV[i]表示長度為i的lis的最小末尾元素,完全可以在s[k+1]時進行lis[k+1]的更新。進一步地發現,其實lis[]陣列已經沒有用了,對於MaxV[1...MAXLENGTH]中值合法對應的最大下標,就是當前最長的lis,也即利用MaxV[]更新自身。

  同時,根據MaxV[]的更新過程,可以得出當i<j時,MaxV[i]<MaxV[j](假設出現了i>j且Max[i]=>Max[j]的情況,那麼在之前的處理中,在發現j長度的lis時,應用它的第i個元素來更新Max[i],仍會導致MaxV[i]<MaxV[j],這與這個現狀發生了矛盾,也即這個情況是不可能到達的)。這樣,在尋找小於s[k+1]的值時,可以使用二分查詢,從而把時間複雜度降低至O(nlogn)

int lis_ologn(int *array, int length) {
    int i, left,right,mid,max_len = 1;
    int *MaxV;
    if(!array)
        return 0;
    MaxV = (int*)malloc(sizeof(int)*(length+1));
    MaxV[0] = -1;
    MaxV[1] = array[0];

    for(i=1;i<length;i++){
        //尋找範圍是MaxV[1, ... , max_len]
        left = 1;
        right = max_len;
        //二分查詢MaxV中第一個大於array[i]的元素
        while(left<right) {
            mid = (left+right)/2;
            if(MaxV[mid]<=array[i])
                left = mid + 1;
            else if(MaxV[mid]>array[i])
                right = mid;
        }
        if((MaxV[right]>array[i])&&(MaxV[right-1]<array[i]))
            MaxV[right] = array[i];
        else if (MaxV[right]<array[i]) {
            MaxV[right+1] = array[i];
            max_len++;
        }
    }
    return max_len;
}

   在這個解法下,不妨考慮如何重構這個lis。

5.最大連續子序列和/積

難度評級:★

  輸入是具有n個數的向量x,輸出時輸入向量的任何連續子向量的最大和。

解法:

  這裡只把O(n)的動態規劃解法列在下面,其中只用一個變數儲存過去的狀態

int max_array_v4(int *array,int length) {
    int i;
    int maxsofar = NI;
    int maxendinghere = 0;
    for(i=0;i<length;i++) {
        maxendinghere = maxnum(maxendinghere + array[i],array[i]);
        //分析:maxendinghere必須包含array[i]
        //當maxendinghere>0且array[i]>0,maxendinghere更新為兩者和
        //當maxendinghere>0且array[i]<0,maxendinghere更新為兩者和
        //當maxendinghere<0且array[i]<0,maxendinghere更新為array[i]
        //當maxendinghere<0且array[i]>0,maxendinghere更新為array[i]
        maxsofar = maxnum(maxsofar,maxendinghere);
    }
    return maxsofar;
}

擴充套件1:

難度評級:★

  給定一個正浮點數陣列,求它的一個最大連續子序列乘積的值。

解法:

  對陣列中每個元素取對數,構成新的數列,在新的數列上使用求最大連續子序列的演算法。

  如果求對數開銷較大,建議使用擴充套件2的方法。

擴充套件2:

難度評級:★

  給定一個浮點數陣列,其值可正可負可零,求它的一個最大連續子序列乘積的值。(假定計算過程中,任意一個序列的積都不超過浮點數最大表示)

解法:

  在最大連續子序列和演算法的基礎上進行修改。由於負負得正,對於當前狀態array[k],需要同時計算出它的最大值和最小值。即:

  new_maxendinghere = max3(maxendinghere*array[k],minendinghere*array[k],array[k])

  new_minendinghere = min3(maxendinghere*array[k],minendinghere*array[k],array[k])

  此後對已遍歷部分的最大積進行更新:

  maxsofar = max(maxsofar,new_maxendinghere)

  如果不習慣用常數個變數來表示,可以看看http://blog.csdn.net/wzy_1988/article/details/9319897,再想想用陣列儲存是不是浪費了空間。(計算max[k]、min[k]只用到了max[k-1]、min[k-1],沒有必要儲存全部狀態)

6.矩陣鏈乘法

難度評級:★

  一個給定的矩陣序列A1A2...An計算連乘乘積,有不同的結合方法,並且在結合時,矩陣的相對位置不能改變,只能相鄰結合。根據矩陣乘法的公式,10*100和100*5的矩陣相乘需要做10*100*5次標量乘法。那麼對於維數分別為10*100、100*5、5*50的矩陣A、B、C,用(A*B)*C來計算需要10*100*5 + 10*5*500 =7500次標量乘法;而A*(B*C)則需要100*5*50+10*100*50=75000次標量乘法。

  那麼對於由n個矩陣構成的鏈<A1,A2,...,An>,對i-1,2,...n,矩陣Ai的維數為pi-1*pi,對乘積A1A2...An求出最小化標量乘法的加括號方案。

解法:

  儘管可以通過遞迴計算取1<=k<n使得P(n)=∑P(k)P(n-k),遍歷所有P(n)種方案,但這並不是一個高效率的解法。

  經過以上幾道題的鍛鍊,很容易看出,子結構是求Ai...Aj括號方法m[i][j]可遞迴地定義為

\[m[i][j]=\left\{\begin{matrix} 0& if \ i=j\\ \underset{i\leqslant k<j}{min}\begin{Bmatrix} m[i][k] + & m[k+1][j] +& p_{i-1}p_{k}p_{j} \end{Bmatrix} & if \ i<j \end{matrix}\right.\]

  這樣,只需利用子結構求解m[1][n]即可,並在在構造m[1][n]的同時,記錄狀態轉換。下面的程式碼展示了這個過程,不再仔細分析。

#include <stdio.h>
#include <stdlib.h>
#define ULT    2147483647

int print_optimal_parens(int **s,int i, int j)
{
    if (i==j)
        printf("A%d",i+1);
    else    {
        printf("(");
        print_optimal_parens(s,i,*(*(s+i)+j));
        print_optimal_parens(s,*(*(s+i)+j)+1,j);
        printf(")");
    }
    return 1;
}



int matrix_chain_order(int *p, int n) {
    int i,j,k,l,q;
    int **m, **s;
    m=(int **)malloc(n*sizeof(int*));
    for(i=0;i<n;i++)
        m[i]=(int*)malloc(n*sizeof(int));
    s=(int **)malloc(n*sizeof(int*));
    for(i=0;i<n;i++)
        s[i]=(int*)malloc(n*sizeof(int));

    for(i=0;i<n;i++)
        s[i][i] = 0;
//m,s可以被壓縮儲存在上三角矩陣

    for(l=2;l<=n;l++) {
        for (i=0;i<n-l+1;i++) {
            j = i+l-1;
            m[i][j] = ULT;
            for (k=i; k<j;k++)    {
                q = m[i][k] + m[k+1][j] + p[i-1+1]*p[k+1]*p[j+1];
                if (q<m[i][j])        {
                    m[i][j] = q;
                    s[i][j] = k;
                }
            }
        }
    }

/*display m[i][j]*/
//    for (i=0;i<n;i++) {
//        for (j=0;j<n;j++)
//            printf("%d ",m[i][j]);
//        printf("\n");
//    }
    print_optimal_parens(s,0,5);
    printf("\n");
    return 1;
}

int main() {
    int p[] = {30,35,15,5,10,20,25};
    int n;
    n = (sizeof(p)/sizeof(int))-1;
    matrix_chain_order(p,n);
    return 1;
}
矩陣鏈乘法

擴充套件:

  矩陣鏈乘法的備忘錄解法(偽碼),來自《演算法導論》第15章。

7.0-1揹包

難度評級:★★

  一個賊在偷竊一家商店時發現了n件物品,其中第i件值vi元,重wi磅。他希望偷走的東西總和越值錢越好,但是他的揹包只能放下W磅。請求解如何放能偷走最大價值的物品,這裡vi、wi、W都是整數。

解法:

  如果每個物品都允許切割並只帶走其一部分,則演變為部分揹包問題,可以用貪心法求解。0-1揹包問題經常作為貪心法不可解決的例項(可通過舉反例來理解),但可以通過動態規劃求解。

  為了找出子結構的形式,粗略地分析發現,對前k件物品形成最優解時,需要決策第k+1件是否要裝入揹包。但是此時剩餘容量未知,不能做出決策。因此把剩餘容量也考慮進來,形成的狀態由已決策的物品數目和剩餘容量兩者構成。這樣,所有狀態可以放入一個n*(W+1)的矩陣c中,其值為當前包中物品總價值,這時有

\[c[i][j]=\left\{\begin{matrix} c[i-1][j]& if \ w_{i}>j\\ \max\begin{Bmatrix} c[i-1][j-w_{i}]+v_{i} \ ,\ c[i-1][j] \end{Bmatrix} & if \ w_{i}\leqslant j \end{matrix}\right.\]

  根據這個遞推公式,很容易寫出求解程式碼。

#include <stdio.h>
#include <stdlib.h>

int package_dp(int *v,int *w,int n,int total) {
    int i,j,tmp1,tmp2;
    int **c = (int **)malloc((n+1)*sizeof(int *));
    for(i=0;i<n+1;i++)
        c[i]=(int *)malloc((total+1)*sizeof(int));
    for(i=0,j=0;j<total;j++)
        c[i][j] = 0;
    for(i=1;i<=n;i++) {
        c[i][0] = 0;
        for(j=1;j<=total;j++) {
            if(w[i]>j)
                c[i][j] = c[i-1][j];
            else {
                tmp1 = v[i]+c[i-1][j-w[i]];
                tmp2 = c[i-1][j];
                c[i][j]=(tmp1>tmp2?tmp1:tmp2);
            }
        }
    }
    printf("c[%d][%d]:%d\n",n,total,c[n][total]);
    return 0;
}

int main() {
    int v[] = {0,10,25,40,20,10};
    int w[] = {0,40,50,70,40,20};
    int total = 120;
    package_dp(v,w,sizeof(v)/sizeof(int)-1,total);
    return 0;
}
0-1揹包問題示例程式碼

8.有代價的最短路徑

難度評級:★★

  無向圖G中有N個頂點,並通過一些邊相連線,邊的權值均為正數。初始時你身上有M元,當走過i點時,需要支付S(i)元,如果支付不起表示不能通過。請找出頂點1到頂點N的最短路徑。如果不存在則返回一個特殊值,如果存在多條則返回最廉價的一條。限制條件:1<N<=100; 0<=M<=100 ; 對任意i, 0<=S[i]<=100。

解法:

  如果不考慮經過頂點時的花費,這就簡化成了一個一般的兩點間最短路徑問題,可以用Dijkstra演算法求解。加入了花費限制之後,就不能直接求解了。

  考察從頂點0到達頂點i的不同狀態,會發現它們之間的區別是:總花費相同但路徑長度不同、總花費不同但路徑長度不同。為了尋找最短路徑,必然要儲存到達i點的最短路徑;同時為了找到最小開銷,應該把到達i點的開銷也進行儲存。根據題目的數值限制,可以將總開銷作為到達頂點i的一個狀態區分。這樣,就可以把Min[i][j]表示為到達頂點i(並經過付錢)時還剩餘j元錢的最短路徑的長度。在此基礎上修改Dijkstra演算法,使其能夠儲存到達同一點不同花費時的最短長度,最終的Min[N-1][0...M]中最小的即為所求。以下是求解過程的虛擬碼。

//初始化
對所有的(i,j),Min[i][j] = ∞,state[i][j] = unvisited;
Min[0][M] = 0;

while(1) {
    for 所有unvisited的(i,j)找出M[i][j]最小的,記為(k,l)
    if Min[k][l] =break;

    state[k][l] = visited;
    for 所有頂點k的鄰接點p
        if (l-S[p]>=0 && Min[p][1-S[p]]>Min[k][l]+Dist[k][p])
            Min[p][1-S[p]] = Min[k][l]+Dist[k][p];

    //通過Dijstra演算法尋找不同花費下的最小路徑
}

for 所有j,找出Min[N-1][j]最小的
    如果存在多個j,那麼選出其中j最大的

9.瓷磚覆蓋(狀態壓縮DP)

難度評級:★★

用 1 * 2 的瓷磚覆蓋 n * m 的地板,問共有多少種覆蓋方式?

 解法:

  分析子結構,按行鋪瓷磚。一塊1*2瓷磚,橫著放對下一行的狀態沒有影響;豎著放時,下一行的對應一格就會被佔用。因此,考慮第i行的鋪法時只需考慮由第i-1行造成的條件限制。列舉列舉第i-1行狀態即可獲得i行可能的狀態,這裡為了與連結一文一致,第i-1行的某格只有兩個狀態:空或者放置。空表示第i行對應位置需要放置一個豎著的瓷磚,這時在鋪第i行時,除去限制以外,只需考慮放還是不放橫著的瓷磚這2種情況即可(不必分為放還是不放、橫到下一層還是豎著一共4種)。同時對於第i-1行的放法,用二進位制中0和1表示有無瓷磚,那麼按位取反恰好就是第i行的限制條件。

//原作者:limchiang
//出處: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;
}

10.工作量劃分

難度評級:★★

  假設書架上一共有9本書,每本書各有一定的頁數,分配3個人來進行閱讀。