狀態壓縮動態規划 狀壓DP


總述

狀態壓縮動態規划,就是我們俗稱的狀壓DP,是利用計算機二進制的性質來描述狀態的一種DP方式

很多棋盤問題都運用到了狀壓,同時,狀壓也很經常和BFS及DP連用,例題里會給出介紹

有了狀態,DP就比較容易了

舉個例子:有一個大小為n*n的農田,我們可以在任意處種田,現在來描述一下某一行的某種狀態:

設n = 9;
有二進制數 100011011(九位),每一位表示該農田是否被占用,1表示用了,0表示沒用,這樣一種狀態就被我們表示出來了:見下表

列 數 1 2 3 4 5 6 7 8 9
二進制 1 0 0 0 1 1 0 1 1
是否用 × × × ×

所以我們最多只需要 \(2^{n + 1} - 1\) 的十進制數就好(左邊那個數的二進制形式是n個1)

現在我們有了表示狀態的方法,但心里也會有些不安:上面用十進制表示二進制的數,枚舉了全部的狀態,DP起來復雜度豈不是很大?沒錯,狀壓其實是一種很暴力的算法,因為他需要遍歷每個狀態,所以將會出現2^n的情況數量,不過這並不代表這種方法不適用:一些題目可以依照題意,排除不合法的方案,使一行的總方案數大大減少從而減少枚舉

位運算

有了狀態,我們就需要對狀態進行操作或訪問

可是問題來了:我們沒法對一個十進制下的信息訪問其內部存儲的二進制信息,怎么辦呢?別忘了,操作系統是二進制的,編譯器中同樣存在一種運算符:位運算 能幫你解決這個問題

(基礎,這里不打算自己寫了,參照這篇博客,以下內容也復制自qxAi的這篇博客,這里謝謝博主)

為了更好的理解狀壓dp,首先介紹位運算相關的知識。

1.’&’符號,x&y,會將兩個十進制數在二進制下進行與運算,然后返回其十進制下的值。例如3(11)&2(10)=2(10)。

2.’|’符號,x|y,會將兩個十進制數在二進制下進行或運算,然后返回其十進制下的值。例如3(11)|2(10)=3(11)。

3.’^’符號,x^y,會將兩個十進制數在二進制下進行異或運算,然后返回其十進制下的值。例如3(11)^2(10)=1(01)。

4.’<<’符號,左移操作,x<<2,將x在二進制下的每一位向左移動兩位,最右邊用0填充,x<<2相當於讓x乘以4。相應的,’>>’是右移操作,x>>1相當於給x/2,去掉x二進制下的最有一位。

這四種運算在狀壓dp中有着廣泛的應用,常見的應用如下:

1.判斷一個數字x二進制下第i位是不是等於1。

方法:\(if ( ( ( 1 << ( i - 1 ) ) \& x ) > 0)\)

將1左移i-1位,相當於制造了一個只有第i位上是1,其他位上都是0的二進制數。然后與x做與運算,如果結果>0,說明x第i位上是1,反之則是0。

2.將一個數字x二進制下第i位更改成1。

方法:\(x = x | ( 1<<(i-1) )\)

證明方法與1類似,此處不再重復證明。

3.把一個數字二進制下最靠右的第一個1去掉。

方法:\(x=x\&(x-1)\)

感興趣的讀者可以自行證明。

位運算例題(結合BFS):P2622 關燈問題II

題目描述

現有n盞燈,以及m個按鈕。每個按鈕可以同時控制這n盞燈——按下了第i個按鈕,對於所有的燈都有一個效果。按下i按鈕對於第j盞燈,是下面3中效果之一:如果a[i][j]為1,那么當這盞燈開了的時候,把它關上,否則不管;如果為-1的話,如果這盞燈是關的,那么把它打開,否則也不管;如果是0,無論這燈是否開,都不管。

現在這些燈都是開的,給出所有開關對所有燈的控制效果,求問最少要按幾下按鈕才能全部關掉。

輸入輸出格式

輸入格式:
前兩行兩個數,n m

接下來m行,每行n個數,a[i][j]表示第i個開關對第j個燈的效果。

輸出格式:
一個整數,表示最少按按鈕次數。如果沒有任何辦法使其全部關閉,輸出-1


這題需要對狀壓及位運算有一定的了解:首先要判斷某一位的燈是開的還是關的,才能進行修改。

具體解法是:對隊首的某一狀態,枚舉每一個開關燈操作,記錄到達這一新狀態的步數(也就是老狀態 + 1),若是最終答案,輸出,若不是,壓入隊列。
也就是說:我們把初始狀態,用每個操作都試一遍,就產生了許多新的狀態,再用所有操作一一操作新狀態,就又產生了新的新狀態,我們逐一嘗試,直到有目標狀態為止,這可以通過BFS實現。

