棋盘问题

GitHub跳转原题关系图返回列表

棋盘上放 k 枚互不同行同列的棋子:逐行决策并把列占用压成 n 位掩码,记忆化搜索 O(n²·2ⁿ) 计数。

OJ: roj

题目 ID: 1217

难度:普及-

标签:搜索递归位运算

创建: 2026-09-30 00:02

更新: 2026-10-05 05:50

题目描述

在一个给定形状的 n×nn \times n 棋盘(形状可能不规则)上摆放棋子,棋子没有区别。要求任意两个棋子不在同一行或同一列,求摆放 kk 个棋子的可行方案数 CC。

输入含多组数据:每组第一行是两个正整数 n,kn, k(n⩽8n \leqslant 8,k⩽nk \leqslant n),随后 nn 行每行 nn 个字符,# 表示棋盘区域、. 表示空白区域;以 -1 -1 结束输入。对每组数据输出一行方案数 CC(保证 C<231C < 2^{31})。样例:棋盘 #./.#、k=1k=1 时输出 2;棋盘 ...#/..#./.#../#...、k=4k=4 时输出 1。

思路

按行决策让"互不同行"自动成立:每行只有"不放"和"在某一列放一枚"两种选择。处理到第 rowrow 行时,未来只取决于哪些列已被占用,于是把列占用压成 nn 位掩码 usedused(已放枚数即 usedused 中 1 的个数),记忆化搜索 count(row,used)=count(row+1,used)+∑c 可放且空闲count(row+1,used∪{c})count(row, used) = count(row+1, used) + \sum_{c \text{ 可放且空闲}} count(row+1, used \cup \{c\})。剪枝:usedused 已有 kk 个 1 时记 1 种方案,剩余行数 n−rown - row 小于还差枚数时记 0,总复杂度 O(n2⋅2n)O(n^2 \cdot 2^n)。

参考代码

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;
}