1. 程式人生 > >狀態壓縮DP入門(算入了個門吧)

狀態壓縮DP入門(算入了個門吧)

感謝博主無私分享

狀態壓縮鼻祖: n皇后問題

描述:

傳統做法是對每一行 i,暴力查詢每一列 j 。 主要浪費時間在於檢查是否有皇后會攻擊,又花費o(n)。因此一共是o(n^3)。

改進演算法引入狀態壓縮,依舊是每行i , 每列j 跑不掉的 o(n^2) ,不過判斷上面可以用位運算花費o(1),實現複雜度降低

那麼位運算是如何用於判斷某個位置上是否收到攻擊的呢? 舉個例子吧

第1行放置的情況   1 0 0 0

第2                        0  0 1 0

結合前兩行,第3行在這些位置上在豎直方向上已經不能再放了( 約束條件 ) 1 0 1 0。按照這個思想,我們再算出主對角線上的約束條件,副對角線上的約束條件,通過總約束條件的約束,剩下的點就是可以放皇后的合法位置, 我們依次dfs這些合法位置就行。

所以現在問題就在於如何計算這些約束條件,這裡有篇很好的文章(再次感謝博主無私分享,上面是我看完自己的理解,可以移步去這個連結了)下面記錄轉載過來的位運算筆記用自己的話理解

   在狀態壓縮中,通常用 ( 1 << N ) - 1 來表示最大狀態MAXST(全部都是1 ,11111111),用 A | B 來合併A和B的狀態位,用  A & ~B 來清除A狀態中所有的B中的狀態位。

一、DFS函式引數Col,MainDiag,ContDiag的表示意義:

      上面講的,前 i行上的總列約束條件、主對角線約束、副對角線約束

二、Col == ( 1 << N ) - 1 的表示意義:

      前i行把所有列上的總約束條件佔滿了 ,那麼就是全擺上1了啊,也就是說找到了一種方案

三、emptycol = ( ( 1 << N ) - 1 ) & ~( Col | MainDiag | ContDiag ) 的表示意義:

      111111 & (~ 00010) 則得到 1111101 ,  相當於扣掉了總約束條件,剩下的合法序列

四、curcol = ( emptycol ) & ( ( ~emptycol ) + 1 ) 的表示意義:

     也就是 lowbit = x & (-x) ,補碼等於反碼+1 , 比如說x等於 110 則 -x : 010 , 因此lowbit = 010,得到最右邊的合法位置。   也就是用於遍歷合法序列

五、 emptycol &= ~curcol 的表示意義:

    類似於三,當前選中了curcol, 那麼合法序列就扣掉當前位置

六、DFS遞迴的函式引數 Col | curcol,( MainDiag | curcol ) >> 1,( ContDiag | curcol ) << 1 的表示意義:

  第一個引數在上面已經講過了,現在講第二個引數, 我們還是分析第1行對第2行的影響。

例如: 1 ->   0 1 0 0 0

第二行怎麼找到自己主對角線上的 約束呢 ,其實只要把 第一行右移一位,然後照著第一個引數的方法累加,得到的就是對第二行的約束。 因為  (x,y) 的主對角線約束是 (x-1, y-1)。

第三個引數也就類似

以上都是本焫雞通過自己能理解的方式理解的,要是有什麼不對的地方請指出

程式碼:

// 位運算
#include<stdio.h>
#include <iostream>
using namespace std;
int n,high,ans;

// col 是前i-1行擺放的皇后 聯合起來對 第i行的約束
// fir 前i-1行 擺放的皇后,在主對角線方向上,對i行的約束
// sec 是前i-1行擺放的皇后,在負對角線上對第i行的約束

void dfs(int col,int fir,int sec)
{
    if(col == high)
    {
        ans++;
        return;
    }
    int cur = high & (~(col|fir|sec));  //11111的情況 剔除掉 三座大山聯合約束的情況 ,得到的序列, 存在某位為1 表示該位可以放

    while(cur)
    {
        int lowbit = cur &(-cur); //得到最後一個位置
        dfs(col|lowbit, (fir|lowbit)>>1 , (sec|lowbit)<<1);
        cur  = cur & (~lowbit); // 最後一個1 標記為1 。  配合while迴圈。相當於把所以可能性遍歷了一遍
    }

}
int main()
{
    while(~scanf("%d",&n)&&n)
    {
        ans=0;
        high = (1<<n)-1;
        dfs(0,0,0);
        printf("%d\n",ans);
    }
    return 0;
}

