初识状态压缩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);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值