玉米田
题目描述
题意解释
题目有两个限制条件:
- 相邻的土地不能同时种植玉米
- 部分土地是不育的,无法种植
假设有 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[i−1][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;
}