求到當前所用的列是第幾位,然後帶回去計算值,記憶話搜尋剪枝

程式碼:

#include <iostream>
#include <bits/stdc++.h>

using namespace std;
const int maxn = 100;

int mp[maxn][maxn], dp[maxn][maxn];
int n,ans, high;

int dfs(int col,int tmp, int i)
{
    if(dp[col][i] != -1) return dp[col][i];
    if(col == high)
    {
       return dp[col][i] = tmp;
    }
    int last = high& (~col);
    int ans2 = 0;
    while(last)
    {
        int lowbit = last & (-last); // 得到最低位的1 的位置
        int cnt = log2(lowbit)+1;
        
       // cout<<cnt<<endl;
        ans2 = max(tmp,dfs(col|lowbit, tmp + mp[i][n-cnt+1], i+1));
        last = last & (~lowbit);
    }
    return dp[col][i]  = ans2;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        memset(dp,-1,sizeof(dp));
        ans = 0;
        scanf("%d",&n);
        high = (1<<n)-1;
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=n; j++)
            {
                scanf("%d",&mp[i][j]);
            }
        }
        printf("%d\n",dfs(0,0,1));
    }

    return 0;
}

lightoj 1011 - Marriage Ceremonies

題意:

有N個怪獸,給出H[ ]陣列,代表每個怪獸的HP值,給出 mp[ n ][ p ],表示第n個怪獸死了之後,獲得它的裝備可以對 第p 個怪獸造成的傷害( 每攻擊一下 ), 另外,我們還有一把小手槍 每攻擊一次 造成 1 傷害,問的是至少攻擊幾次能把所有怪獸殺死?

思路:

如果用狀態壓縮儲存怪獸存亡狀態,那麼陣列只需要開 1<<16,我們考慮的是殺小怪獸的順序,和使用哪把武器( 這裡無限使用 ),因此我們暴力殺怪獸的順序,n ! 級別, 每殺一隻怪獸都使用效果最好的武器( 貪心 ),因此搜尋的結構就是(搜尋第一步一定要確立好結構,之後再填上細節,剪枝)

int dfs(int state)
{

    for(怪獸:) //選出還活著的
    {
        
        for( 武器: ) // 貪心找出效果最好的
        {
            cos = min ;
        }
    
        DP[state] = max(DP[state], dfs(殺完小怪獸的state) + cos);
    }
    
}

main{
    dfs(0);
}

我們可以用1 表示怪獸已經死了,這裡使用的不是之前的位運算遍歷, 因為狀態的0 我們也需要考慮, 那麼如果僅僅接受狀態,而不接收n , 例如 state = 1 , 那麼我們無法知道還活著的還有幾隻,也就是1前面有幾個

程式碼:

#include <iostream>
#include <bits/stdc++.h>

using namespace std;
const int maxn = 1000;
const int inf = 0x3f3f3f3f;

int mp[maxn][maxn], dp[maxn], h[maxn];
char st[maxn];
int n,ans, high;

 

int dfs(int state)
{
    if(dp[state] != inf)
        return dp[state];
    if(state == high)
        return dp[state] = 0;

    for(int i=1; i<=n; i++) // 選擇殺哪個怪物
    {
        if( !(state& (1<< (i-1) ) ) ) // 找出活的
        {
           // cout<<"I am alive"<<endl;
            int cnt = inf;
            for(int j=1; j<=n; j++) // 用哪個槍
            {
                if(state & (1<< (j-1) ) && mp[j][i] != 0) // 死了的為1 , 那麼& 真, 就是能用這把槍
                {
                    //  cout<<"you are my gun"<<endl;
                    int cos = h[i] / mp[j][i] + ((h[i] % mp[j][i])? 1:0) ;  
                    cnt = min(cnt, cos);
                }
            }
            if(cnt == inf)
            {
                cnt = h[i];
            }
            dp[state] = min(dp[state], dfs(state | ( (1<<i-1) )) + cnt);

        }
    }
    return dp[state] ;
}

