玉米田

本文详细解析了一道关于玉米田种植的动态规划问题,讲解了如何利用位运算技巧来优化状态表示和计算过程。通过预处理田地状况、枚举合法状态并判断相邻土地种植条件,实现状态转移方程。文章提供了两种不同的代码实现方式,并对代码逻辑进行了深入的解释。
摘要由CSDN通过智能技术生成

玉米田

题目描述

在这里插入图片描述


题意解释

题目有两个限制条件:

  • 相邻的土地不能同时种植玉米
  • 部分土地是不育的,无法种植

假设有 2 × 3 2\times 3 2×3田地,输入 110 011 。

在这里插入图片描述


核心思路

状态表示:

f [ i ] [ a ] f[i][a] f[i][a]表示已经种植了前i行,第i行中第a个状态时的方案数

状态计算

f [ i ] [ a ] = ∑ f [ i − 1 ] [ b ] f[i][a]=\sum f[i-1][b] f[i][a]=f[i1][b]

总方案数

a n s = ∑ f [ n ] [ a ] ans=\sum f[n][a] ans=f[n][a]

不要把【枚举时用1表示这个地方种植玉米,0表示不种植】和题目说的【1表示该块土地肥沃,0表示该块土地不育】混淆了。

在这里插入图片描述

如何理解下面这段代码呢?

for(int i=1;i<=n;i++)
{
    for(int j=1;j<=m;j++)
    {
        int x;
        cin >>x;
        g[i]=(g[i]<<1)+x;	//保存各行的状态值   最终算出来的g[i]表示第i行的十进制数
    }
}

举个例子,比如输入 2 × 3 2\times 3 2×3矩阵,第一行输入1 1 0,那么经过for循环后, g [ 1 ] = 6 g[1]=6 g[1]=6,也就是说 g [ i ] g[i] g[i]存储的是第i行我们所输入的这块田地状况所组成的十进制数。输入1 1 0,组成的十进制数是6

第二行输入0 1 1,那么经过for循环后, g [ 2 ] = 3 g[2]=3 g[2]=3,输入的0 1 1组成的十进制数就是3。记住这种位运算的骚操作即可。

在这里插入图片描述

如何理解下面这段代码呢?

for(int i=0;i<(1<<m);i++)	//枚举一行中所有的状态,然后挑选出合法的状态
{
    if(!(i&i>>1))//如果不存在相邻的1
        s[cnt++]=i;//保存一行中合法状态的个数
}

其实这段代码就是在求:枚举一行中所有的状态,然后挑选出合法的状态

比如所有状态:{000,001,010,011,100,101,110,111}

合法状态有:{000,001,010,100,101},如下图所示,s[0]保存的是000,s[1]保存的是001,s[2]保存的是010,s[3]保存的是100,s[4]保存的是101

在这里插入图片描述

总结

  • 每行包含若干个类
  • 每个类累加上一行的兼容类
  • 每行的类之间无关

如何判断a是否种植在肥沃土地上还是种植在贫瘠土地上呢?

写法1:

if((s[a]&g[i])==s[a])

写法2:

if(s[a]&~g[i])
    continue;

状态压缩DP的特点:

  • 用二进制表示状态,用十进制数来存储状态
  • 用位运算筛选出合法状态
  • 用位运算判断状态转移的条件
  • 计算时每个类累加上一行的兼容类

在这里插入图片描述

在这里插入图片描述

设计一个数组,g[i]表示第i行不能种植土地的状态,由于我们把玉米田的0和1翻转了,设此时得到的g[1]=1011,表示第一行中的第一个田地、第三个田地、第四个田地不能种植玉米。

设枚举时第i行的合法状态是s,那么当s&g[1]为true,则说明s不能种植到g[1]=1011中。

先预处理出所有的合法状态,然后再把这些合法状态去种植到g[i]中,通过s&g[i],即可知道s这个合法状态是否能够种植到g[i]中了。


代码

写法1

