[SCOI2005] 互不侵犯

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

状态压缩 DP:压缩每行国王摆放为 bitmask,逐行转移,合法状态需满足同行不相邻且上下行不冲突。

OJ: luogu

题目 ID: P1896

难度:普及+/提高

标签:动态规划状压DP位运算

日期: 2026-07-07 00:00

题意

N×NN\times N 棋盘放 KK 个国王,国王攻击周围 88 格。求互不攻击的方案数。N9N\leqslant 9KN2K\leqslant N^2

思路

N9N\leqslant 9 但直接枚举 N×NN\times N 格子的 01 序列是 2N22^{N^2},完全不可行。先看一个只适合小数据的暴力:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * date: 2026-07-07 00:00:00
 */
// brute.cpp:小数据暴力解,使用 01 序列递归枚举每个格子是否放国王。
#include <bits/stdc++.h>
using namespace std;

int n, k;
int board[10][10];   // board[r][c] = 1 表示 (r,c) 放国王
int choose_cnt;       // 当前已放国王数
long long ans;

// 检查 (r,c) 放国王是否与已放置的冲突(只检查上方和左方)
bool can_place(int r, int c) {
    for (int dr = -1; dr <= 1; dr++) {
        for (int dc = -1; dc <= 1; dc++) {
            if (dr == 0 && dc == 0) continue;
            int nr = r + dr, nc = c + dc;
            if (nr >= 1 && nr <= n && nc >= 1 && nc <= n) {
                if (board[nr][nc] == 1) return false;
            }
        }
    }
    return true;
}

// 按行优先顺序递归枚举每个格子
void dfs(int r, int c) {
    if (r > n) {
        // 所有格子处理完
        if (choose_cnt == k) ans++;
        return;
    }

    int nr = r, nc = c + 1;
    if (nc > n) { nr++; nc = 1; }

    // 选择1:不放国王
    board[r][c] = 0;
    dfs(nr, nc);

    // 选择2:放国王
    if (choose_cnt < k && can_place(r, c)) {
        board[r][c] = 1;
        choose_cnt++;
        dfs(nr, nc);
        choose_cnt--;
        board[r][c] = 0;
    }
}

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

    cin >> n >> k;
    dfs(1, 1);
    cout << ans << '\n';
    return 0;
}

暴力按行优先顺序递归枚举每个格子选或不选,放置前检查周围 8 格是否冲突。这个写法只适合 N4N\leqslant 4 验证数据。

状态压缩

每一行只有 N9N\leqslant 9 个格子,可以把一行的国王摆放方案压缩成一个二进制数(bitmask)。第 ii 位为 11 表示该列放国王。

单行约束:同一行相邻两格不能都有国王,即 (mask & (mask << 1)) == 0

SS 为所有满足单行约束的合法 mask 集合。N=9N=9S|S| 不到 9090 个。

DP 定义与转移

dp[row][mask][cnt]:前 row 行,第 row 行摆放状态为 mask,总共放了 cnt 个国王的方案数。

转移:从第 row-1 行的 prev 状态转移到第 row 行的 cur 状态,需要满足:

  • 正上方不冲突:(cur & prev) == 0
  • 左上方不冲突:((cur << 1) & prev) == 0
  • 右上方不冲突:((cur >> 1) & prev) == 0

第 1 行初始化:dp[1][mask][popcount(mask)] = 1(对每个合法 mask)。

最终答案:maskSdp[n][mask][k]\sum_{mask \in S} dp[n][mask][k]

样例 DP 状态转移表

N=3,K=2N=3, K=2 的样例为例,下表展示部分关键行的 DP 转移:

row cur (二进制) 国王数 来自 prev dp[row][cur][cnt] 累计值
1 000 (0) 0 1
1 001 (1) 1 1
1 010 (2) 1 1
1 100 (4) 1 1
1 101 (5) 2 1
2 000 (0) 0 0/2/5 总计行1所有 cnt=0→3 份
2 010 (2) 1 0 dp[1][0][0] = 1
2 010 (2) 2 1,4 1+1 = 2
2 000 (0) 1 1,2,4 3

重点观察:第 2 行 cur=010 时,只能从第 1 行那些不与 010 冲突的 prev 转移过来。比如 prev=001(第 1 列有王)会与 cur=010 的左上方冲突,因此 prev=1 不可作为转移来源。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * date: 2026-07-07 00:00:00
 */
// main.cpp:状态压缩 DP,逐行转移。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 10;
const int MAXK = 85;
const int MAXS = 1 << 9;  // N ≤ 9

int n, k;
long long dp[MAXN][MAXS][MAXK];  // dp[row][mask][cnt]

// valid_one[mask] = 1 表示 mask 中没有任何相邻的 1
int valid_one[MAXS];
// king_cnt[mask] = mask 中 1 的个数
int king_cnt[MAXS];
// valid_sets 保存所有合法的单行状态
vector<int> valid_sets;

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

    cin >> n >> k;

    // 预处理所有合法的单行状态
    for (int mask = 0; mask < (1 << n); mask++) {
        if (mask & (mask << 1)) continue;  // 同一行有相邻国王
        valid_one[mask] = 1;
        king_cnt[mask] = __builtin_popcount(mask);
        valid_sets.push_back(mask);
    }

    // 第 1 行初始化
    for (int mask : valid_sets) {
        if (king_cnt[mask] <= k) {
            dp[1][mask][king_cnt[mask]] = 1;
        }
    }

    // 逐行 DP
    for (int row = 2; row <= n; row++) {
        for (int cur : valid_sets) {
            int cnt_cur = king_cnt[cur];
            if (cnt_cur > k) continue;

            for (int prev : valid_sets) {
                // 检查上下行冲突
                if ((cur & prev) || ((cur << 1) & prev) || ((cur >> 1) & prev)) continue;

                for (int c = cnt_cur; c <= k; c++) {
                    dp[row][cur][c] += dp[row - 1][prev][c - cnt_cur];
                }
            }
        }
    }

    // 汇总答案
    long long ans = 0;
    for (int mask : valid_sets) {
        ans += dp[n][mask][k];
    }

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

复杂度

  • 预处理合法 mask:O(2N)O(2^N)
  • DP 转移:O(NS2K)O(N \cdot |S|^2 \cdot K)S90|S| \approx 90N=9N=9K81K\leqslant 81,实际约 9×8100×815.9×1069 \times 8100 \times 81 \approx 5.9\times 10^6 次运算
  • 空间复杂度:O(NSK)O(N \cdot |S| \cdot K), 可用滚动数组优化到 O(SK)O(|S| \cdot K)

总结

状压 DP 的经典特征:NN 很小(通常 20\leqslant 20),且状态可以用"一行/一列"为单位处理。本题中 N9N\leqslant 9 是关键提示。核心步骤:

  1. 把一行压缩为 bitmask
  2. 预处理单行合法状态
  3. 定义 dp[row][mask][附加信息],按行转移
  4. 用位运算快速判断行间冲突