固定列压缩的状压 DP:行内合法方案只有 60 个,按行推进时状态记录本行与上一行的列方案,新行的方案要求与这两行都不同列。
OJ: roj
题目 ID: 3153
难度:提高
标签:动态规划状态压缩DP
创建: 2026-10-01 21:19
更新: 2026-10-10 11:30
形式化题目
给定 P 或山地 H。在平原格上放置若干门炮,每格至多一门;两门炮会互相攻击,当且仅当它们同行且列距
正解
思路
题面给出
第一步:把"攻击"写成坐标条件。
设两门炮位于
斜方向(比如
3 2
HP
PP
HP正确答案是 2(
第二步:观察"行内合法方案"只由
同一行里两门炮的列距要
前者排除列距 1,后者排除列距 2。这个条件只和
地形是第二个过滤条件:必须
第三步:行间约束只往前看两行。
第
于是状态只需带上最近两行的列方案:
题面样例地图与每行的平原掩码如下(列 1 << c):
| 行 | 平原掩码 |
过滤后的可选方案 | ||||
|---|---|---|---|---|---|---|
| 1 | P | H | P | P | 1101 |
0000 0001 0100 1000 1001 |
| 2 | P | P | H | H | 0011 |
0000 0001 0010 |
| 3 | P | P | P | P | 1111 |
全部 6 个 |
| 4 | P | H | P | P | 1101 |
同第 1 行 |
| 5 | P | H | H | P | 1001 |
0000 0001 1000 1001 |
表的最后两列就是本题最关键的两张"零件表":全 0000、0001、0010、0100、1000、1001),再按每行地形过滤后每行只剩 3~6 个。第 2 行因 HH 丢掉 0100/1000/1001,第 5 行只剩 4 个——先把每行的方案表压小,后面的枚举才便宜。
第四步:转移。
第
这里不需要检查
同一个键
| 层 | 部分状态键 |
说明 |
|---|---|---|
(0000, 0000)=0、(0001, 0000)=1、(1001, 0000)=2 |
第 1 行的每个可选方案各自一个状态, |
|
(0001, 0000)=1、(0001, 0100)=2、(0010, 1001)=3 |
第 2 行方案为 0010、第 1 行为 1001 时累计 3 门 |
对照 (0010, 1001)=3 这一行:第 2 行 0010(列 2)与第 1 行 1001(列 1、4)按位与为 0,所以这一步合法,值就是
第五步:初始值与答案。
第 1 行没有上一行,让它带着
答案是
样例推演。 样例答案 6,一组达到 6 的方案是 PHHP 只有
作为对照,用哈希表存状态时的逐层规模是:第 1 行 5 个状态(含 1001 这种一行两门炮的情形),到第 4 行涨到 22 个,第 5 行因地形受限收缩回 13 个,而各层最大值依次为
代码
/**
* 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
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 炮兵阵地:逐行推进的状态压缩 DP,键是 (本行列方案 x, 上一行列方案 y)。
#include <cstdio>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
typedef long long ll;
const ll SPREAD = 2; // 一门炮沿行、沿列都攻击左右各 SPREAD 格
ll width;
std::vector<ll> legal; // 一行内部互不攻击的列方案
// 一行内部互不攻击的列方案 x:两炮列距为 1 或 2 时会互相攻击
void build_legal() {
legal.clear();
for (ll x = 0; x < (1LL << width); x++) {
if ((x & (x << 1)) == 0 && (x & (x << 2)) == 0) legal.push_back(x);
}
}
ll popcount_ll(ll x) {
ll c = 0;
while (x) {
c += (x & 1);
x >>= 1;
}
return c;
}
int main() {
ll n;
if (scanf("%lld %lld", &n, &width) != 2) return 0;
build_legal();
// 题面每行可能连写,也可能逐字符用空格分隔,统一拼成一串再按行切
std::string flat;
std::string tok;
while (std::cin >> tok) flat += tok;
std::vector<std::string> rows(n);
for (ll i = 0; i < n; i++) rows[i] = flat.substr(i * width, width);
ll S = 1LL << width;
std::vector<ll> cur(S * S, -1), nxt(S * S, -1); // 值 = 已决策行里最多放了多少炮
std::vector<int> keys;
// 每行的平原掩码与真正可选的列方案集合
std::vector<std::vector<ll> > allowed(n);
for (ll r = 0; r < n; r++) {
ll plain = 0;
for (ll c = 0; c < width; c++) {
if (rows[r][c] == 'P') plain |= 1LL << c;
}
for (size_t t = 0; t < legal.size(); t++) {
if ((legal[t] & ~plain) == 0) allowed[r].push_back(legal[t]);
}
}
// 第 0 行没有上一行,用 y = 0 代表这一层约束不存在
for (size_t t = 0; t < allowed[0].size(); t++) {
ll x = allowed[0][t];
cur[x * S + 0] = popcount_ll(x);
keys.push_back((int)(x * S + 0));
}
for (ll r = 1; r < n; r++) {
for (ll t = 0; t < S * S; t++) nxt[t] = -1;
std::vector<int> nkeys;
for (size_t a = 0; a < keys.size(); a++) {
ll x = keys[a] / S, y = keys[a] % S;
ll total = cur[keys[a]];
ll blocked = x | y; // 本行、上行的炮位
for (size_t c = 0; c < allowed[r].size(); c++) {
ll z = allowed[r][c];
if (z & blocked) continue; // 同列即纵向距离 ≤ SPREAD,冲突
ll nk = z * S + x;
ll cand = total + popcount_ll(z);
if (cand > nxt[nk]) {
if (nxt[nk] < 0) nkeys.push_back((int)nk);
nxt[nk] = cand;
}
}
}
cur.swap(nxt);
keys.swap(nkeys);
}
ll ans = 0;
for (size_t a = 0; a < keys.size(); a++) {
if (cur[keys[a]] > ans) ans = cur[keys[a]];
}
printf("%lld\n", ans);
return 0;
}#!/usr/bin/env python3
# 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
# create_at: 2026-10-01 21:19
# update_at: 2026-10-01 21:19
import sys
SPREAD = 2 # 一门炮沿行、沿列都攻击左右各 SPREAD 格;攻击不受地形阻挡
def legal_columns(width: int) -> list[int]:
"""一行内部互不攻击的列方案 x:两炮列距为 1 或 2 时会互相攻击。"""
return [x for x in range(1 << width) if not x & (x << 1) and not x & (x << 2)]
def advance(states: dict[tuple[int, int], int], allowed: list[int]) -> dict[tuple[int, int], int]:
"""枚举当前行的合法列方案,把状态键从 (本行 x, 上一行 y) 推进到 (新行 z, 本行 x)。
states 的值是「已决策的那些行里最多放了多少炮」;键用题解里的 (x, y) 符号,
即本行、上一行的列占用方案,所以新行的候选变量也沿用题解的 z。
"""
nxt: dict[tuple[int, int], int] = {}
for (x, y), total in states.items():
blocked = x | y # 本行、上行的炮位;新行在这些列同列放置就会误伤
for z in allowed:
if z & blocked: # 同列即纵向距离 ≤ SPREAD,冲突
continue
key = (z, x)
total_here = total + z.bit_count()
best_before = nxt.get(key, -1) # 同一个键可能由不同的更早行到达
if total_here > best_before:
nxt[key] = total_here
return nxt
def deploy(rows: list[str], width: int) -> int:
"""逐行推进的状态压缩 DP,返回全地图最多能部署的炮兵数。"""
legal = legal_columns(width)
# 每行的平原掩码:第 c 位为 1 表示该格是平原,可以放炮;山地格不能放
plains = [sum(1 << c for c, cell in enumerate(row) if cell == "P") for row in rows]
# 行内合法且不含炮位落在山地的方案,就是该行真正可选的列方案集合
allowed = [[x for x in legal if not x & ~plain] for plain in plains]
# 第 0 行没有上一行,用 y = 0(空掩码)代表这一层约束不存在
states = {(x, 0): x.bit_count() for x in allowed[0]}
for row_allowed in allowed[1:]:
states = advance(states, row_allowed)
return max(states.values())
def solve() -> None:
it = iter(sys.stdin.buffer.read().split())
try:
n = int(next(it))
width = int(next(it))
except StopIteration:
return
# 题面每行可能连写,也可能逐字符用空格分隔,统一拼成一串再按行切
flat = b"".join(it).decode()
rows = [flat[i * width:(i + 1) * width] for i in range(n)]
print(deploy(rows, width))
if __name__ == "__main__":
solve()复杂度
设
- 时间:预处理
;每行枚举 ,共 。 的粗略上界约 ,而实际每层状态数远小于 ;地图中的山地会进一步压缩每行的可选方案。全 P的最坏数据实测约 0.44s / 16 MB,满足 1000ms、128MB 的限制。 - 空间:每层一个哈希表,键数
;另存每行可选方案 。整体 。
总结
- 先固定列方向:
大 小( 、 )就是"按行推进 + 列方向状压"的信号, 。 - 把行内合法性提前枚举掉:行内约束只由
决定, 时只剩 60 个合法掩码,比在转移里现场判断划算得多。 - 状态记住最近两行:因为同列且行距
都会攻击,行距 2 也不例外,所以两行记忆刚好够;更早的行约束已被逐步消费。 - 地形是过滤器,不是状态:它只影响每行"允许哪些方案",不影响攻击传播,所以放在状态外面处理。
- 历史型状态取
:与"轮廓线/插头 DP"里需要收尾归零的状态不同,这里的末态没有欠账。
图示解析
状态推进路线
N*M 网格(P/H)
`- 攻击 = 同行列距<=2 或 同列行距<=2(斜向不算)
|- 行内约束只由 M 决定 -> 预计算 60 个合法掩码,再按地形过滤每行方案
`- 行间约束只涉及最近两行 -> 状态 (本行 x, 上一行 y)
|- 新行 z 满足 z & (x|y) == 0 才允许
`- f[i+1][z][x] = max(..., f[i][x][y] + popcount(z)) -> 答案 max(f[N])先看第一层如何把"十字攻击"化简成两个一维条件,再看状态为什么只需要两行,最后确认答案为什么不需要收尾约束。图中用 <= 做文本示意,正文中的严格表述是不等式
