[USACO06NOV] Corn Fields G

GitHub跳转原题关系图返回列表

把每一行压成二进制状态,预处理单行合法状态后按行做状压 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 只在可种位置上取 1
  • cur & pre == 0

就可以从上一行状态转移过来。

DP 转移方程

按行滚动时,若 cur 是当前行合法状态,pre 是上一行合法状态,且 cur & pre == 0,则:

ndp[cur]=(ndp[cur]+dp[pre]) mod 100000000 ndp[cur] = (ndp[cur] + dp[pre]) \bmod 100000000

初始可以看成第 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,时间复杂度为 O(n∗S2)O(n * S^2),空间复杂度为 O(S)O(S)。

总结

这题是“按行状压计数”的基础模板题。

关键在于把“格子之间不能相邻”改写成“单行合法 + 相邻两行兼容”,这样就能自然落到状压 DP 上。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析