题解:AcWing 1064 小国王

【题目来源】

AcWing:1064 小国王 - AcWing题库

【题目描述】

\(n\times n\) 的棋盘上放 \(k\) 个国王,国王可攻击相邻的 \(8\) 个格子,求使它们无法互相攻击的方案总数。

【输入】

共一行,包含两个整数 \(n\)\(k\)

【输出】

共一行,表示方案总数,若不能够放置则输出 \(0\)

【输入样例】

3 2

【输出样例】

16

【核心思想】

  1. 问题分析:在 \(n \times n\) 的棋盘上放置 \(k\) 个国王,国王可攻击周围 \(8\) 个相邻格子(上下左右及四个对角线)。求使所有国王互不攻击的方案总数。数据范围 \(n \leq 10\),直接枚举 \(C_{n^2}^k\) 不可行。由于每行只有 \(n\) 列,可用二进制数表示每行的放置状态,将问题转化为状态压缩动态规划

  2. 算法选择

    • 状态压缩(State Compression):用 \(n\) 位二进制数 \(s\) 表示一行的放置状态,第 \(j\) 位为 \(1\) 表示第 \(j\) 列放置了国王。每行状态范围为 \([0, 2^n)\)
    • 合法状态预处理:同一行内国王不能相邻,即二进制状态中不能有相邻的 \(1\)。通过 check(state) 筛选所有合法状态
    • 状态转移预处理:相邻两行(包括对角线)不能互相攻击。设上行状态为 \(a\),下行状态为 \(b\),需满足:\((a \& b) == 0\)(无同列国王)且 \(\text{check}(a \mid b)\)(无对角线相邻国王)
    • 三维动态规划\(f[i][j][a]\) 表示前 \(i\) 行放置了 \(j\) 个国王,且第 \(i\) 行状态为第 \(a\) 个合法状态的方案数
  3. 关键步骤

    • 预处理合法状态\(O(2^n \cdot n)\)):
      • 枚举 \(s \in [0, 2^n)\),用 check(s) 判断同行是否有相邻国王
      • 记录合法状态集合 state[],每个状态的国王数量 cnt[s]
    • 预处理状态转移\(O(S^2)\)\(S\) 为合法状态数):
      • 对每对合法状态 \((a, b)\),检查 \((a \& b) == 0\)\(\text{check}(a \mid b)\)
      • 若合法,则 \(b\) 可以转移到 \(a\),加入邻接表 head[]
    • 动态规划转移
      • 初始化:\(f[0][0][0] = 1\)(第 \(0\) 行放置 \(0\) 个国王,空状态)
      • 枚举行 \(i\)\(1\)\(n\)(或 \(n+1\) 方便统计)
      • 枚举已放置国王数 \(j\)\(0\)\(k\)
      • 枚举当前行合法状态 \(a\),其中国王数为 \(c = \text{cnt}[\text{state}[a]]\)
      • 枚举上一行合法状态 \(b \in \text{head}[a]\)
      • \(j \geq c\):\ \(f[i][j][a] += f[i-1][j-c][b]\)
    • 输出答案\(f[n][k][\text{任意合法状态}]\) 或利用虚拟第 \(n+1\)\(f[n+1][k][0]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot k \cdot S^2)\),其中 \(S\) 为一行合法状态数(\(n=10\)\(S \approx 144\))。预处理 \(O(2^n \cdot n + S^2)\),DP 转移为 \(O(n \cdot k \cdot S^2)\)
    • 空间复杂度:\(O(n \cdot k \cdot S)\),存储三维 DP 数组
  5. 状态压缩动态规划的核心思想

    • 二进制状态表示:将二维棋盘的一行压缩为一个二进制整数,把组合问题转化为状态间的转移问题。每个二进制位代表一个格子的决策(放或不放),大幅降低了状态维度
    • 合法状态剪枝:通过预处理筛选出不含相邻 \(1\) 的状态(斐波那契数列增长,远少于 \(2^n\)),将状态数从指数级降到多项式级
    • 转移相容性检查:不仅检查同一行内是否合法,还要检查相邻两行之间是否冲突(同列、对角线)。利用位运算 \((a \& b)\)\((a \mid b)\)\(O(1)\) 内完成检查
    • 逐行递推:棋盘问题中,当前行的决策只与上一行有关,因此可以按行进行 DP,将二维棋盘问题分解为逐行的状态转移
    • 多维度 DP 设计:本题需要同时记录行数、已放置国王数和当前行状态三个维度,通过增加国王数量维度来限制方案必须恰好使用 \(k\) 个国王
    • 适用于:棋盘放置问题、网格路径问题、有限制条件的排列组合问题等