所以現在知道為什么狀壓比較暴力了吧。

#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
using namespace std;
int RD(){
    int out = 0,flag = 1;char c = getchar();
    while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
    while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
    return flag * out;
    }
const int maxn = 2048;
int num,m,numd;
struct Node{
    int dp,step;
    };
int vis[maxn];
int map[maxn][maxn];
void BFS(int n){
    queue<Node>Q;
    Node fir;fir.step = 0,fir.dp = n;//初始狀態入隊
    Q.push(fir);
    while(!Q.empty()){//BFS
        Node u = Q.front();
        Q.pop();
        int pre = u.dp;
        for(int i = 1;i <= m;i++){//枚舉每個操作
            int now = pre;
            for(int j = 1;j <= num;j++){
                if(map[i][j] == 1){
                    if( (1 << (j - 1)) & now){
                        now = now ^ (1 << (j - 1));//對狀態進行操作
                        }
                    }
                else if(map[i][j] == -1){
                    now = ( (1 << (j - 1) ) | now);//對狀態進行操作
                    }
                }
            fir.dp = now,fir.step = u.step + 1;//記錄步數
            if(vis[now] == true){
                continue;
                }
            if(fir.dp == 0){//達到目標狀態
                vis[0] = true;//相當於一個標記flag
                cout<<fir.step<<endl;//輸出
                return ;//退出函數
                }
            Q.push(fir);//新狀態入隊
            vis[now] = true;//表示這個狀態操作過了(以后在有這個狀態就不用試了)
            }
        }
    }
int main(){
    num = RD();m = RD();
    int n = (1 << (num)) - 1;
    for(int i = 1;i <= m;i++){
        for(int j = 1;j <= num;j++){
            map[i][j] = RD();
            }
        }
    BFS(n);
    if(vis[0] == false)
        cout<<-1<<endl;
    return 0;
    }

狀壓 + DP = 狀壓DP

同樣也是一種挺暴力的DP方式,我們直接看題吧

P1879 [USACO06NOV]玉米田Corn Fields

題目描述

Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can't be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.

Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.

農場主John新買了一塊長方形的新牧場,這塊牧場被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一塊正方形的土地。John打算在牧場上的某幾格里種上美味的草,供他的奶牛們享用。

遺憾的是,有些土地相當貧瘠,不能用來種草。並且,奶牛們喜歡獨占一塊草地的感覺,於是John不會選擇兩塊相鄰的土地,也就是說,沒有哪兩塊草地有公共邊。

John想知道,如果不考慮草地的總塊數,那么,一共有多少種種植方案可供他選擇?(當然,把新牧場完全荒廢也是一種方案)

輸入輸出格式

輸入格式:
第一行:兩個整數M和N,用空格隔開。

第2到第M+1行:每行包含N個用空格隔開的整數,描述了每塊土地的狀態。第i+1行描述了第i行的土地,所有整數均為0或1,是1的話,表示這塊土地足夠肥沃,0則表示這塊土地不適合種草。

輸出格式:
一個整數,即牧場分配總方案數除以100,000,000的余數。


其實這題是可以減少狀態來達到減少復雜度的,可是我寫着題的時候還不會。。。關於減少復雜度可以看下面一篇例題

這題也是用二進制來表示狀態,先預處理:枚舉一行內(不考慮地圖因素)每一種狀態,看看是否合法,合法的話就打個標記(其實這里可以減狀態數的那時候還不知道QAQ),以便后面操作,最后先處理第一行,枚舉下面行的時候再枚舉一遍上一行,看看兩行放置是否合法,合法就累計上一行計數即可。

#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
using namespace std;
int RD(){
    int out = 0,flag = 1;char c = getchar();
    while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
    while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
    return flag * out;
    }
const int maxn = 4096,M = 100000000;
int n,m;
int tmap[19][19];
int map[19];
int dp[19][maxn];
bool can[maxn];

int main(){
    n = RD(),m = RD();
    for(int i = 1;i <= n;i++){
        for(int j = 1;j <= m;j++){
            tmap[i][j] = RD();
            map[i] = (map[i] << 1) + tmap[i][j];//利用把地圖變為二進制的形式可以快速計算是否合法,要對位運算熟悉掌握
            }
        }
    int maxstate = (1 << m) - 1;//最大狀態數
    for(int i = 0;i <= maxstate;i++){
        if((((i << 1) & i) == 0) & (((i >> 1) & i) == 0)){
            can[i] = true;//后面有更優的寫法,看下一篇題目
            }
        }
    for(int i = 0;i <= maxstate;i++){
        if((can[i]) & ((i & map[1]) == i)){
            dp[1][i] = 1;//先預處理出第一行(對於某一行的狀態,只受上一行影響)
            }
        }
    for(int i = 2;i <= n;i++){
        for(int j = 0;j <= maxstate;j++){
            if((can[j]) & (j & map[i]) == j){
                for(int k = 0;k <= maxstate;k++){
                    if((k & j) == 0){
                        dp[i][j] = (dp[i][j] + dp[i - 1][k]) % M;//dp過程
                        }
                    }
                }
            }
        }
    long long ans = 0;
    for(int i = 0;i <= maxstate;i++){
        ans = (ans + dp[n][i]) % M;//答案在最后一行
        }
    cout<<ans<<endl;
    return 0;
    }

