##[$AcWing$ $327$. 玉米田 ](https://www.acwing.com/problem/content/329/) ### 一、题目描述 农夫约翰的土地由 $M×N$ 个小方格组成,现在他要在土地里种植玉米。 非常遗憾,部分土地是 **不育** 的,无法种植。 而且,**相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘**。 现在给定土地的大小,**请你求出共有多少种种植方法**。 **土地上什么都不种也算一种方法**。 **输入格式** 第 $1$ 行包含两个整数 $M$ 和 $N$。 第 $2..M+1$ 行:每行包含 $N$ 个整数 $0$ 或 $1$,用来描述整个土地的状况,$1$ 表示该块土地肥沃,$0$ 表示该块土地不育。 **输出格式** 输出总种植方法对 $10^8$ 取模后的值。 **数据范围** $1≤M,N≤12$ **输入样例**: ```cpp {.line-numbers} 2 3 1 1 1 0 1 0 ``` **输出样例**: ```cpp {.line-numbers} 9 ``` ### 二、样例数据解析 原始数据 > 部分土地是不育的,无法种植。 > $1$是可以摆放的,$0$是不可以摆放的。 ```c++ 1 1 1 0 1 0 ```
> 相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。 可能的摆放办法 ![](https://img2020.cnblogs.com/blog/8562/202201/8562-20220101125401909-656540172.png) ### 三、棋盘类状态压缩$DP$套路 - ① **是否存在连续$1$** ```cpp {.line-numbers} //是不是存在连续1 bool check(int x) { return !(x & x >> 1);//存在连续1,就是不合法,返回false,否则返回true } ``` - ② **预处理出所有有效状态【没有连续数字1的状态就是有效状态】** ```cpp {.line-numbers} for (int i = 0; i < 1 << m; i++) // 0~2^m-1,相当于遍历二进制的每一种可能性 if (check(i)) st.push_back(i); //棋盘类,用check检查状态的合法性,记录合法状态,不允许有连续的数字1 ``` - ③ **使用一维数组压缩存储不允许种田的位置** ```cpp {.line-numbers} for (int i = 1; i <= n; i++) // n行 for (int j = 0; j < m; j++) {//m列 int x; scanf("%d", &x); if (x == 0) g[i] |= 1 << j; } ``` >$Q$:为什么$g[i]$要记录 **无法摆放的位置**,而不记录 **可以摆放的位置** 呢? **答**: 因为这样设计可以 **快速判断是不是上下行兼容**: > 思考路径: > - ① 不考虑原始地图中不育的情况下,单纯讨论是不是本行无连续$1$,上下行是不是存在同列$1$,这两个用`check(x)`,`a|b==0`就可以了~ > - ② 只有上面的考虑怕是还不行,还需要考虑 **原始地图** 与 当前枚举到的状态 **是否兼容**,也就是你想往某个位置上种玉米,结果那个位置上不育,需要$continue$ > - ③ 怎么判断呢?用 **位运算** 呗 > $g[i]$表示原始地图状态,$g[i]$有两种设计思路: > - $1$可以放,$0$不可以放 > - $0$可以放,$1$不可以放 > >这两种设计用哪个呢?当然是哪个方便用哪个! >- **如果选择($1$)方式** > 假设有一个场景:$2$个位置是肥沃的,可以放的,现要让我们检查一个状态是不是合法,此状态要求往$3$个位置放玉米,此时,无论我们用`&`,还是用`|`,都是无法准确检测此状态的非法性的! >- **如果选择($2$)方式** > 只要`g[i] & a > 0`, 现实含义:**想放不让放,表示冲突** 。 > >**总结**:正难则反,变换思路 - ④ **预处理合法状态之间的兼容关系** ```cpp {.line-numbers} for (int a : st) for (int b : st) //此步骤只处理两行之间没有竖着的冲突就算合理转化,不考虑无法耕种情况 if ((a & b) == 0) head[a].push_back(b); ``` - **算法步骤**: 一层:枚举每一行 二层:枚举每一种合法状态,如果存在不能摆放的状态,需要做一下与运算$continue$掉 三层:枚举每种状态的兼容前序状态 尝试进行状态上的转化,合理设计状态转移方程 - **枚举最后一行的所有可能状态,累加出答案** ### 四、实现代码 ```cpp {.line-numbers} #include using namespace std; const int MOD = 1e8; // 按1e8取模 const int N = 14; // M*N个小方格,上限都是12,这里我们故意取大一点,到14. const int M = 1 << 12; // 0~2^12-1,共2^12个状态,记录每一行的摆放状态 int n, m; // n行,m列 int g[N]; // 记录哪个位置是无法种田的,在题目中,输入的位置是0表示无法种田 vector st; // 合法状态 vector head[M]; // 一个状态可以转化为哪些状态(与哪此状态兼容) int f[N][M]; // 一维:完成了第i行,二维:在状态是j(二进制状态)的情况下,值:方案数量 // 状态检查是否合法,某个状态是不是存在连续1 bool check(int x) { return !(x & x >> 1); } int main() { // 1、输入地图 scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) // n行 for (int j = 1; j <= m; j++) { // m列 int x; scanf("%d", &x); if (x == 0) // 当前位置是不育的玉米地 g[i] |= 1 << (j - 1); // 在对应的二进制位上标识为1,这里g[i]存放的是十进制,所以将增加的二进制转为十进制加入g[i] } // 2、预处理出所有合法状态 for (int i = 0; i < 1 << m; i++) if (check(i)) st.push_back(i); // 3、预处理出每个合法状态兼容哪些状态 for (int a : st) for (int b : st) if ((a & b) == 0) head[a].push_back(b); // 4、开始DP f[0][0] = 1; // 已经放完第0行,状态是一个也没有放,算一种方案 for (int i = 1; i <= n; i++) // 枚举每一行 for (int a : st) { // 枚举每一个合法状态 // 如果当前枚举出的a说某个位置需要种植玉米,而g[i]记录的此位置不育,那么就不能在此位置耕种 if ((g[i] & a)) continue; // 到这里时,表明此状态完全合法,找它的所有兼容状态,所有兼容状态都可以顺利转移到a状态 for (int b : head[a]) f[i][a] = (f[i][a] % MOD + f[i - 1][b] % MOD) % MOD; } // 结果 int res = 0; for (int a : st) res = (res % MOD + f[n][a] % MOD) % MOD; printf("%d", res); return 0; } ```