逐个放人的阶梯状线性 DP:最高者必占 (1,1),状态只需各排已放人数,前缀阶梯 + 列约束转移计数标准 Young 表个数。
OJ: roj
题目 ID: 3134
难度:提高+/省选-
标签:动态规划线性DPpython
创建: 2026-10-01 19:08
更新: 2026-10-10 11:30
形式化题目
把
样例
正解
思路
朴素做法:枚举
关键观察:编号
于是倒过来想:按身高从高到低(编号
转移:要放当前最高(编号最小)的未放者,可以放在第
:这一排还有空位; 或 :上方那一格(第 排第 个位置)已经有人,否则这个位置根本不在合法的"列"里(列必须连续且从后到前递减)。
设
答案为
以样例形状 ■ 为已放,· 为空位):
| 状态 |
图示 | 合法放法 |
|---|---|---|
■■ / ■· / ■ |
只能第 1 排( |
|
■■ / ■■ / ·· |
只能第 1 排(第 2 排已满,第 3 排上方无人) | |
■■■ / ■·· / ··· |
第 2 排(第 1 排已满,第 3 排上方无人) |
状态用 tuple 直接做哈希表的键,配合 functools.cache 记忆化搜索,一行生成器表达式就完成了全部转移。
代码
/**
* 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
*/
// 方块堆叠方案计数:记忆化搜索,最高的人必须站在某一排最左的空位。
#include <cstdio>
#include <vector>
#include <map>
typedef long long ll;
std::map<std::vector<int>, ll> memo;
std::vector<int> caps;
ll K;
// 已放好 rows[i] 个人的状态下,继续放完所有人(当前最高者)的方案数
ll count_ways(std::vector<int> rows) {
if (rows == caps) return 1; // 所有位置都放满,剩下的恰好填满
std::map<std::vector<int>, ll>::iterator it = memo.find(rows);
if (it != memo.end()) return it->second;
ll sum = 0;
for (ll i = 0; i < K; i++) {
// 第 i 排未满,且它前面一排(更靠后)已放人数更多,保证列不悬空
if (rows[i] < caps[i] && (i == 0 || rows[i - 1] > rows[i])) {
rows[i]++;
sum += count_ways(rows);
rows[i]--;
}
}
memo[rows] = sum;
return sum;
}
int main() {
ll k;
while (scanf("%lld", &k) == 1) {
if (k == 0) break;
caps.assign(k, 0);
for (ll i = 0; i < k; i++) {
scanf("%d", &caps[i]);
}
K = k;
memo.clear();
std::vector<int> rows(k, 0);
printf("%lld\n", count_ways(rows));
}
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 19:08
# update_at: 2026-10-01 19:09
import sys
from functools import cache
@cache
def count(rows: tuple[int, ...], caps: tuple[int, ...]) -> int:
"""已放好 rows[i] 个人的状态下,继续从高到低放完所有人( tallest 未放者)的方案数。"""
if rows == caps: # 所有位置都放满,剩下的恰好填满
return 1
# 当前最高的未放者必须站在某排最左的空位:第 i 排未满,
# 且它前面一排(更靠后)已放人数更多,保证列不悬空。
return sum(
count(rows[:i] + (r + 1,) + rows[i + 1:], caps)
for i, r in enumerate(rows)
if r < caps[i] and (i == 0 or rows[i - 1] > r)
)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
while (k := next(data)) != 0:
caps = tuple(next(data) for _ in range(k)) # 从后向前每排容量,非增
out.append(str(count((0,) * k, caps)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 状态数是
的一部分(其中各 还受 的阶梯限制,实际远小于), 、 时不超过 ; - 每个状态枚举
个转移,总时间 ,约 量级;空间即记忆化哈希表 。 - Python 原生大整数直接输出精确值,无需取模。
总结
- 本题是标准 Young 表(SYT)计数:给定行长的 Young 图形状,求行列都递增的填法数(该形状恰有钩长公式可算,但 DP 更通用)。
- 核心洞察是按编号顺序放人时中间局面必为左对齐行前缀阶梯,把"棋盘上放什么"压缩成"
元组各排已放人数"。 - 转移条件"上一排严格更多"一步编码了列连续性与列递减两个约束。
、 下状态量级 ,记忆化搜索天然免手工进制编码;同样的算法在 C++ 中可开五维数组(只开到各 )填表实现。
图示解析
形状下的放人决策树(局部)
下图展示按编号 f(0,0,0) 处第 f(1,0,0) 后第
graph TD
A["f(0,0,0) = 16<br/>空局面"] -->|放第1排| B["f(1,0,0)<br/>■ / · / ·"]
B -->|放第1排| C["f(2,0,0)<br/>■■ / · / ·"]
B -->|放第2排| D["f(1,1,0)<br/>■ / ■ / ·"]
C -->|放第1排| E["f(3,0,0)<br/>■■■ / · / ·"]
C -->|放第2排| F["f(2,1,0)<br/>■■ / ■ / ·"]
D -->|放第1排| F
观察要点:f(2,1,0) 有两条到达路径,但记忆化只算一次——这正是 DP 计数"按状态合并、无重无漏"的体现;而 f(2,0,0) 到 f(3,0,0) 之后第