P1896 [SCOI2005]互不侵犯King

題目描述

在N×N的棋盤里面放K個國王,使他們互不攻擊,共有多少種擺放方案。國王能攻擊到它上下左右,以及左上左下右上右下八個方向上附近的各一個格子,共8個格子。

輸入輸出格式

輸入格式:
只有一行,包含兩個數N,K ( 1 <=N <=9, 0 <= K <= N * N)

輸出格式:
所得的方案數


題目十分簡短:觀察數據范圍我們可以知道:這題有龐大的狀態量,所以我們就用狀壓DP解決問題

dp思路:三維,第一維表示行數,第二維表示狀態(二進制),第三維表示已經放了的棋子數(說實話做題做多了會有套路的,有數量限制的dp一般都要開一維表示用了的數量)

直接上代碼:

#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
int RD(){
    int out = 0,flag = 1;char c = getchar();
    while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
    while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
    return flag * out;
    }
int len,k;
    
ll dp[19][1024][110];
int need[1024];//表示每種狀態用的棋子數
bool can[1024];
int main(){
    len = RD();k = RD();
    int maxstate = (1 << len) - 1;
    for(int i = 0;i <= maxstate;i++){
        int temp = i;
        while(temp != 0){
            if(temp % 2 == 1)need[i] += 1;//處理所需棋子數
            temp /= 2;
            }
        }
    for(int i = 0;i <= maxstate;i++){
        if(((i << 1) & i) == 0){
            can[i] = true;//處理一行內不沖突的情況
            }
        }
    for(int i = 0;i <= maxstate;i++){
        if(can[i] & need[i] <= k)dp[1][i][need[i]] = 1;//預處理第一行
        }
    for(int i = 2;i <= len;i++){
        for(int j = 0;j <= maxstate;j++){
            if(can[j]){
                for(int s = 0;s <= maxstate;s++){
                    if(can[s] == false)continue;
                    if((s & j) != 0)continue;//正面上我啊
                    if(((s << 1) & j) != 0)continue;//左邊上我啊
                    if(((s >> 1) & j) != 0)continue;//右邊上我啊
                    for(int l = k;l >= need[j];l--){
                        dp[i][j][l] += dp[i - 1][s][l - need[j]];
                        }
                    }
                }
            }
        }
    ll ans = 0;
    for(int i = 0;i <= maxstate;i++){
        ans += dp[len][i][k];
        }
    cout<<ans<<endl;
    return 0;
    }

通過題目要求減少狀態量

這可以說是狀壓的一大精華了。一般狀壓的題目會有大量的狀態,枚舉所有狀態則需要大量的時間,時間承受不了,若和dp結合起來,dp數組開個三四維,空間也吃不消。

所以我們可以通過預處理狀態,去掉不合法的狀態,減少時空的需要

具體實現和STL中的map很相似:我們用一個序號來映射狀態,開一個數組INDEX[ ](這里有坑,小寫的index會和cstring庫沖突,如果給用的話我絕對用小寫魔禁萬歲!!!(雖然我站上琴) )INDEX[i]表示第i個合法的狀態是什么,然后枚舉的時候直接枚舉INDEX數組就好了

P2704 [NOI2001]炮兵陣地

題目描述

司令部的將軍們打算在NM的網格地圖上部署他們的炮兵部隊。一個NM的地圖由N行M列組成,地圖的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示),如下圖。在每一格平原地形上最多可以布置一支炮兵部隊(山地上不能夠部署炮兵部隊);一支炮兵部隊在地圖上的攻擊范圍如圖中黑色區域所示:

如果在地圖中的灰色所標識的平原上部署一支炮兵部隊,則圖中的黑色的網格表示它能夠攻擊到的區域:沿橫向左右各兩格,沿縱向上下各兩格。圖上其它白色網格均攻擊不到。從圖上可見炮兵的攻擊范圍不受地形的影響。 現在,將軍們規划如何部署炮兵部隊,在防止誤傷的前提下(保證任何兩支炮兵部隊之間不能互相攻擊,即任何一支炮兵部隊都不在其他支炮兵部隊的攻擊范圍內),在整個地圖區域內最多能夠擺放多少我軍的炮兵部隊。

輸入輸出格式