【算法标签】

状压DP

【代码详解】

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

#define int long long  // 定义宏,将int替换为long long类型
const int N = 12, M = 1 << 10, K = 105;  // 定义常量:棋盘最大行数N,状态数M,国王数K
int n, k;  // 棋盘大小n*n,需要放置的国王数量k
vector<int> state;  // 存储所有合法状态(不相邻的国王放置方式)
int id[M], cnt[M];  // id记录状态对应的索引,cnt记录状态中的国王数量
vector<int> head[M];  // 存储每个状态可以转移到的其他状态
int f[N][K][M];  // DP数组,f[i][j][a]表示前i行放置j个国王且第i行状态为a的方案数

// 检查状态是否合法(没有相邻的国王)
bool check(int state) {
    for (int i = 0; i < n; i++)
        if ((state >> i & 1) && (state >> (i + 1) & 1))  // 检查是否有相邻的1
            return false;
    return true;
}

// 计算状态中1的个数(国王数量)
int count(int state) {
    int res = 0;
    for (int i = 0; i < n; i++)
        res += state >> i & 1;
    return res;
}

signed main() {
    cin >> n >> k;  // 输入棋盘大小和国王数量

    // 预处理所有合法状态
    for (int i = 0; i < (1 << n); i++) {
        if (check(i)) {
            state.push_back(i);  // 存储合法状态
            id[i] = state.size() - 1;  // 记录状态对应的索引
            cnt[i] = count(i);  // 记录状态中的国王数量
        }
    }

    // 预处理状态之间的转移关系
    for (int i = 0; i < state.size(); i++) {
        for (int j = 0; j < state.size(); j++) {
            int a = state[i], b = state[j];
            // 检查状态a和b是否可以相邻(不冲突且合并后仍合法)
            if ((a & b) == 0 && check(a | b)) {
                head[i].push_back(j);  // 记录状态i可以转移到状态j
            }
        }
    }

    // 初始化DP数组:第0行放置0个国王且状态为0的方案数为1
    f[0][0][0] = 1;

    // 动态规划过程
    for (int i = 1; i <= n + 1; i++) {  // 遍历每一行(多处理一行方便统计结果)
        for (int j = 0; j <= k; j++) {  // 遍历可能的国王数量
            for (int a = 0; a < state.size(); a++) {  // 遍历当前行的状态
                for (int b : head[a]) {  // 遍历可以转移到状态a的上一行状态b
                    int c = cnt[state[a]];  // 当前行状态a的国王数量
                    if (j >= c) {
                        // 状态转移:前i行放置j个国王的方案数 += 前i-1行放置j-c个国王的方案数
                        f[i][j][a] += f[i - 1][j - c][b];
                    }
                }
            }
        }
    }

    // 输出结果:前n+1行放置k个国王且第n+1行状态为0(即第n行无限制)的方案数
    cout << f[n + 1][k][0] << endl;

    return 0;
}
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, k;  // 棋盘行数,国王总数
int cnt;  // 同一行的合法状态个数
int s[1<<12];  // 同一行的合法状态集
int num[1<<12];  // 每个合法状态包含的国王数
int f[12][144][1<<12];  // f[i,j,a]表示前i行方了j个国王,第i行第a个状态时的方案数

