棋盘 / 轮廓状压
互不侵犯·炮兵阵地
本课摘要
棋盘 / 轮廓状压课程回答“逐行棋盘约束怎样编码为兼容位掩码”。内容以互不侵犯·炮兵阵地为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断棋盘 / 轮廓状压的适用条件与状态边界
- 围绕“互不侵犯·炮兵阵地”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
当「一行的选择」有了 2ⁿ 种
先看一个具体问题:在 的棋盘上放国际象棋的王,王会攻击周围 8 格,要求两两互不攻击,问放 2 个王有多少种方案。 如果一行一行地放,每一行的状态无非是「哪些列放了王」——这本身就是 种可能。
为什么不能像线性 DP 那样,只记「这一行放了几个王」?因为下一行能不能放,不只取决于个数,还取决于放在哪些列——两个王只要斜对角相邻就冲突。 「几个」这个标量丢掉了列的信息,是有后效性的。我们需要把「这一行的完整摆法」原封不动地记进状态里。
关键的一步:把一整行的摆法压成一个整数。第 列放了王就让二进制第 位为 ,否则为 。 于是「列 1、列 3 放了王」就是 。整行的 种摆法,一一对应 到 这些整数——这就是状态压缩:用一个 mask 承载一行的全部信息。
这类「逐行推进、把当前行压成 mask」的状压,叫棋盘状压 / 轮廓状压。它只在 很小(通常 )时可行,因为每行要枚举 种摆法。
两道判定:行内合法、行间合法
压成 mask 之后,「合不合法」全部变成位运算——这正是状压的威力。放王有两条约束:
① 行内不相邻。同一行里两个王不能挨着(左右相邻会互相攻击)。把摆法 左移一位再和自己按位与:。 若结果非 ,说明存在某位和它左边一位同时为 1,即有相邻——不合法。合法当且仅当 。
② 行间不冲突。本行 与上一行 ,王会攻击正下、左下、右下三个方向。用三个按位与一起判: 正上方 、左上 、右上 ,三者全为 才合法。
有了判定,状态与转移就顺理成章。设 表示:前 行、一共放了 个王、且第 行摆法为 时的方案数。转移枚举上一行摆法 :
其中记号 表示上一行摆法 与本行 兼容—— 行内合法、且与 行间不冲突。边界:第 1 行 。答案:。
本质
状压把「一行的组合结构」塞进一个整数,于是合法性判定 = 一两条位运算、状态转移 = 枚举相邻两行的 mask 配对。指数级的摆法被 的表格容纳——只在 小才划算,这也是状压的适用边界。
跟着算一遍
用 棋盘、放 个王,把方程跑几步。先列出「行内合法」的一行摆法():
逐行放王,再数尽所有方案
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
当攻击「隔两格」:状态升到两行
互不侵犯里,冲突只发生在相邻行之间,所以状态记住「上一行」就够了。可一旦攻击范围更远,一行就不够了——炮兵阵地就是典型:炮兵沿行、列方向攻击两格,于是同一列上,第 行的炮兵会打到第 行和第 行。
要判断新一行合不合法,必须同时知道前两行的摆法。状态因此升维成 ——第 行摆 、第 行摆 ;转移再枚举第 行的 ,要求 (三行两两同列不撞)。
同行内部也更严:两个炮兵至少隔 列,判定变成 且 。这一步「从记一行升到记两行」,是轮廓状压最常见的进阶跳板——状态里到底要留几行,取决于约束能跨多远。
常见陷阱:合法 mask 要预处理,别每次重算
时合法摆法只有几十个,务必先枚举一遍存进数组(连同它的 ),转移时只在这几十个之间配对。若在四重循环里对全部 现算判定,炮兵那种三行枚举会直接超时。这是棋盘状压能否通过的关键工程点。
例题
#include <iostream>
using namespace std;
long long f[10][2005][1 << 9]; // f[行][已放王数][本行摆法mask]
int st[600], num[600], cnt; // 预处理:行内合法的 mask 及其王数
int main()
{
int n, K;
cin >> n >> K;
for (int s = 0; s < (1 << n); s++) // 枚举一行所有摆法
{
if (s & (s << 1)) continue; // ★行内:相邻两列都放则丢弃
st[cnt] = s;
num[cnt] = __builtin_popcount(s); // 这行放了几个王
cnt++;
}
for (int i = 0; i < cnt; i++) // 第 1 行:直接填
if (num[i] <= K)
f[1][num[i]][st[i]] = 1;
for (int r = 2; r <= n; r++) // 逐行递推
for (int i = 0; i < cnt; i++) // 本行摆法
for (int j = num[i]; j <= K; j++)
for (int p = 0; p < cnt; p++) // 上一行摆法
{
int a = st[i], b = st[p];
if (a & b) continue; // ★正上方相邻
if (a & (b << 1)) continue; // ★左上相邻
if (a & (b >> 1)) continue; // ★右上相邻
f[r][j][a] += f[r - 1][j - num[i]][b];
}
long long ans = 0;
for (int i = 0; i < cnt; i++)
ans += f[n][K][st[i]];
cout << ans << endl;
return 0;
}#include <iostream>
using namespace std;
const int MOD = 1e8;
int g[15]; // g[i]:第 i 行的「贫瘠格」掩码(1=不能种)
int f[15][1 << 12];
int st[5000], cnt; // 行内合法(无横向相邻)的 mask
int main()
{
int m, n;
cin >> m >> n;
for (int i = 1; i <= m; i++)
for (int j = 0; j < n; j++)
{
int x; cin >> x;
if (x == 0) g[i] |= (1 << j); // 0 = 不可种 → 记入贫瘠掩码
}
for (int s = 0; s < (1 << n); s++)
if (!(s & (s << 1))) st[cnt++] = s; // 行内无相邻
f[0][0] = 1; // 第 0 行(虚拟空行)方案数 1
for (int i = 1; i <= m; i++)
for (int a = 0; a < cnt; a++)
{
int s = st[a];
if (s & g[i]) continue; // ★踩到贫瘠格,非法
for (int b = 0; b < cnt; b++)
{
int t = st[b];
if (s & t) continue; // 上下相邻同列不能都种
f[i][s] = (f[i][s] + f[i - 1][t]) % MOD;
}
}
int ans = 0;
for (int a = 0; a < cnt; a++)
ans = (ans + f[m][st[a]]) % MOD;
cout << ans << endl;
return 0;
}#include <iostream>
#include <algorithm>
using namespace std;
int g[105]; // g[i]:第 i 行山地(H)掩码,1=不能放
int st[105], num[105], cnt; // 行内合法:任意两个 1 至少隔 2 列
int f[105][105][105]; // f[行][上一行mask下标][上上行mask下标]
bool ok(int s) // 同行炮兵间隔 ≥ 3(攻击隔两格)
{
return !(s & (s << 1)) && !(s & (s << 2));
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 0; j < m; j++)
{
char c; cin >> c;
if (c == 'H') g[i] |= (1 << j);
}
for (int s = 0; s < (1 << m); s++)
if (ok(s)) { st[cnt] = s; num[cnt] = __builtin_popcount(s); cnt++; }
for (int i = 1; i <= n; i++)
for (int a = 0; a < cnt; a++) // 本行
{
if (st[a] & g[i]) continue;
for (int b = 0; b < cnt; b++) // 上一行
{
if (st[a] & st[b]) continue;
for (int c = 0; c < cnt; c++) // 上上行
{
if (st[a] & st[c]) continue;
if (st[b] & st[c]) continue;
f[i][a][b] = max(f[i][a][b],
f[i - 1][b][c] + num[a]);
}
}
}
int ans = 0;
for (int a = 0; a < cnt; a++)
for (int b = 0; b < cnt; b++)
ans = max(ans, f[n][a][b]);
cout << ans << endl;
return 0;
}
