初識狀態壓縮dp poj3254 Corn Fields

題目:

給出一個n行m列的草地,1表示肥沃,0表示貧瘠,現在要把一些牛放在肥沃的草地上,但是要求所有牛不能相鄰,問你有多少种放法。

這是第一道狀態壓縮題目,在這劃時代的時候,讓我情不自禁的喊出一句:hello,world!

本來是一直懵比的,直到看到了這篇博客:swallowblank的博客

寫的很nice的。這裏搬運過來,再說一遍不是我寫的,是上面哪位博主寫的,侵刪。

內容:

————————————————————————————分界線————————————————————

一開始根本不會狀壓dp,上網各種找題解,但發現他們寫的都很......反正我作爲一個沒有接觸過狀態壓縮的,根本看不懂!

 

然後看了好多狀態壓縮的題的題解,總結了一下思路,思路很重要,有了思路轉換成計算機語言就好了。因此我先講一下思路:

  先說說地圖,地圖上每一行的01代表一個狀態,比如輸入樣例中的111、010,表示第一行的三個位置都可以種稻子,第二行中間的位置可以種稻子,然後,不能種稻子的地方一定不能種稻子(廢話...)

可以種稻子的地方可以選擇種也可以選擇不種,然後有一個前提條件,就是上下左右相鄰的地方不能種稻子。

  再說說怎麼狀態壓縮,狀態壓縮就是把每一個狀態壓縮成二進制,二進制就是由01組成的,0代表不種,1代表種。二進制就要牽扯到位運算,位運算我就不想說了,百度吧。因此,一串01的二進制數就

可以代表一個狀態,例如輸入樣例第一行是111,那麼可以放入第一行的狀態有,100、010、001、101、000,因爲相鄰位置不能放所以只有5種方法,那麼第二行就只有2種方法000、010(不考慮其他行)

  那麼看第一行和第二行(第一行——第二行),100——000,010——000,001——000,101——000,000——000,這是5種對應方法,還可以100——010,001——010,101——010,000——010這是另外的4種對應方法(第一行5種狀態對吧?第二行2種狀態,按照乘法原理,應該有5*2 = 10種方法,但是111——010是不合法的,因此樣例的答案是10-1 = 9)。

dp[i][j]意思是推到第i行狀態爲j的方案總數。

那麼“100——000”即爲dp[2][000]可以由dp[1][100]得到,那麼dp[2][000] = dp[2][000] + dp[1][100];

那麼“010——000”即爲dp[2][000]可以由dp[1][010]得到,那麼dp[2][000] = dp[2][000] + dp[1][010];

......

以此類推,逐行遞推。

  總結一下思路:先枚舉第一行,把所有可能的狀態和第一行的地圖對比,如果成功,則在循環裏繼續枚舉第二行,把所有可能的狀態和第二行的地圖對比,如果成功,再和第一行填入的狀態對比,如果又匹配成功,則dp[2][000] = dp[2][000] + dp[1][100];方法數加到第二行。這就是一次循環結束了,從新枚舉第二行...

把思路轉換成代碼

can[]代表可行的狀態,稍後解釋。cur[i]代表地圖的第i行
 1 for(int i=1;i<m;i++)//枚舉每一行
 2 {
 3       for(int j=0;j<tot;j++)//對第i行枚舉所有可行的狀態j
 4       {
 5              if((can[j]&cur[i])==0)//如果狀態j和第i行匹配了
 6              {
 7                   for(int k=0;k<tot;k++)//枚舉第i+1行的所有可行的狀態k
 8                   {
 9                         if(((can[k]&cur[i+1])==0)&&((can[k]&can[j])==0))//狀態k和第i+1行匹配且和狀態j匹配
10                             dp[i+1][can[k]] = dp[i+1][can[k]]+dp[i][can[j]];//狀態數相加
11                   }
12              }
13       }
14 }

這樣核心代碼就實現了。

有一個小方法,就是枚舉可行狀態的時候,假如一行是8列,不必從00000000枚舉到11111111,這樣很麻煩,所以要預處理。

就是在一開始把,一行的可行狀態先求出來就拿“11111111”來說,這肯定是不可能的,因爲有相鄰的1,所以在一開始就可以捨棄掉。怎麼做呢?

假如一行是8列,先從00000000枚舉到11111111,對於每一個狀態把它左移1位,再和他自己&運算,假如結果>0,就說明有有相鄰的1,舉個簡單的例子:

  01011要判斷有沒有相鄰的1,if(((01011<<1) & (01011)) > 0 )則有相鄰的1,(01011<<1) & (01011) 就是 010110和01011按位且運算,這兩個紅色地方1&1 == 1,因此結果大於0。

怎麼實現呢?

1 tot = 0;//全局變量,相當於棧的top,代表可行的狀態數
2 for(int i=0;i<(1<<n);i++)//n是列數,i是枚舉的狀態
3      if((i&(i<<1))==0) can[tot++] = i;

  dp[][]肯定要初始化對吧?不然全是0了,只要對第一行初始化就行了,因爲後面的的行都是由第一行得來的

1 for(int i=0;i<tot;i++)
2      if((cur[1]&can[i])==0) dp[1][can[i]] = 1;//和cur[1](第一行)匹配,就給對應的dp賦值爲1

  最後一步就是得到cur[]

