[蓝桥杯 2021 省 AB2] 国际象棋

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

把每一列压成二进制状态,利用马只会影响前两列的性质,做记录前两列状态和已放马数量的轮廓 DP。

OJ: luogu

题目 ID: P8756

难度:提高+/省选-

标签:状态压缩动态规划轮廓DP计数dp

日期: 2026-06-21 05:26

题意

N x M 的棋盘上放 K 个马,要求任意两个马都不能互相攻击。 问方案数,对 1e9+7 取模。

思路

先看一个小数据暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1000000007;

int n, m, k_need;
int board[10][105];
long long ans;
int dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2};
int dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1};

bool ok_place(int x, int y) {
    for (int i = 0; i < 8; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && board[nx][ny]) {
            return false;
        }
    }
    return true;
}

void dfs_cell(int pos, int used) {
    if (used > k_need) {
        return;
    }
    if (pos == n * m) {
        if (used == k_need) {
            ans++;
        }
        return;
    }

    int x = pos % n + 1;
    int y = pos / n + 1;

    dfs_cell(pos + 1, used);

    if (ok_place(x, y)) {
        board[x][y] = 1;
        dfs_cell(pos + 1, used + 1);
        board[x][y] = 0;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // brute.cpp:小数据直接枚举每个格子放不放马。
    cin >> n >> m >> k_need;
    memset(board, 0, sizeof(board));
    ans = 0;
    dfs_cell(0, 0);
    cout << ans % MOD << '\n';
    return 0;
}

暴力就是逐格枚举放不放马,再检查是否合法。

正解要利用 N<=6 很小这一点,把“一整列”压成一个二进制状态。

关键观察是:

  • 当前列只会和前一列、前两列发生马的攻击关系
  • 更早的列不会再影响当前列

因此状态中只需要记住:

  • 前两列的摆放情况
  • 已经放了多少个马

设列状态为 s

预处理两种合法性:

  • ok1[a][b]:相邻两列 a,b 是否冲突
  • ok2[a][b]:相隔两列 a,b 是否冲突

然后做按列推进的 DP:

  • dp[col][pre2][pre1][used]

DP 转移方程

枚举当前列状态 cur,若它和前两列都不冲突,则:

dp[col+1][pre1][cur][used+popcount(cur)]+=dp[col][pre2][pre1][used] dp[col+1][pre1][cur][used+popcount(cur)] \mathrel{+}= dp[col][pre2][pre1][used]

其中合法条件是 ok1[pre1][cur]ok2[pre2][cur]

每次枚举当前列状态 cur,只要满足:

  • ok1[pre1][cur]
  • ok2[pre2][cur]

就能转移。

这就是典型的“小行数、大列数”的轮廓 DP。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1000000007;
const int MAXS = 1 << 6;

int n, m, k_need;
int bit_cnt[MAXS];
int ok1[MAXS][MAXS];
int ok2[MAXS][MAXS];
int valid_next[MAXS][MAXS][MAXS];
int valid_cnt[MAXS][MAXS];
int dp[2][MAXS][MAXS][25];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k_need;

    int full = 1 << n;
    for (int s = 0; s < full; s++) {
        bit_cnt[s] = __builtin_popcount((unsigned int) s);
    }

    int row_mask = full - 1;
    for (int a = 0; a < full; a++) {
        int attack_adj = ((a << 2) | (a >> 2)) & row_mask;
        int attack_far = ((a << 1) | (a >> 1)) & row_mask;
        for (int b = 0; b < full; b++) {
            ok1[a][b] = ((attack_adj & b) == 0);
            ok2[a][b] = ((attack_far & b) == 0);
        }
    }

    for (int p2 = 0; p2 < full; p2++) {
        for (int p1 = 0; p1 < full; p1++) {
            valid_cnt[p2][p1] = 0;
            for (int cur = 0; cur < full; cur++) {
                if (ok1[p1][cur] && ok2[p2][cur]) {
                    valid_next[p2][p1][valid_cnt[p2][p1]++] = cur;
                }
            }
        }
    }

    memset(dp, 0, sizeof(dp));
    dp[0][0][0][0] = 1;

    for (int col = 0; col < m; col++) {
        int now = col & 1;
        int nxt = now ^ 1;
        memset(dp[nxt], 0, sizeof(dp[nxt]));

        for (int p2 = 0; p2 < full; p2++) {
            for (int p1 = 0; p1 < full; p1++) {
                for (int used = 0; used <= k_need; used++) {
                    int ways = dp[now][p2][p1][used];
                    if (ways == 0) {
                        continue;
                    }
                    for (int idx = 0; idx < valid_cnt[p2][p1]; idx++) {
                        int cur = valid_next[p2][p1][idx];
                        int nused = used + bit_cnt[cur];
                        if (nused > k_need) {
                            continue;
                        }
                        int &ref = dp[nxt][p1][cur][nused];
                        ref += ways;
                        if (ref >= MOD) {
                            ref -= MOD;
                        }
                    }
                }
            }
        }
    }

    int last = m & 1;
    int ans = 0;
    for (int p2 = 0; p2 < full; p2++) {
        for (int p1 = 0; p1 < full; p1++) {
            ans += dp[last][p2][p1][k_need];
            if (ans >= MOD) {
                ans -= MOD;
            }
        }
    }

    cout << ans << '\n';
    return 0;
}

复杂度

时间复杂度约为 O(M23NK)O(M * 2^{3N} * K),但由于 N<=6,状态数很小,可以通过。

总结

这题的关键是识别马的攻击范围只跨 1 列和 2 列,因此状态只需要保留前两列。 看清这一点后,问题就会自然落到列状压 DP 上。

一图流解析

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

一图流解析