把每一行压成二进制状态,预处理单行合法状态后按行做状压 DP,统计所有不相邻的种草方案数。
OJ: luogu
题目 ID: P1879
难度:普及+/提高-
标签:状态压缩动态规划计数DP网格DP
创建: 2026-06-21 05:51
更新: 2026-08-09 06:46
后置题目
roj 3153炮兵阵地B 逐行转移时把行内合法性提前枚举掉,用的正是 A 教的同一列掩码位与左移判相邻技巧(s & (s<<1)),只是把左右各 1 格扩成左右各 2 格,并叠加了地形过滤与最近两行的状态设计。roj 3152「Mondriaan's Dream」 蒙德里安的梦想B 的轮廓线 DP 直接复用 A 立的「按行划分阶段 + 把列信息压成一个二进制 mask 作状态」这一步(main.py 里 g(j,s) 用 `s & b` 判位、`cur | b` 记伸出列),只是把 mask 的语义从「本行种草格子」换成「下一行被竖牌占住的列」,再叠加行内逐格 dfs 的竖牌登记与横牌右邻判定这个新流程。luogu P2704[NOI2001] 炮兵阵地B 直接沿用 A 教的两步:先枚举满足 ok_self 的单行合法态,再用 cur&pre1、cur&pre2 的同列不相容检查筛掉冲突行,只是把「只依赖上一行」扩成「依赖最近两行」,并把计数改成 +popcount 取 max。luogu P3888[GDOI2014] 拯救莫莉斯两题都用同一个关键观察(限制只在上下左右,故可把每行压成二进制状态、按行枚举相邻行状态转移),B 直接沿用 A 教的行压状态 + 两行滚动骨架,只是把兼容性判定换成三行覆盖判定的 row_ok,并叠加 (总代价, 油库数) 双关键字取 min;A 的代码里是 if (cur & last) continue 后 dp[now][cur] += dp[pre][last],B 是同形循环里改判 row_ok 后再累加 cost,属模板级复用加台阶。luogu P8756[蓝桥杯 2021 省 AB2] 国际象棋B 直接复用 A 教的「把一层压成二进制状态、先预处理单层合法状态、再逐层枚举兼容状态做计数 DP」这一整套按层状压步骤,只是层从行变成列、兼容表从 cur&pre==0 换成 ok1/ok2 两张表,并按马的数量多开一维 used。
题意
给定一个 n x m 的农田网格。
1 表示这块地可以种草,0 表示不能种草。
要求统计所有合法种草方案数,满足:
- 只能在
1的位置种草 - 任意两块种草的格子不能有公共边
空方案也算一种,答案对 100000000 取模。
思路
先看一个适合小数据理解和对拍的暴力:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MOD = 100000000;
int n, m;
int a[15][15];
int used[15][15];
int ans;
bool ok_place(int x, int y) {
if (a[x][y] == 0) {
return false;
}
if (y > 1 && used[x][y - 1]) {
return false;
}
if (x > 1 && used[x - 1][y]) {
return false;
}
return true;
}
void dfs_cell(int pos) {
if (pos == n * m) {
ans++;
if (ans >= MOD) {
ans -= MOD;
}
return;
}
int x = pos / m + 1;
int y = pos % m + 1;
// 不在这个格子种草。
dfs_cell(pos + 1);
// 在这个格子种草。
if (ok_place(x, y)) {
used[x][y] = 1;
dfs_cell(pos + 1);
used[x][y] = 0;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// brute.cpp:逐格枚举种或不种,用来帮助理解题意和辅助对拍。
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
memset(used, 0, sizeof(used));
ans = 0;
dfs_cell(0);
cout << ans << '\n';
return 0;
}暴力会逐格枚举“种”或“不种”,如果当前格子能种,并且和上方、左方都不冲突,就尝试放下去。
但正解不能逐格做,因为总状态接近 2^(n*m)。
这题真正该利用的是:限制只有上下左右相邻,所以当我们按行处理时:
- 同一行内部只需要保证没有相邻的两个
1 - 当前行只会和上一行发生纵向冲突
于是可以把每一行压成一个二进制状态 mask。
先预处理所有单行合法状态 s,要求:
s & (s << 1) == 0
然后按行 DP,设当前行状态为 cur,上一行状态为 pre。
只要满足:
cur只在可种位置上取1cur & pre == 0
就可以从上一行状态转移过来。
DP 转移方程
按行滚动时,若 cur 是当前行合法状态,pre 是上一行合法状态,且 cur & pre == 0,则:
初始可以看成第 0 行状态为 0,答案是最后一行所有状态之和。
因为只依赖上一行,所以直接用滚动数组即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MOD = 100000000;
const int MAXN = 15;
const int MAXS = 1 << 12;
int n, m;
int allow_mask[MAXN]; // allow_mask[i] 的 1 表示这一格可以种草
int states[MAXS], state_cnt; // 所有单行内部合法状态
int dp[2][MAXS];
bool ok_self(int s) {
// 同一行里不能出现相邻的两块草地。
return (s & (s << 1)) == 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int mask = 0;
for (int j = 0; j < m; j++) {
int x;
cin >> x;
if (x == 1) {
mask |= 1 << j;
}
}
allow_mask[i] = mask;
}
int full = 1 << m;
for (int s = 0; s < full; s++) {
if (ok_self(s)) {
states[state_cnt++] = s;
}
}
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for (int row = 1; row <= n; row++) {
int now = row & 1;
int pre = now ^ 1;
memset(dp[now], 0, sizeof(dp[now]));
for (int i = 0; i < state_cnt; i++) {
int cur = states[i];
// 当前行选的位置必须全部是可种草的土地。
if ((cur & allow_mask[row]) != cur) {
continue;
}
for (int j = 0; j < state_cnt; j++) {
int last = states[j];
// 上下两行不能在同一列都种草。
if (cur & last) {
continue;
}
dp[now][cur] += dp[pre][last];
if (dp[now][cur] >= MOD) {
dp[now][cur] -= MOD;
}
}
}
}
int last = n & 1;
int ans = 0;
for (int i = 0; i < state_cnt; i++) {
ans += dp[last][states[i]];
if (ans >= MOD) {
ans -= MOD;
}
}
cout << ans << '\n';
return 0;
}复杂度
设单行合法状态数为 S,时间复杂度为
总结
这题是“按行状压计数”的基础模板题。
关键在于把“格子之间不能相邻”改写成“单行合法 + 相邻两行兼容”,这样就能自然落到状压 DP 上。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