int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        // memset(dp,-1,sizeof(dp));
        ans = 0;
        scanf("%d",&n);
        for(int i=1; i<=n; i++)
            scanf("%d",&h[i]);
        high = (1<<n) -1;
        for(int i=0; i<= high ; i++)
            dp[i] = inf;

        for(int i=1; i<=n; i++)
        {
            scanf("%s",st);
            for(int j=0; j<n; j++)
            {
                mp[i][j+1] = st[j]-'0';
            }
        }
        printf("%d\n",dfs(0));  
    }

    return 0;
}

Poj - 3254

題目大意:農夫有一塊地,被劃分為m行n列大小相等的格子,其中一些格子是可以放牧的(用1標記),農夫可以在這些格子裡放牛,其他格子則不能放牛(用0標記),並且要求不可以使相鄰格子都有牛。現在輸入資料給出這塊地的大小及可否放牧的情況,求該農夫有多少种放牧方案可以選擇(注意:任何格子都不放也是一種選擇,不要忘記考慮!

Sample Input

2 3
1 1 1
0 1 0

Sample Output

9

分析

本題中,假設第i行允許 j 种放牛方式(即j種狀態) ,那麼對於每一種狀態 k (k (= [1, j]),把上一行 i-1 上滿足和 k 不衝突的(題目要求上下不能同時為1 )狀態累加,相當於每個k對應一系列 i-1 行滿足的狀態 , 遍歷 k 累加就行,直接上程式碼讀起來容易些。

該型別題目由於狀態很小,所以每次暴力查詢狀態。

程式碼:

#include <cstdio>
#include <cstring>
using namespace std;

#define mod 100000000
const int maxn = 10050;

int M,N,top = 0;
//top表示每行最多的狀態數

int state[600],num[110];
//state存放每行所有的可行狀態(即沒有相鄰的狀態
//

int dp[20][600];
//dp[i][j]:對於前i行資料,每行有前j種可能狀態時的解
int cur[20];
//cur[i]表示的是第i行整行的情況

inline bool ok(int x) 	//判斷狀態x是否可行
{
    if(x&x<<1)
        return false;//若存在相鄰兩個格子都為1,則該狀態不可行
    return true;
}
void init() 			//遍歷所有可能的狀態
{
    top = 0;
    int total = 1 << N; //遍歷狀態的上界
    for(int i = 0; i < total; ++i)
    {
        if(ok(i))
            state[++top] = i;
    }
}
inline bool fit(int x,int k)  //判斷狀態x 與第k行的實際狀態的逆是否有‘重合’
{
    if(x&cur[k])
        return false; //若有重合,(即x不符合要求)
    return true;  //若沒有,則可行
}

int main()
{
    while(scanf("%d%d",&M,&N)!= EOF)
    {
        init(); // 1 - 1<<N 的所以可行狀態儲存在state[1-top]中
        memset(dp,0,sizeof(dp));
        for(int i = 1; i <= M; ++i)
        {
            cur[i] = 0; // cur[i] 代表第i行草地的情況 1001 還是 0100等等
            int num;
            for(int j = 1; j <= N; ++j)   //輸入時就要按位來儲存,cur[i]表示的是第i行整行的情況,每次改變該數字的二進位制表示的一位
            {
                scanf("%d",&num);  //表示第i行第j列的情況(0或1)
                if(num == 0) //若該格為0
                    cur[i] +=(1<<(N-j)); //則將該位置為1(注意要以相反方式儲存,即1表示不可放牧
                    // 假設輸入 1010 那麼cur[i]為 1010 , 第一個讀入的 左移 4-1 次
            }
        }
        for(int i = 1; i <= top; i++)
        {
            if(fit(state[i],1))   //判斷所有可能狀態與第一行的實際狀態的逆是否有重合
            {
                dp[1][i] = 1;  //若第1行的狀態與第i種可行狀態吻合,則dp[1][i]記為1
            }
            // 例如第一行本應該是 111  那麼轉換成了 000 , 則狀態101 跟第一行fit為1 ,則dp[1][state - 101] = 1

        }

        /*
        狀態轉移過程中,dp[i][k] =Sigma dp[i-1][j] (j為符合條件的所有狀態)
        dp[i][k] = Sigma dp[i-1][j] (j為滿足條件的所有狀態)
        */
        for(int i = 2; i <= M; ++i)   //i索引第2行到第M行
        {
            for(int k = 1; k <= top; ++k)  //該迴圈針對所有可能的狀態,找出一組與第i行相符的state[k]
            {
                if(!fit(state[k],i))
                    continue; //判斷是否符合第i行實際情況
                for(int j = 1; j <= top ; ++j) //找到state[k]後,再找一組與第i-1行符合,且與第i行(state[])不衝突的狀態state[j]
                {
                    if(!fit(state[j],i-1))
                        continue;  //判斷是否符合第i-1行實際情況
                    if(state[k]&state[j])
                        continue;  //判斷是否與第i行衝突
                    dp[i][k] = (dp[i][k] +dp[i-1][j])%mod;  //若以上皆可通過,則將'j'累加到‘k'上
                    // dp[i][k] = dp[i][k] + d[i-1][j]  第i行的狀態 加上i-1行可用的狀態。
                    // 比如說 i行有兩種狀態 010 000 那麼i就要考慮這兩種情況, 當選擇第一個010 時, i-1 有四種情況,也就是j有四種
                }
            }
        }
        int ans = 0;
        for(int i = 1; i <= top; ++i)  //累加最後一行所有可能狀態的值,即得最終結果!!!泥馬寫註釋累死我了終於寫完了!
        {
            ans = (ans + dp[M][i])%mod;
        }
        printf("%d\n",ans);
    }
}

棋盤放子

有一個N*M(N<=5,M<=1000)的棋盤,現在有1*2及2*1的小木塊無數個,要蓋滿整個棋盤,有多少種方式?答案只需要mod1,000,000,007即可。

例如:對於一個2*2的棋盤,有兩種方法,一種是使用2個1*2的,一種是使用2個2*1的。

由於行數n很小,那麼我們每一列的狀態最多隻有 1<<5 種,做法就是針對每一列,搜尋出所有合法狀態。

當前列僅僅影響下一列,比如說第一列放了1*2 ,那麼第二列該位置就不能再放。

狀態轉移方程  dp[ j+1 ][ nex ] += dp[ j ][ state ] , nex是由當前state限制下能構成的合法狀態

程式碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int INF = 0x3f3f3f3f;
const int maxn = 100;
const int mod = 1e9 + 7;
int T, n, m;
ll dp[11][(1 << 11) + 5];

void dfs(int i,int j,int state, int nex)
// 第幾行,第幾列,當前狀態,對下一行的影響
{
    if(i == n) // 到達第n+1 行的時候,當前列已經得到一種狀態 state,且產生對下一行允許的 nex
    {
        dp[j+1][nex] += dp[j][state];
        return;
    }
    if( (1<<(i) &state) > 0)  // 當前行 被佔用了,那麼就跳到下一行
        dfs(i+1,j,state,nex);
    else
    {
        dfs(i+1,j,state, nex|(1<<i));//在當前行擺上 1*2,則下一列被佔用一個

        if(i+1<n && (((1<<(i+1)) & state) == 0) ) // 在當前列擺上2*1
        {
            dfs(i+2,j,state,nex);
        }
    }
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        if(m == 0 && n == 0) break;
        memset(dp,0,sizeof(dp));
        dp[1][0] = 1;
        for(int j=1; j<=m; j++)
        {
            // 第j列
            for(int k=0; k<(1<<n); k++)
                // 各個狀態
            {
                if(dp[j][k])//dp[j][k]!=0 表示第j行 k狀態下,有方案可以放置木板,
                    dfs(0,j,k,0);
            }
        }

        printf("%lld\n",dp[m+1][0]);
    }// 要求鋪滿,那麼m+1 不能有露出來的}
    return 0;
}