輸入格式:
第一行包含兩個由空格分割開的正整數,分別表示N和M;

接下來的N行,每一行含有連續的M個字符(‘P’或者‘H’),中間沒有空格。按順序表示地圖中每一行的數據。N≤100;M≤10。

輸出格式:
僅一行,包含一個整數K,表示最多能擺放的炮兵部隊的數量。


自己推一下就可以發現,判斷此行是否合法需要枚舉上一行和上兩行的狀態,(dp要開三維:第一維表示行數,第二維表示現在枚舉的狀態,第三維表示上一行的狀態,所以dp[i][j][k]表示第i行排成j個狀態,且上一行狀態是k的最大數量),直接枚舉所有狀態是肯定會超時的,這時候我們就需要通過題目要求減少狀態量了。

減少狀態量做法上面已經提過了,其他做法與普通狀壓類似。

總結一下此類題目的dp方法(玉米田也是這類問題):若某個狀態可以對下n行的狀態造成影響,那么就要預處理前n行合法的,對於n + 1行及以后,判斷某狀態是否合法需要往上枚舉n行,所以dp數組要開n + 1維,第一維表示行數,第二維表示現在的狀態,再往后第n維表示上n - 2行的狀態(其實不可能出太多行的,時間指數增長)

這樣dp就這樣進行:

for(所有狀態)
	for(所有狀態)
    	...{向上枚舉n行}
        	dp[i][j][k][l]...[n + 1] += dp[i - 1][k][l]...[最上面一行];
            //求最大方案數就max()
            //意會吧,不怎么講得清楚

AC代碼:

#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
int RD(){
    int out = 0,flag = 1;char c = getchar();
    while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
    while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
    return flag * out;
    }
const int maxn = 110;
int lenx,leny;
ll dp[110][maxn][maxn];
bool can[maxn];
bool cann[110][maxn];
int tmap[110][19];
int map[maxn];
int put[maxn];
int INDEX[maxn];
int cnt;
char in;
int main(){
    lenx = RD();leny = RD();
    for(int i = 1;i <= lenx;i++){
        for(int j = 1;j <= leny;j++){
            cin>>in;
            if(in == 'P')tmap[i][j] = 1;
            }
        }
    for(int i = 1;i <= lenx;i++){
        for(int j = 1;j <= leny;j++){
            map[i] = (map[i] << 1) + tmap[i][j];//和玉米田類似,處理為二進制地圖
            }
        }
    int maxstate = (1 << leny) - 1;
    for(int i = 0;i <= maxstate;i++){//枚舉一行里的狀態
        if((((i << 1) & i) == 0) & (((i << 2) & i) == 0)){
            INDEX[++cnt] = i;//合法的存在INDEX里,最終cnt表示合法方案數
            can[cnt] = true;
            int temp = i;
            while(temp != 0){
                if(temp % 2 == 1){put[cnt] += 1;}
                temp /= 2;
                }
            }
        }
    for(int i = 1;i <= cnt;i++){//第一行
        if(can[i] & ((INDEX[i] & map[1]) == INDEX[i])){
            cann[1][i] = true;
            dp[1][i][0] = put[i];
            }
        }
    for(int i = 1;i <= cnt;i++){
        if(can[i] & ((INDEX[i] & map[2]) == INDEX[i])){//選一個第二行合法的
            cann[2][i] = true;//標記一下合法,減少再計算
            for(int j = 1;j <= cnt;j++){//在第一行找一個
                if(!cann[1][j])continue;//要在第一行合法
                    if((INDEX[i] & INDEX[j]) == 0){//還要不與第二行沖突
                    dp[2][i][j] = max(dp[2][i][j],dp[1][j][0] + put[i]);
                    }
                }
            }
        }
    for(int i = 3;i <= lenx;i++){
        for(int j = 1;j <= cnt;j++){
            if(can[j] & ((INDEX[j] & map[i]) == INDEX[j])){
                cann[i][j] = true;
                for(int k = 1;k <= cnt;k++){//枚舉上兩行狀態
                    if(!cann[i - 2][k])continue;
                    if(!((INDEX[j] & INDEX[k]) == 0))continue;
                    for(int l = 1;l <= cnt;l++){
                        if(!cann[i - 1][l])continue;//枚舉上一行狀態
                        if(((INDEX[j] & INDEX[l]) != 0) || ((INDEX[k] & INDEX[l]) != 0))continue;
                        dp[i][j][l] = max(dp[i][j][l],dp[i - 1][l][k] + put[j]);
                        }
                    }
                }
            }
        }
    ll ans = 0;
    for(int i = 1;i <= cnt;i++){
        for(int j = 1;j <= cnt;j++){
            ans = max(ans,dp[lenx][i][j]);
        }
    }
    cout<<ans<<endl;
    return 0;
    }


免責聲明!

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



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