交错条件只比较相邻两块的高低,把剩余板按大小重编号即可压出 (剩余块数,当前名次,角色) 的计数状态;递推后逐位试填,整块跳过定位字典序第 C 个排列。
OJ: roj
题目 ID: 3162
难度:提高
标签:动态规划计数DP排列字典序python
创建: 2026-10-01 22:00
更新: 2026-10-10 12:00
形式化题目
求一个排列
即每个内部位置都是局部极值:
把满足条件的排列按字典序排序,给定
以 1 2 3 与 3 2 1 是单调的、不合法,剩下 4 个按字典序排为
1 3 2、2 1 3、2 3 1、3 1 2,所以 2 3 1。
正解
思路
朴素做法:
两条路的瓶颈是同一个——它们只能一个一个地走:
| 做法 | 代价 | |
|---|---|---|
| 枚举全部排列 | ||
| 只枚举合法排列 | 合法围栏总数 | |
| 从第 1 个逐个走到第 |
要除掉这个瓶颈,就必须能直接算出"固定了某个前缀之后,后面还有多少种排法",再用这些块大小一次跳过一整块——这正是"字典序第
观察一:字典序就是试填顺序。 木板长度互不相同,每个方案都是
观察二:合法性只依赖相邻的大小关系,用不到具体数值。 交错条件只比较相邻两块的高低。枚举前缀时,后面能接什么只取决于两件事:当前这块在剩余板里的相对名次,以及它是高位还是低位。已经用掉了哪些具体长度不影响计数——只要还剩
这一步把状态从"用掉的子集 + 具体端点值"压成了
其中
观察三:
:只剩一块板,后面没有板要排, ; 且 :第二块必须更矮,即 ;被剥掉的首元素比第 块高,所以第 块重编号后名次不变:
且 :第二块必须更高,即 ;被剥掉的首元素比第 块矮,所以第 块重编号后名次减 1:
两个分支角色都翻转,且都只依赖
下表是
| 该 |
|||
|---|---|---|---|
| 1 | 1 | 1 | — |
| 2 | 0, 1 | 1, 0 | 2 |
| 3 | 0, 1, 1 | 1, 1, 0 | 4 |
| 4 | 0, 1, 2, 2 | 2, 2, 1, 0 | 10 |
| 5 | 0, 2, 4, 5, 5 | 5, 5, 4, 2, 0 | 32 |
| 6 | 0, 5, 10, 14, 16, 16 | 16, 16, 14, 10, 5, 0 | 122 |
把
观察四:候选取值会被上一块收窄。 定第
- 上一块是高位 → 本块必须更矮 → 只在剩余板中名次更小的那一段里试;
- 上一块是低位 → 本块必须更高 → 只在剩余板中名次更大的那一段里试。
代码里把还没用上的板按升序放在 avail 中,这一段就是连续的下标区间 avail[lo..hi]。
观察五:同一块板的两个角色有固定先后。 第一块没有前驱约束,两个角色都合法。以
把五条观察合起来:
- 用
@cache记忆化算出所有会用到的,一次预处理,多组询问复用; - 从左到右逐位确定:按试填顺序枚举候选
,算出这一堆的大小 ; 大于它就整体跳过并让 减去它,否则选中 ,把 从 avail移除、角色设为、 候选区间收窄到 的一侧,进入下一位。
以
| 步 | 剩余板 | 试填板 | 角色 | 减去前的 |
结论 | |
|---|---|---|---|---|---|---|
| 1 | 1 2 3 4 | 1 | 高位 | 0 | 10 | 跳过( |
| 1 | 1 2 3 4 | 1 | 低位 | 2 | 10 | 跳过 → |
| 1 | 1 2 3 4 | 2 | 高位 | 1 | 8 | 跳过 → |
| 1 | 1 2 3 4 | 2 | 低位 | 2 | 7 | 跳过 → |
| 1 | 1 2 3 4 | 3 | 高位 | 2 | 5 | 跳过 → |
| 1 | 1 2 3 4 | 3 | 低位 | 1 | 3 | 跳过 → |
| 1 | 1 2 3 4 | 4 | 高位 | 2 | 2 | 选中( |
| 2 | 1 2 3 | 1 | 低位 | 1 | 2 | 跳过 → |
| 2 | 1 2 3 | 2 | 低位 | 1 | 1 | 选中 |
| 3 | 1 3 | 3 | 高位 | 1 | 1 | 选中 |
| 4 | 1 | 1 | 低位 | 1 | 1 | 选中 |
得到 4 2 3 1。对照
| 排名 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 围栏 | 1 3 2 4 |
1 4 2 3 |
2 1 4 3 |
2 3 1 4 |
2 4 1 3 |
3 1 4 2 |
3 2 4 1 |
3 4 1 2 |
4 1 3 2 |
4 2 3 1 |
最后一格正是上面的推演结果,说明"最后一个排列"确实排在最大排名上。
代码
/**
* 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>
using namespace std;
typedef long long ll;
const int MAXN = 22;
// way[k][rank][high]:k 块待排的板里,本块排第 rank 小、它是 high 位(1 高位 / 0 低位)时,
// 后面还有多少种排法。板长互不相同,「谁挨着谁」只由相对大小决定,所以剩下 k 块板的长度
// 可以重新编号成 1..k,rank 就是本块的编号——这是唯一需要的状态。
ll way[MAXN + 1][MAXN + 2][2];
ll n, c;
ll avail[MAXN + 2]; // 还没用上的板,长度升序存放;下标 i 就是第 i+1 小
int cnt; // avail 里的板数
ll ans[MAXN + 2];
ll solved_cnt; // 本次询问实际填入的板数(无解时为 0,与 main.py 的空列表对齐)
void build_way() {
way[1][1][0] = way[1][1][1] = 1; // 只剩本块,后面没有板,跳不跳都合法
for (int k = 2; k <= MAXN; k++) {
for (int rank = 1; rank <= k; rank++) {
ll sum_high = 0, sum_low = 0;
for (int r = 1; r < rank; r++) sum_high += way[k - 1][r][0]; // 高位块下一块必须更矮
for (int r = rank + 1; r <= k; r++) sum_low += way[k - 1][r - 1][1]; // 低位块下一块必须更高
way[k][rank][1] = sum_high;
way[k][rank][0] = sum_low;
}
}
}
// 字典序第 c 个围栏:从左往右逐位试填,用「这一位取它时后面还有多少排法」整块跳过
void kth_fence() {
cnt = n;
for (ll i = 1; i <= n; i++) avail[i - 1] = i;
ll high = -1; // -1 表示本块是第一块,没有前一块来限制它
ll lo = 0, hi = n - 1; // 本块的候选板在 avail 中的下标闭区间,由前一块的高低决定
for (ll step = 0; step < n; step++) {
ll k = cnt;
for (ll idx = lo; idx <= hi; idx++) {
// 试填顺序即字典序:候选板按长度升序,同一块板先数「它是高位」的方案(下一块更矮,更小)
ll roles[2];
int role_cnt;
if (high < 0) {
roles[0] = 1;
roles[1] = 0;
role_cnt = 2;
} else {
roles[0] = 1 - high; // 高低交错:角色与上一块相反
role_cnt = 1;
}
int picked = 0;
for (int t = 0; t < role_cnt; t++) {
ll role = roles[t];
ll ways = way[k][idx + 1][role];
if (c > ways) {
c -= ways;
continue;
}
ans[solved_cnt] = avail[idx];
solved_cnt += 1; // ★ 与 main.py 的 ans.append 对齐:计数 = 实际填入次数,不是 step+1
// (否则中途有空轮时 ans[] 留洞,输出残留 0)
for (ll i = idx; i + 1 < cnt; i++) avail[i] = avail[i + 1]; // 删掉这块板
cnt--;
high = role;
if (role == 1) {
lo = 0;
hi = idx - 1; // 高位块只配更矮的板
} else {
lo = idx;
hi = k - 2; // 低位块只配更高的板(删掉后下标整体左移 1)
}
picked = 1;
break;
}
if (picked) break;
}
}
}
int main() {
build_way();
ll cases;
scanf("%lld", &cases);
for (ll t = 0; t < cases; t++) {
scanf("%lld%lld", &n, &c);
solved_cnt = 0;
kth_fence();
// 与 main.py 对齐:无解(c 超过总方案数)时 ans 为空 ⇒ 输出空行
for (ll i = 0; i < solved_cnt; i++) {
if (i) printf(" ");
printf("%lld", ans[i]);
}
printf("\n");
}
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 22:00
# update_at: 2026-10-01 22:00
import sys
from functools import cache
@cache
def suffixes(k: int, rank: int, high: bool) -> int:
"""k 块待排的板里,本块排第 rank 小、它是 high 位时,后面还有多少种排法。
板长互不相同,"谁挨着谁"只由相对大小决定,所以剩下的 k 块板可以把长度重新
编号成 1..k,rank 就是本块的编号——这是唯一需要的状态。角色只约束"下一块往
哪跳":高位板两侧都更低,下一块必须更矮;低位板两侧都更高,下一块必须更高。
"""
if k == 1:
return 1 # 只剩本块,后面没有板,跳不跳都合法
if high: # 下一块取第 1..rank-1 小,它在剥掉本块后仍是第 r 小
return sum(suffixes(k - 1, r, False) for r in range(1, rank))
return sum(suffixes(k - 1, r - 1, True) for r in range(rank + 1, k + 1)) # 本来更高的板,编号前移 1
def kth_fence(n: int, c: int) -> list[int]:
"""字典序第 c 个围栏:从左往右逐位试填,用"这一位取它时后面还有多少排法"整块跳过。"""
avail = list(range(1, n + 1)) # 还没用上的板,长度升序;下标 i 就是第 i+1 小
ans: list[int] = []
high: bool | None = None # None 表示本块是第一块,没有前一块来限制它
lo, hi = 0, n - 1 # 本块的候选板在 avail 中的下标闭区间,由前一块的高低决定
for _ in range(n):
k = len(avail)
roles = (True, False) if high is None else (not high,) # 高低交错:角色与上一块相反
# 试填顺序即字典序:候选板按长度升序,同一块板先数"它是高位"的方案(下一块更矮,更小)。
for idx, role in ((idx, role) for idx in range(lo, hi + 1) for role in roles):
ways = suffixes(k, idx + 1, role)
if c > ways: # 第 c 个落在这一块之后,整块跳过
c -= ways
continue
ans.append(avail[idx])
del avail[idx]
high = role
lo, hi = (0, idx - 1) if role else (idx, k - 2) # 高位块只配更矮的板,低位块只配更高的板
break
return ans
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
cases = next(data) # 询问组数
out: list[str] = []
for _ in range(cases):
n, c = next(data), next(data) # 木板数、要求的排名
out.append(' '.join(map(str, kth_fence(n, c))))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间:
的状态数为 ( 、 、两个角色),每个状态一次 求和,预处理 ;之后每组询问逐位定位,每一位最多试填 个候选、共 位, 。 时合计不到一万次加法,且与 的大小无关。 - 空间:缓存
个计数(Python 原生大整数),加上 avail、ans各。 - 与代码对应:
suffixes就是,它的两个分支分别对应"下一块更矮"和"下一块更高"; kth_fence是逐位定位,lo, hi就是被上一块角色收窄的候选区间;solve只负责读入、调用和输出。
总结
求字典序第
本题的计数状态之所以能压到
实现时有两个容易写错的地方:剥掉首元素后,比它高的板名次要减 1(代码中的 suffixes(k-1, r-1, True));以及第一块板没有前驱约束、两个角色都要试,且同一块板先数高位(下一块更矮的那一堆字典序在前)。
