狀態壓縮DP入門


什么是狀壓DP:
動態規划的狀態有時候比較惡心,不容易表示出來,需要用一些編碼技術,把狀態壓縮的用簡單的方式表示出來。
典型方式:當需要表示一個集合有哪些元素時,往往利用2進制用一個整數表示。
動態規划本來就很抽象,狀態的設定和狀態的轉移都不好把握,而狀態壓縮的動態規划解決的就是那種狀態很多,不容易用一般的方法表示的動態規划問題,這個就更加的難於把握了。難點在於以下幾個方面:狀態怎么壓縮?壓縮后怎么表示?怎么轉移?是否具有最優子結構?是否滿足后效性?涉及到一些位運算的操作,雖然比較抽象,但本質還是動態規划。找准動態規划幾個方面的問題,深刻理解動態規划的原理,開動腦筋思考問題。這才是掌握動態規划的關鍵。
分析

運算名 符號 效果
& 按位與 如果兩個相應的二進制位都為1,則該位的結果值為1,否則為0
l 按位或 兩個相應的二進制位中只要有一個為1,該位的結果值為1
^ 按位異或 若參加運算的兩個二進制位值相同則為0,否則為1
~ 取反 ~是一元運算符,用來對一個二進制數按位取\反,即將0變1,將1變0
<< 左移 用來將一個數的各二進制位全部左移N位,右補0
*>> 右移 將一個數的各二進制位右移N位,移到右端 的低位被舍棄,對於無符號數,高位補0

下面來看三道題目:
例題一:
[POJ3254]Corn Fields(其實就是牛吃草)
題目大意
一個矩陣里有很多格子,每個格子有兩種狀態,可以放牧和不可以放牧,可以放牧用1表示,否則用0表示,在這塊牧場放牛,要求兩個相鄰的方格不能同時放牛(不包括斜着的),即牛與牛不能相鄰。問有多少種放牛方案(一頭牛都不放也是一種方案)
輸入
1<=n<=12,1<=m<=12
輸出
一個mod100000000的整數
樣例輸入
2 3
1 1 1
0 1 0
樣例輸出
9
分析
從題意我們可以知道牛與牛之間不能相鄰,我們可以很容易的想到可以一行一行的遞推,因為每只牛能不能放只與上一行和當前這一行有關。
所以dp中有一個維度是用來表示第幾行的,還有一個維度就是用來表示哪一行的狀態的。
假設有m列,則狀態最多只有(1《m-1)種,這是顯然的。因為他每一列只有放和不放這兩種決策。
那么怎么表示狀態是個關鍵問題,這就要用到狀態壓縮了,用二進制來表示某個狀態,比如
0101代表的就是1、3列不放牛,2、4列放牛。這樣不僅省空間省代碼還省時間。
要用位運算是必須的。
參考代碼

#include <cstdio> 
#include <cstring> 
const int N = 13;  
const int M = 1<<N;  
const int mod = 100000000;  
int st[M],map[M];  ///分別存每一行的狀態和給出地的狀態 
int dp[N][M];  //表示在第i行狀態為j時候可以放牛的種數 
bool judge1(int x)  //判斷二進制有沒有相鄰的1 
{  
    return (x&(x<<1));  
}  
bool judge2(int i,int x)  
{  
    return (map[i]&st[x]);  
}  
int main()  
{  
    int n,m,x;  
    while(~scanf("%d%d",&n,&m))  
    {  
        memset(st,0,sizeof(st));  
        memset(map,0,sizeof(map));  
        memset(dp,0,sizeof(dp));  
        for(int i=1;i<=n;i++)  
        {  
            for(int j=1;j<=m;j++){  
                scanf("%d",&x);  
                if(x==0)  
                    map[i]+=(1<<(j-1));  
            }  

        }  
        int k=0;  
        for(int i=0;i<(1<<m);i++){  
            if(!judge1(i))  
                st[k++]=i;  
        }  
        for(int i=0;i<k;i++)  
        {  
            if(!judge2(1,i))  
                dp[1][i]=1;  
        }  
        for(int i=2;i<=n;i++)  
        {  
            for(int j=0;j<k;j++)  
            {  
                if(judge2(i,j))  //判斷第i行 假如按狀態j放牛的話行不行。 
                    continue;  
                for(int f=0;f<k;f++)  
                {  
                    if(judge2(i-1,f))   //剪枝 判斷上一行與其狀態是否滿足 
                        continue;  
                    if(!(st[j]&st[f]))  
                        dp[i][j]+=dp[i-1][f];  
                }  
            }  
        }  
        int ans=0;  
        for(int i=0;i<k;i++){  
            ans+=dp[n][i];  
            ans%=mod;  
        }  
        printf("%d\n",ans);  
    }  
    return 0;  
}  