#include<iostream>
using namespace std;
const int N=14,Mod=100000000;
int n,m;//n表示行数  m表示列数
int cnt;    //同一行中所有合法状态的个数
int s[1<<N];    //存储合法状态的集合,比如s={000,001,010,100,101}
int g[N];   //g[i]存储的是第i行的玉米田状况的十进制数(状态值),比如第一行输入111,那么g[1]=7
//f[i][a]表示已经种植了前i行,第i行中第a个状态时的方案数
int f[N][1<<N];
int main()
{
    scanf("%d%d",&n,&m);    //输入行数和列数
    //预处理 田地的状况
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            int x;//输入田地的状况
            scanf("%d",&x);
            //通过下面这个式子,可求出g[i]表示第i行玉米田地的状态值
            g[i]=(g[i]<<1)+x;
        }
    }
    
    //枚举一行中所有的状态,找出合法的状态
    for(int i=0;i<(1<<m);i++)
    {
        if(!(i&i>>1))//如果不存在相邻的1,则合法
        {
            s[cnt++]=i; //保存一行中所有的合法状态
        }
    }
    //边界
    f[0][0]=1;  //题目说了 土地上什么都不种也算一种方法。
    //枚举行,从第1行枚举到第n+1行
    for(int i=1;i<=n+1;i++)
    {
         for(int a=0;a<cnt;a++)//枚举第i行的所有合法状态
         {
             for(int b=0;b<cnt;b++)//枚举第i-1行的所有合法状态
             {
                 //要保证两个条件:(1)上下两行a和b不能同列 (2)a种在肥沃的土地上
                 //(s[a]&g[i])==s[a]//可以判断出是否是种在肥沃的土地上
                 //写法1
                 if(!(s[a]&s[b])&&(s[a]&g[i])==s[a])
                 f[i][a]=(f[i][a]+f[i-1][b])%Mod;
                 
                 //写法2
                //  if((s[a]&~g[i])||(s[a]&s[b]))
                //  continue;
                //f[i][a]=(f[i][a]+f[i-1][b])%Mod;
             }
         }
    }
    //第n+1行没有合法状态,相当于就只是在第1到第n行种植
    printf("%d\n",f[n+1][0]);
    return 0;
}

写法2

#include<iostream>
#include<vector>
using namespace std;
const int N=14,Mod=100000000;
int n,m;
vector<int>state;   //存储合法状态的集合,比如state={000,001,010,100,101}
//存储与合法状态不冲突的方案 比如对于001来说,与它不冲突的合法状态就是000 100
//所以head[001]={000,100}
vector<int>head[1<<N];//
int g[N];   //g[i]表示第i行玉米种植的状况,是个十进制数
//f[i][a]表示已经种植了前i行,第i行中第a个状态时的方案数
int f[N][1<<N];
//检查每行内相邻之间是否种植玉米,即检查行内的这个state状态是否为合法状态
bool check(int state)
{
    if(!(state&state>>1))
    return true;
    else
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<m;j++)
        {
            int x;
            scanf("%d",&x);
            //比如状况是110,那么我们翻转左移后,构成的新数是100,即g[i]=4
            g[i]+=(!x<<j);
        }
    }
    //预处理一行内的所有的合法状态
    for(int i=0;i<(1<<m);i++)
    {
        if(check(i))
        {
            state.push_back(i);
        }
    }
    //判断上下两行的相邻之间是否有种植玉米
    for(int i=0;i<state.size();i++)//下一行  比如状态集合为{000,001,010,100,101}
    {
        for(int j=0;j<state.size();j++)//上一行 比如状态集合为{000,001,010,100,101}
        {
            //假设此时枚举到下一行的state[i]=001,我想看看上一行的所有合法状态中哪个合法状态不与state[i]冲突
            //显然,上一行中000,100这两个状态不会与state[i]产生冲突,000对应下标j=0,100对应下标j=3
            //所以head[001]={000,100},用十进制来存储状态就是:head[1]={0,3}
            if(!(state[i]&state[j]))
            head[i].push_back(j);
        }
    }
    //处理边界
    f[0][0]=1;
    for(int i=1;i<=n+1;i++) //从第1行枚举到第n+1行
    {
        for(int a=0;a<state.size();a++) //枚举下一行即第i行中所有的合法状态  a表示是第几个合法状态
        {
            //如果第i行中的第a个合法状态不能种植再g[i]这一片土地上,则跳过
            if(state[a]&g[i])
            continue;
            //写法1
            for(int b=0;b<head[a].size();b++)//枚举上一行即第i-1行中所有的合法状态 b表示是第几个合法状态
            {
                f[i][a]=(f[i][a]+f[i-1][head[a][b]])%Mod;
            }
            
            // //写法2
            // for(int b:head[a])
            // f[i][a]=(f[i][a]+f[i-1][b])%Mod;
        }
    }
    cout <<f[n+1][0]<<endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值