[AcWing], 蒙德里安的夢想

_ 菜 -∞發表於2020-10-11

題目來源

https://www.acwing.com/problem/content/description/293/

在這裡插入圖片描述

問題分析

就是將一個n * m的二維矩陣,分成若干個1 * 2的方格,有多少種分配方式(完全分配)

可以對放置的方式進行模擬,先放置橫著的1* 2方格,再放置豎著的2 * 1方格。那麼擺放的小方格方案數 等價於 橫著擺放的小方格方案數,因為當橫著合法擺放的方格確定後,豎著擺放的方式就已經確定了,直接內嵌。

他的資料範圍為1~11,暗示可以使用二進位制來進行操作,狀態壓縮。那麼我們選擇對每一列的狀態用一個二進位制進行表示,噹噹前行所在的bit為0時,表示該位置沒有放置;bit為1時,表示當前位置已經進行了放置。

在這裡插入圖片描述

列號狀態表示
00001
11001
21010
30010

所使用的變數
在這裡插入圖片描述

預處理合法的擺放方式

所以,我們在判斷橫著擺放方格的時候,就需要提前判斷合法的擺放方式,防止豎著的方格放完後,不能完全鋪滿整個矩陣。
在這裡插入圖片描述
那麼預處理的方法就是,對於每一種狀態(二進位制),然後依次遍歷每一行,在遍歷的途中需要記錄的就是連續的沒有放置的方格數(連續的0),當出現連續的0位奇數個時,表示該列的這種狀態是不合法的。

在這裡插入圖片描述

動態規劃

在動態規劃中,我們從每一列開始遍歷,然後從0 ~ 2^n依次遍歷每一個狀態。對於每一個j狀態,我們需要找到一個他可以和哪個狀態進行轉換。

在這裡插入圖片描述
那麼對於當前列在遍歷時的狀態,我們需要知道這個狀態可以在前面的哪一個狀態的情況後插入。如果前面當前行i-1是1,表示現在這一行i1*2方格的後半段,所以對於當前狀態是不能插入的。

在判斷當前狀態是否合法時,可以使用 &來操作,如果結果為0,表示沒有衝突。

除此之外,之前還做了一次預處理,判斷當前狀態是否合法。那麼這個合法的狀態就需要使用|來進行操作。
在這裡插入圖片描述

這樣會出現一個小問題,就是這樣問題的時間複雜度為O( 11 * 2^n * 2^n ),那麼對於最壞的情況下,結果為 11 * 2^11 * 2^11 近似於 5 * 10^7,這是一個臨界點,在超時的邊緣徘徊。

(i & j) == 0 && st[i | j] 這兩個條件換個位置後,就會超時)

在這裡插入圖片描述

所以我們可以將找狀態集的過程提取出來,減少在三重迴圈中的判斷次數。

初始化狀態集

將結果儲存在一個vector的可變陣列中
在這裡插入圖片描述

結果

棋盤一共有0 ~ m-1列

  • f[i][j]表示 前i-1列的方案數已經確定,從i-1列伸出,並且第i列的狀態是j的所有方案數
  • f[m][0]表示 前m-1列的方案數已經確定,從m-1列伸出,並且第m列的狀態是0的所有方案數

也就是m列不放小方格,前m-1列已經完全擺放好並且不伸出來的狀態
在這裡插入圖片描述

完整程式碼

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <vector>

using namespace std;
typedef long long ll;

const int N = 12, M = 1 << N; // 1-11 列,2^11 個狀態
ll f[N][M];             // f[i][j] 從第i-1列伸出,到i列,狀態為j,  j 的二進位制位為1表示當前行放置方格,為0表示不放置
vector<int> state[M];   // 每一種狀態,對應可以放置的狀態
bool st[M];             // 是否可以成功轉移,j 狀態放置 k狀態後 是否合法

int n, m;

int main() {
    while(cin >> n >> m, n || m) {
        
        // 初始化
        for(int i = 0; i < (1 << n); i ++) {
            int cnt = 0;    // 前面連續0的數量
            bool is_valid = true;
            
            for(int j = 0; j < n; j ++) {
                if((i >> j) & 1) {      // 1 放置
                    if(cnt & 1) {       // 前面連續0位奇數,不合法
                        is_valid = false;
                        break;
                    }
                    cnt = 0;
                } else {    // 0 不放置,連續0 + 1
                    cnt++;
                }
            }
            
            if(cnt & 1) is_valid = false;   // 餘下0位奇數個,不合法
            st[i] = is_valid;
        }
        
        for(int i = 0; i < (1 << n); i ++) {
            state[i].clear();
            for(int j = 0; j < (1 << n); j ++) 
                if((i & j) == 0 && st[i | j])   state[i].push_back(j);  // 對於狀態i來說,可以插入的狀態為j
        }
        
        memset(f,0x00,sizeof(f));
        f[0][0] = 1;
        for(int i = 1; i <= m; i ++)
            for(int j = 0; j < (1 << n); j++)
                for(auto& k : state[j]) f[i][j] += f[i - 1][k];
        
        cout << f[m][0] << endl;
    }
    return 0;
}

相關文章