signed main()
{
    cin >> n >> k;  // 输入行数,国王总数
    // 预处理
    for (int i=0; i<(1<<n); i++)  // 枚举一行的所有状态
        if (!(i&i>>1))  // 如果不存在相邻的1
        {
            s[cnt++] = i;  // 保存一行的合法状态
            for (int j=0; j<n; j++)
                num[i] += (i>>j & 1);  // 统计每个合法状态包含的国王数
        }
    // 状态计算
    f[0][0][0] = 1;  // 不放国王也是一种方案
    for (int i=1; i<=n; i++)  // 枚举行
        for (int j=0; j<=k; j++)  // 枚举国王数
            for (int a=0; a<cnt; a++)  // 枚举第i行的合法状态
                for (int b=0; b<cnt; b++)  // 枚举第i-1行的合法状态
                {
                    int c = num[s[a]];  // 第i行第a个状态的国王数
                    if ((j>=c) && !(s[b]&s[a]) && !(s[b]&(s[a]<<1)) && !(s[b]&(s[a]>>1)))
                        f[i][j][a] += f[i-1][j-c][b];  // 从第i-1行向第i行转移
                }
    int ans = 0;
    for (int i=0; i<cnt; i++) ans += f[n][k][i];  // 计算方案数
    cout << ans << endl;
    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 12, M = 1 << 10, K = 105;
int n, k;  // n: 棋盘大小, k: 国王数量
vector<int> state;  // 存储所有合法状态
int id[M], cnt[M];  // id: 状态对应的下标, cnt: 状态中1的个数
vector<int> head[M];  // 状态转移关系
int f[N][K][M];  // DP数组: f[行数][已放置国王数][状态]

// 检查状态是否合法:相邻两列不能有相邻的1
bool check(int state)
{
    for (int i = 0; i < n; i++)
    {
        if ((state >> i & 1) && (state >> i + 1 & 1))  // 相邻位置都是1
        {
            return false;
        }
    }
    return true;
}
// 计算状态中1的个数
int count(int state)
{
    int res = 0;
    for (int i = 0; i < n; i++)
    {
        res += state >> i & 1;  // 统计1的个数
    }
    return res;
}
signed main()
{
    cin >> n >> k;  // 输入棋盘大小和国王数量
    // 预处理所有合法状态
    for (int i = 0; i < (1 << n); i++)
    {
        if (check(i))  // 如果状态合法
        {
            state.push_back(i);
            id[i] = state.size() - 1;  // 合法状态对应的下标
            cnt[i] = count(i);  // 保存合法状态中1的个数
        }
    }
    // 预处理状态转移关系
    for (int i = 0; i < state.size(); i++)
    {
        for (int j = 0; j < state.size(); j++)
        {
            int a = state[i], b = state[j];
            // 上下行不冲突的条件:
            // 1. 上下行没有重叠的1 (a&b)==0
            // 2. 斜向也不冲突 check(a|b)
            if ((a & b) == 0 && check(a | b))
            {
                head[i].push_back(j);
            }
        }
    }
    // 动态规划
    f[0][0][0] = 1;  // 初始化:第0行放0个国王,状态为0
    for (int i = 1; i <= n + 1; i++)  // 遍历每一行
    {
        for (int j = 0; j <= k; j++)  // 遍历已放置的国王数
        {
            for (int a = 0; a < state.size(); a++)  // 当前行状态
            {
                for (int b : head[a])  // 上一行状态
                {
                    int c = cnt[state[a]];  // 当前行国王数
                    if (j >= c)  // 国王数足够
                    {
                        f[i][j][a] += f[i - 1][j - c][b];
                    }
                }
            }
        }
    }
    cout << f[n + 1][k][0] << endl;  // 第n+1行不放国王,状态为0
    return 0;
}

【运行结果】

3 2
16
posted @ 2026-06-23 10:51  团爸讲算法  阅读(10)  评论(0)    收藏  举报