狀態壓縮動態規划學習筆記


狀態壓縮動態規划學習筆記

算法介紹

狀態壓縮動態規划是近些年來NOIP提高組常考的算法,也是日后ACM必備的算法之一,因此我們有必須要學習此類高級算法.而且此類算法往往是NP算法的最強優化之一.


算法思想

狀態壓縮動態規划,顧名思義也就是,將動態規划中的狀態數組進行了壓縮.
那么想到壓縮,我們不免就要想到一種常用的時間空間優化技巧,或者說一種特殊的算法,也就是位運算.

卡常算法就是它,高端暴力就是它,奇跡算法還是它.

位運算,也就是二進制的運算,而且我們的二進制,是一種計算機中最為核心的編碼,這也就是為什么,電腦對於這種編碼運算速度最快.

狀態壓縮動態規划,就是利用了位運算,的這三大優化性質,來起到簡化代碼,優化代碼,解決難題的目的.


位運算基礎

& | ^ << >>
中文意思 異或 右移 左移
舉例說明 1&1=1 1|0=1 1^0=1 1<<1=10 10>>1=1
1&0=0 0|0=0 1^1=0 11<<1=110 101>>1=10

QQ截圖20190429202509.png

判斷算法

首先拿到一道題目,我們第一步就是要看數據范圍,題目描述. 而且當你發現數據范圍和題目描述具有以下三大特點的時候,那么我們就可以初步判斷這道題目需要使用狀態壓縮動態規划.

  1. 數據中的N,M范圍很小,基本上不超過30,20.(N,M廣義理解)
  2. 題目似乎要我們求方案數,或者說極值問題.
  3. 題目似乎是個棋盤覆蓋這種類型的問題.

算法處理

一般來說,狀態壓縮動態規划算法,最為困難,也是最為關鍵的一步,就在於

\[{\color{red}{狀態如何以二進制表示}} \]

那么接下來我就來詳細解說,狀態壓縮的狀態到底如何設置.

一般來說狀態設置,往往是一個整數,表示一個二進制決策集合.

比如說13,它就可以表示為1011,那么我們一般來說可以表示第一個點,第二點,第四個點已經選擇這個意思.

因為我們可以確定算法為狀態壓縮,那么我們現在的主力攻擊,就是狀態設置,既然現在我們已經有了這個目標,顯然我們就是盡量地將題目的條件進行轉化,在這里我們具體以棋盤類型來分析.


對於條件而言的話,我們需要捕捉到關鍵點.

  1. 如果說題目中出現了這一個點不可以選擇,那么你的神經中樞第一時間就要條件反射地,對自己的內心說一句,這里是1.

這里是1到底是什么意思?其實這個意思,就是告訴我們這個點不可以選擇,我們可以通過開一個特殊數組來保存,那么到了以后,對於我們枚舉的一個決策集合,那么我們可以通過&運算,來判斷這個點是否可以選擇.

比如說我們現在要求第五個點不可以選擇,那么我們可以構造一個判斷數組.10000表示第五個點不可以選擇.

那么假如說我們當前枚舉的狀態決策集合是11000,這個意思是,我們當前選擇第四個點和第五個點.

那么我們可以通過&運算,來進行判斷.

11000的十進制表示為24,而我們10000表示為16.那么我們進行&運算.

24&16 ==> 11000 & 10000 ==> 16 ==> 10000

總結:所以說我們可以通過&運算,進行判斷是否選擇.同理,如果題目說必須選擇,顯然&運算也可以發揮作用.

其他運算操作以后再慢慢補充吧,我們先來幾道題目感受感受.


題目選講


第一題


題目描述

求把\(N \times M\)的棋盤分割成若干個\(1 \times 2\)的的長方形,有多少種方案。

例如當\(N=2,M=4\)時,共有5種方案。當\(N=2,M=3\)時,共有3種方案。

如下圖所示:

2411_1.jpg

輸入格式

輸入包含多組測試用例。

每組測試用例占一行,包含兩個整數N和M。

當輸入用例

\(N=0 M=0\)時,表示輸入終止,且該用例無需處理。

輸出格式

每個測試用例輸出一個結果,每個結果占一行。

數據范圍

\[1 \le N,M \le 11 \]

輸入樣例:

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

輸出樣例:

1
0
1
2
3
5
144
51205

題意解析

首先這道題目題意很好理解,我就不說一句話題意了,但是我們要明確這道題目給我們的信息.

  1. 條件:1*2的長方形,有豎着的,也有橫着的.
  2. 性質:棋盤性質,數據范圍很小
  3. 最后答案 所有的合法方案數

通過上面給我們的信息,我們不難判斷這道題目是一道狀態壓縮動態規划算法,那么接下來我們按照上面所說的,我們現在需要處理如何二進制化狀態.


算法解析

我們發現,對於任何一個方案而言,假如說我們現在目光定格在,某一行的話,換句話說把棋盤分成兩部分,那么我們似乎會發現什么有趣的東西.
狀態壓縮動態規划學習筆記1.png

我們發現如果說我們確定了第一排的紅色,那么顯然綠色也隨着確定了.

而且如果我們確定了第一排的紅色,那么同樣的我們的第二排一部分紅色也就絕對確定了.切記不是所有的紅色.

因此我們顯然可以認為紅色為1,綠色為0,因為這道題目除了紅色擺放,就是綠色擺放了,所以說這就是這道題目二進制的狀態轉化.


狀態處理

綜上所述,我們就可以把行號看作階段.
接着我們可以設\(f[i][j]\)表示為第i行決策集合為j.(j為二進制集合,不過用十進制存儲)

那么接下來我們就要看決策的條件了.

對於\(f[i-1][k]\)轉移到\(f[i][j]\)顯然是有兩個條件的.

  1. j&k==0 也就是說每個數字1的下面必須是數字0,否則無法放入1*2的豎着長方形
  2. j|k的二進制表示中,每一段連續的0個數必須為偶數,否則無法放入1*2的橫着長方形.
代碼解法
#include <bits/stdc++.h>
using namespace std;
const int N=11;
long long f[12][1<<N];
int n,m;
bool st[1<<N];
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m && n)
    {
        for(int i=0;i<1<<m;i++)//預處理,處理所有[0,2^m -1]內所有滿足二進制表示下每一段連續01都有偶數個的整數.
        {
            bool cnt=0,odd=0;
            for(int j=0;j<m;j++)
                if (i>>j & 1)//判斷第j是否選擇了
                    odd|=cnt,cnt=0;
                else
                    cnt^=1;
            st[i]=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 && st[j|k])//滿足上面說的條件
                        f[i][j]+=f[i-1][k];//轉移過來了
            }
        cout<<f[n][0]<<endl;//最后的結果
    }
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM