题解:AcWing 1064 小国王
【题目来源】
AcWing:1064 小国王 - AcWing题库
【题目描述】
在 \(n\times n\) 的棋盘上放 \(k\) 个国王,国王可攻击相邻的 \(8\) 个格子,求使它们无法互相攻击的方案总数。
【输入】
共一行,包含两个整数 \(n\) 和 \(k\)。
【输出】
共一行,表示方案总数,若不能够放置则输出 \(0\)。
【输入样例】
3 2
【输出样例】
16
【核心思想】
-
问题分析:在 \(n \times n\) 的棋盘上放置 \(k\) 个国王,国王可攻击周围 \(8\) 个相邻格子(上下左右及四个对角线)。求使所有国王互不攻击的方案总数。数据范围 \(n \leq 10\),直接枚举 \(C_{n^2}^k\) 不可行。由于每行只有 \(n\) 列,可用二进制数表示每行的放置状态,将问题转化为状态压缩动态规划。
-
算法选择:
- 状态压缩(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\) 个合法状态的方案数
-
关键步骤:
- 预处理合法状态(\(O(2^n \cdot n)\)):
- 枚举 \(s \in [0, 2^n)\),用
check(s)判断同行是否有相邻国王 - 记录合法状态集合
state[],每个状态的国王数量cnt[s]
- 枚举 \(s \in [0, 2^n)\),用
- 预处理状态转移(\(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]\)
- 预处理合法状态(\(O(2^n \cdot n)\)):
-
时间/空间复杂度:
- 时间复杂度:\(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 数组
-
状态压缩动态规划的核心思想:
- 二进制状态表示:将二维棋盘的一行压缩为一个二进制整数,把组合问题转化为状态间的转移问题。每个二进制位代表一个格子的决策(放或不放),大幅降低了状态维度
- 合法状态剪枝:通过预处理筛选出不含相邻 \(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
浙公网安备 33010602011771号