1 for(int i=1;i<=m;i++)
2 {
3        for(int j=0;j<n;j++)
4        {
5              int num;
6              scanf("%d",&num);
7              if(num==0) cur[i] = (cur[i]|(1<<j));//這裏要給0的地方變爲1,1的地方放上0,因爲要保證不合法的匹配一定是獨一無二的。自己思考一下吧
8        }
9 }

最後貼一下完整代碼,一開始學的時候,感覺主流代碼都一模一樣,而且一大堆亂七八糟的函數,麻煩又看不懂,於是下定決心如果自己搞明白了,一定要寫一個大家都看得懂的題解,感覺自己講的比其他都清楚了,如果看不懂就真沒辦法了......

這裏略去
——————————————分界線—————————————下面是我自己的了———————————
然後我們再感受一下,什麼叫狀態壓縮。

自己的一點理解,請青打臉,不不不,輕打臉。

我們首先用樸素的方法想一想這個題。

1 1 1

0 1 0

我們直接枚舉試試,先不考慮複雜度,(0,0)座標可以放也不以不放,如果(0,0)放,那麼我們在(0,1)也是兩種情況放還是不放,同理(0,2)也是這樣。然後我們假設第一行狀態是1 0 1,那麼我們第二行的狀態(1,0),(1,2)沒有選擇,(1,1)可以選擇放還是不放。。很容易理解。。

等等,我們回過頭想想,我們是怎樣定義狀態的,我們是以每個點的座標作爲一個狀態,但是我們可不可以這樣想,把上面一行整個的狀態看成一個狀態,掐指一算,大概是是8種狀態(每個選或者不選),但去除不能有相鄰1的情況後只有5種,怎麼把它表示出來,這就是上面的博主說的二進制壓縮了,然後我們所講就是一行的狀態與另一行狀態的故事了。

我們再回顧一下,解那個題,我們做了哪些事情。

(1)枚舉所有備選的,有可能可行的狀態,也就是剔除掉一定不行的,暨有相鄰1的狀態。

(2)找出每一行的地圖的狀態,暨 0 0 0 ,我們按照上面博主所說,將0 變成1,1變成0,無非就是在狀態匹配的時候爲了方便統一的寫出來,我們假如不這麼做 比如原地圖 1 1 1,我們有一個備選方案0 0 1 ,下一行有一個備選方案0 1 0 ,以我們肉眼觀察是可行的,但是計算機不能,我們怎樣讓二者達到統一。但我們反轉一下地圖 

0 0 0

1 0 1

,010&101==0and 001&000==0,001&010==0這不就統一化了嘛,什麼?太巧了?好吧我說個不行的,上面方案是010,下面方案是010,010&000==0and010&101==0,but 010&010!=0,所以方案不行。不過實事求是說,我應該想不到這點,積累經驗吧。

(3)枚舉。枚舉這一行的備選方案,再與地圖匹配,看滿不滿足條件,合不合適,滿足的話,再選一個狀態作爲下一行的備選狀態,同樣看和地圖合不合適,如果合適的話,再看看與上一行合不合適,如果合適ok,dp[i+1][j]=dp[i+1][j]+dp[i][k]。

(4)初始化,這其實應該放在上面,但直接出來又比較突兀。


最後注意位運算的優先級,我開始照着樓上博主的代碼打都打錯。。。。


代碼是樓上博主的,(略去的地方)我自己加的點註釋:



#include<cstdio>
#include<cstring>
#include<algorithm>
#define mod 100000000


using namespace std;
int dp[13][1<<12],cur[13];//cur[i]第i行的地圖
int can[1<<12],tot,m,n;//can[i]可行的狀態,是爲了消除110,這種
//只是備選項,能不能行還得匹配了才知道。
/*tot是狀態總數*/
int main()
{
    while(~scanf("%d%d",&m,&n))
    {//m行,n列,最多有pow(2,n)個狀態
        tot = 0;
        for(int i=0;i<(1<<n);i++)
            if((i&(i<<1))==0) can[tot++] = i;//相當於棧。
        /*篩掉一定不行的*/
        memset(cur,0,sizeof(cur));
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=m;i++)
        {
            for(int j=0;j<n;j++)
            {
                int num;
                scanf("%d",&num);
                if(num==0) cur[i] = (cur[i]|(1<<j));
            }
        }
        for(int i=0;i<tot;i++)
            if((cur[1]&can[i])==0) dp[1][can[i]] = 1;
        //初始化,第一行的地圖與所有的狀態匹配,能成功就標上1
        for(int i=1;i<m;i++)
        {
            for(int j=0;j<tot;j++)
            {
                if((can[j]&cur[i])==0)//第1行的狀態與第一行的地圖匹配
                {
                    for(int k=0;k<tot;k++)
                    {//下一行的狀態與下一行的地圖匹配   下一行與上一行匹配
                        if(((can[k]&cur[i+1])==0)&&((can[k]&can[j])==0))
                            dp[i+1][can[k]] = dp[i+1][can[k]]+dp[i][can[j]];
                    }
                }
            }
        }
        int ans = 0;
        for(int i=0;i<tot;i++)
        {
            ans += dp[m][can[i]];
            ans = ans % mod;
        }
        printf("%d\n",ans);
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章