1. 程式人生 > 實用技巧 ># 0x56 動態規劃-狀態壓縮DP

# 0x56 動態規劃-狀態壓縮DP

Mondriaan's Dream

Description

Squares and rectangles fascinated the famous Dutch painter Piet Mondriaan. One night, after producing the drawings in his ‘toilet series’ (where he had to use his toilet paper to draw on, for all of his paper was filled with squares and rectangles), he dreamt of filling a large rectangle with small rectangles of width 2 and height 1 in varying ways.

Expert as he was in this material, he saw at a glance that he’ll need a computer to calculate the number of ways to fill the large rectangle whose dimensions were integer values, as well. Help him, so that his dream won’t turn into a nightmare!

Input

The input contains several test cases. Each test case is made up of two integer numbers: the height h and the width w of the large rectangle. Input is terminated by h=w=0. Otherwise, 1<=h,w<=11.

Output

For each test case, output the number of different ways the given rectangle can be filled with small rectangles of size 2 times 1. Assume the given large rectangle is oriented, i.e. count symmetrical tilings multiple times.

Sample Input

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
Null

Sample Output

1
0
1
2
3
5
144
51205
Null

題意

給出一個 n×m 的方格,問用 1×2 的小方格來填充總共有多少種方法。

思路

我們定義 dp[i][j] 代表第 i-1 行已經放滿,第 i 行狀態為 j 時候的方案數。

其中每一行的狀態我們可以用一個二進位制來表示, 0 代表未填充, 1 代表已填充。

因為方塊有兩種擺放形式:豎放、橫放

所以當第 i 行第 j 列豎放一個方塊時,第 i-1 行第 j 列需要留空;而當第 i 行第 j 列與第 j+1 列橫放一個方塊時,第 i-1 行第 j 列與第 j+1 列則需已填充,因為我們定義的 dp 需要把第 i-1 行全部放滿。

狀態轉移方程: \(dp[i][s]=sum(dp[i−1][si])dp[i][s]=sum(dp[i−1][si])\) 其中狀態 s 與狀態 si 必須相容,也就是狀態 s 中豎放的塊能夠填滿狀態 si 中的空。

這裡有一個優化,也就是當 \(n×m\)結果為奇數的時候,無論怎樣都不可能成功放置,因為每一個塊的面積是偶數。

// https://www.dreamwings.cn/poj2411/4615.html 千千dalao的解法
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;

#define MAX ((1<<11)+10)
typedef __int64 LL;
int n,m;

LL dp[15][MAX];
LL ans[15][15];

bool jud(int x) // 判斷 x 二進位制中是否存在獨立的1
{
    bool is1=false;
    for(int i=0; i<m; i++)
    {
        if(x&(1<<i))
            is1=!is1;
        else if(is1)return false;
    }
    return true;
}

bool ok(int s, int ss)  //判斷狀態s與狀態ss是否相容
{
    for(int j=0; j<m; )
        if(s & (1<<j)) //第i行第j列為1
        {
            if( ss & (1<<j)) //第i-1行第j列也為1,那麼第i行必然是橫放
            {
                //第i行和第i-1行的第j+1都必須是1,否則是非法的
                if( j==m-1 || !(s&1<<(j+1)) || !(ss&(1<<(j+1))) ) return false;
                else  j+=2;
            }
            else j++; //第i-1行第j列為0,說明第i行第j列是豎放
        }
        else //第i行第j列為0,那麼第i-1行的第j列應該是已經填充了的
        {
            if(ss&(1<<j)) j++;//已經填充
            else return false;
        }
    return true;
}

void solve()
{
    int maxs;
    if(n<m)swap(n,m);   // 交換之後可以得到更小的狀態數
    maxs=(1<<m)-1;      // 狀態全1的情況
    memset(dp,0,sizeof(dp));
    for(int i=0; i<=maxs; i++)  // 初始化第一行
        dp[1][i]=jud(i);
    for(int c=2; c<=n; c++)     // 列舉 [2,n] 行
        for(int i=0; i<=maxs; i++)  // 第i行的狀態
            for(int si=0; si<=maxs; si++)   //第i-1行的狀態
                if(ok(i,si))
                    dp[c][i]+=dp[c-1][si];
    ans[n][m]=ans[m][n]=dp[n][maxs];
    printf("%I64d\n",dp[n][maxs]);
}

int main()
{
    memset(ans,0,sizeof(ans));
    while(cin>>n>>m&&(n||m))
    {
        if(ans[n][m])   //如果之前計算過,則直接給出結果
        {
            printf("%I64d\n",ans[n][m]);
            continue;
        }
        if(n&1&&m&1)    // 如果兩邊長都為奇數,則其面積也是奇數,無法放置
        {
            printf("0\n");
            continue;
        }
        solve();
    }
    return 0;
}
//學習演算法競賽提升指南的寫法
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[12][1 << 11];
int in_s[1 << 11];
int main() {
    //freopen("in.txt", "r", stdin);
    int n, m;
    while (cin >> n >> m && n) {
        for (int i = 0; i < 1 << m; ++i) {
            bool cnt = 0, has_odd = 0;
            for (int j = 0; j < m; ++j)
                if (i >> j & 1) has_odd |= cnt, cnt = 0;
                else cnt ^= 1;
            in_s[i] = has_odd | cnt ? 0 : 1;
        }
        f[0][0] = 1;
        for (int i = 1; i <= n; ++i)
            for (int j = 0; j < 1 << m; ++j) {
                f[i][j] = 0;
                for (int k = 0; k < 1 << m; ++k)
                    if ((j & k) == 0 && in_s[j | k])
                        f[i][j] += f[i - 1][k];
            }
        cout << f[n][0] << endl;
    }
}

炮兵陣地