例題二:
[POJ3311]Hie With The Pie
題目大意:
一個送外賣的人,從0點出發,要經過所有的地點然后再回到店里(就是0點),求最少花費的代價。
輸入
1<=n<=10
輸出
一個整數,代表最小花費。
樣例輸入
3
0 1 10 10
1 0 1 2
10 1 0 10
10 2 10 0
0
樣例輸出
8
分析
怎么做?我們可以先從暴力來分析分析。

搜索解法:這種解法其實就是計算排列子集樹的過程。從0點出發,要求遍歷123點后回到0點。以不同的順序來依次遍歷123點就會導出不同的路徑(0->1->2->3->00->1->3->2->0等等),總共有3!=6條路徑需要考慮,從中選出最短的那條就是所求。搜索解法的時間復雜度為 O(n!) 。

需要注意的是題目顯然給的是個鄰接矩陣,並不代表各點之間的距離,所以我們需要先Floyd求出各點的最短路

動歸解法:仔細觀察搜索解法的過程,其實是有很多重復計算的。比如從0點出發,經過12345點后回到0點。那么0->1->2->(345三個點的排列)->00->2->1->(345三個點的排列)->0就存在重復計算(345三點的排列)->0路徑集上的最短路徑。只要我們能夠將這些狀態保存下來就能夠降低一部分復雜度。下面就讓我們用動歸來求解這一問題。記dp(S,v)為走完了集合S后最后停留在v點的最小花費。

我們不難得出遞推方程式為

dp[S][v] = min(dp[S除去點v)][k] + dis[k][v],dp[S][v])

好吧o(╯□╰)o,和floyd確實有那么二兩相似。
參考代碼

#include<iostream> 
#define INF 100000000 
using namespace std;  
int dis[12][12];  
int dp[1<<11][12];  
int n,ans,_min;  
int main()  
{  
    //freopen("in.txt","r",stdin); 
    while(scanf("%d",&n) && n)  
    {  
        for(int i = 0;i <= n;++i)  
            for(int j = 0;j <= n;++j)  
                scanf("%d",&dis[i][j]);  
        for(int k = 0;k <= n;++k)  
            for(int i = 0;i <= n;++i)  
                for(int j = 0;j <= n;++j)  
                    if(dis[i][k] + dis[k][j] < dis[i][j])  
                        dis[i][j] = dis[i][k] + dis[k][j];  

        for(int S = 0;S <= (1<<n)-1;++S)//枚舉所有狀態,用位運算表示 
            for(int i = 1;i <= n;++i)  
            {  
                if(S & (1<<(i-1)))//狀態S中已經過城市i 
                {  
                    if(S == (1<<(i-1)))   dp[S][i] = dis[0][i];//狀態S只經過城市I,最優解自然是從0出發到i的dis,這也是DP的邊界 
                    else//如果S有經過多個城市 
                    {  
                        dp[S][i] = INF;  
                        for(int j = 1;j <= n;++j)  
                        {  
                            if(S & (1<<(j-1)) && j != i)//枚舉不是城市I的其他城市 
                                dp[S][i] = min(dp[S^(1<<(i-1))][j] + dis[j][i],dp[S][i]);  
                            //在沒經過城市I的狀態中,尋找合適的中間點J使得距離更短,和FLOYD一樣 
                        }  
                    }  
                }  
            }  
        ans = dp[(1<<n)-1][1] + dis[1][0];  
        for(int i = 2;i <= n;++i)  
            if(dp[(1<<n)-1][i] + dis[i][0] < ans)  
                ans = dp[(1<<n)-1][i] + dis[i][0];  
        printf("%d/n",ans);  
    }  
    return 0;  
}  

