棋盘上放 k 枚互不同行同列的棋子:逐行决策并把列占用压成 n 位掩码,记忆化搜索 O(n²·2ⁿ) 计数。
OJ: roj
题目 ID: 1217
难度:普及-
标签:搜索递归位运算
创建: 2026-09-30 00:02
更新: 2026-10-05 05:50
题目描述
在一个给定形状的
输入含多组数据:每组第一行是两个正整数 # 表示棋盘区域、. 表示空白区域;以 -1 -1 结束输入。对每组数据输出一行方案数 #./.#、2;棋盘 ...#/..#./.#../#...、1。
思路
按行决策让"互不同行"自动成立:每行只有"不放"和"在某一列放一枚"两种选择。处理到第
参考代码
Python 版:
python
#!/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-09-30 00:03
# update_at: 2026-09-30 00:21
import sys
from functools import cache
@cache
def count(rows: tuple[int, ...], row: int, k: int, used: int) -> int:
"""第 row 行及以下放完剩余棋子、已占列集合为 used 时的方案数。
每放一枚棋子必然占掉一列,所以"已经放了几枚"由 used 里 1 的个数决定,
不必再单独传一个参数——这正是把状态从"摆到哪"换成"占了哪些列"的收益。
"""
rest = k - used.bit_count() # 还差几枚棋子没放
if rest == 0:
return 1 # 棋子恰好放完:记一种方案
if len(rows) - row < rest:
return 0 # 剩下的行数不够摆,本分支作废
free = rows[row] & ~used # 本行既能放、列又还空闲的位置
return count(rows, row + 1, k, used) + sum(
count(rows, row + 1, k, used | 1 << c) # 本行在第 c 列摆一枚
for c in range(len(rows))
if free >> c & 1
)
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
out: list[str] = []
while True:
n, k = int(next(data)), int(next(data))
if n == -1 and k == -1: # 题面的输入结束标记
break
# 每行压成一个 n 位列掩码:第 c 位为 1 表示 (row, c) 是可放的 '#'
rows = tuple(
sum(1 << c for c, cell in enumerate(next(data).decode()) if cell == '#')
for _ in range(n)
)
out.append(str(count(rows, 0, k, 0)))
count.cache_clear() # 换棋盘就换命名空间,缓存不跨题累积
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
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
* create_at: 2026-10-05 05:50
* update_at: 2026-10-05 05:50
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 8; // n <= 8
int n, k; // 棋盘边长与要摆的棋子数
ll ans; // 当前棋盘的方案数
ll rowMask[MAXN]; // rowMask[r]:第 r 行的 '#' 压成的 n 位掩码,第 c 位为 1 表示 (r,c) 可放
// 记忆化:memo[row][used] 表示第 row 行及以下、列占用掩码为 used 时的方案数
// -1 表示还没算过;每行至多一枚棋子,已放枚数就是 used 里 1 的个数
ll memo[MAXN + 1][1 << MAXN];
bool vis[MAXN + 1][1 << MAXN]; // 对应状态是否已算过
// 第 row 行及以下、列占用为 used 时,把剩余棋子放完的方案数
// 还差枚数 = k - used 中 1 的个数
ll dfs(int row, int used) {
int rest = k - __builtin_popcount(used);
if (rest == 0) return 1; // 棋子恰好放完:记 1 种方案
if (n - row < rest) return 0; // 剩余行数不够摆:剪枝
if (vis[row][used]) return memo[row][used];
ll res = dfs(row + 1, used); // 本行不放棋子
int freeMask = rowMask[row] & ~used; // 本行可放且列还空闲的格子
for (int c = 0; c < n; c++) {
if (freeMask >> c & 1) {
// 本行在第 c 列放一枚棋子
res += dfs(row + 1, used | (1 << c));
}
}
vis[row][used] = true;
memo[row][used] = res;
return res;
}
int main() {
while (true) {
scanf("%d %d", &n, &k);
if (n == -1 && k == -1) break; // 题面的输入结束标记
for (int r = 0; r < n; r++) {
char s[MAXN + 2];
scanf("%s", s);
rowMask[r] = 0;
for (int c = 0; c < n; c++) {
if (s[c] == '#') rowMask[r] |= 1LL << c;
}
}
// 清空记忆化:不同棋盘不能共用状态
for (int r = 0; r <= n; r++) {
for (int m = 0; m < (1 << n); m++) vis[r][m] = false;
}
ans = dfs(0, 0);
printf("%lld\n", ans);
}
return 0;
}