例題三
[POJ1185]炮兵陣地
描述
司令部的將軍們打算在N*M的網格地圖上部署他們的炮兵部隊。一個N*M的地圖由N行M列組成,地圖的每一格可能是山地(用”H” 表示),也可能是平原(用”P”表示),如下圖。在每一格平原地形上最多可以布置一支炮兵部隊(山地上不能夠部署炮兵部隊);一支炮兵部隊在地圖上的攻擊范圍如圖中黑色區域所示:
這里寫圖片描述
如果在地圖中的灰色所標識的平原上部署一支炮兵部隊,則圖中的黑色的網格表示它能夠攻擊到的區域:沿橫向左右各兩格,沿縱向上下各兩格。圖上其它白色網格均攻擊不到。從圖上可見炮兵的攻擊范圍不受地形的影響。
現在,將軍們規划如何部署炮兵部隊,在防止誤傷的前提下(保證任何兩支炮兵部隊之間不能互相攻擊,即任何一支炮兵部隊都不在其他支炮兵部隊的攻擊范圍內),在整個地圖區域內最多能夠擺放多少我軍的炮兵部隊。
輸入
第一行輸出數據測試組數X(0~X~100)
接下來每組測試數據的第一行包含兩個由空格分割開的正整數,分別表示N和M; 接下來的N行,每一行含有連續的M個字符(‘P’或者’H’),中間沒有空格。按順序表示地圖中每一行的數據。0<=N <= 100;0<=M <= 10。
輸出
每組測試數據輸出僅一行,包含一個整數K,表示最多能擺放的炮兵部隊的數量。
樣例輸入
1
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
樣例輸出
6
參考代碼

#include <cstdio> 
#include <iostream> 
#include <cstring> 
#include <algorithm> 
using namespace std;  
const int N = 105;  
int Map[N];  
int dp[N][65][65];  //dp[i][j][k]表示放第i行時,第i行為第j個狀態,第i-1行為第k個狀態最多可以放多少個炮兵 
int s[N], num[N];  
int n, m, p;  

bool check(int x) {  //判斷本行的炮兵是否互相攻擊 
    if(x & (x >> 1)) return false;  
    if(x & (x >> 2)) return false;  
    return true;  
}  

int Count(int x)
{
    int i=1, ans=0; 
    while(i<=x)
    {
        if(x&i) ans++;  
        i<<=1;  
    }  
    return ans;  
}  
void Init()
{  
    p=0;  
    memset(s,0,sizeof(s));  
    memset(num,0,sizeof(num));  
    for(int i=0;i<(1<<m);i++){
        if(check(i))
        {  
            s[p]=i;
            num[p++]=Count(i);  //計算狀態為x時可以放多少個炮兵 
        }  
    }
}  

int main() {  
    char ch;
    scanf("%d%d", &n, &m);
    memset(dp, 0, sizeof(dp));  
    memset(Map, 0, sizeof(Map));  
    for(int i=0;i<n;i++)
    {  
        for(int j=0;j<m;j++)
        {  
            cin>>ch;
            if(ch == 'H')  
                Map[i]+=(1<<(m-1-j));//P為0,H為1 
        } 
    }  
    Init();//預處理出合法狀態 
    for(int i = 0; i < p; i++) //求第一行最多放多少 
        if(!(Map[0]&s[i]))// 不在山上 
            dp[0][i][0]=num[i];  
    for(int i = 0; i < p; i++)//前兩行最多放多少 
    {
        if(!(Map[1]&s[i]))//不與第一行沖突 
        {  
            for(int j=0;j<p;j++)
            {  
                if((!(s[i]&s[j])))//一二行不沖突 
                {  
                    dp[1][i][j]=max(dp[1][i][j],dp[0][j][0]+num[i]);
                }  
            }  
        }  
    }  
    for(int r=2;r<n;r++)//枚舉行數 
    {
        for(int i=0;i<p;i++)//當前行的狀態 
        {   
            if(!(s[i]&Map[r]))//不在山上 
            {  
                for(int j = 0; j < p; j++) //上一行的狀態 
                {  
                    if(!(s[j] & Map[r-1]))//不在山上 
                    {  
                        if(!(s[i] & s[j]))//不與當前行沖突 
                        {  
                            for(int k = 0; k < p; k++)//上上一行的狀態
                            {    
                                if(!(s[k] & Map[r-2])) //不在山上 
                                {  
                                    if(!(s[j] & s[k])) //不與上一沖突 
                                    {  
                                        if(!(s[i] & s[k]))//不與當前行沖突 
                                        {  
                                                dp[r][i][j]=max(dp[r][i][j],dp[r-1][j][k]+num[i]);  
                                        }  
                                    }  
                                }  
                            }  
                        }  
                    }  
                }  
            }  
        }  
    }  
    int ans = 0;  
    for(int i = 0; i < p; i++)
    {  
        for(int j = 0; j < p; j++)
        {  
            if(ans<dp[n-1][i][j])  
            ans=dp[n-1][i][j];  
        }  
    }  
    printf("%d\n", ans);   
    return 0;  
}  


免責聲明!

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



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