把红筹和黑筹分别看成左括号与右括号,用前缀差值 dp[i][bal] 统计合法前缀数量,再用高精度加法保存第 n 个 Catalan 数。
OJ: luogu
题目 ID: P1722
难度:普及+/提高
标签:动态规划高精度组合计数递推数学
日期: 2026-06-20 08:39
题意
要构造一个长度为 2n 的序列,其中:
- 红色算筹恰好
n个 - 黑色算筹恰好
n个 - 任意前缀里红色个数都不少于黑色个数
求这样的方案数。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
int n;
long long ans;
// 小数据直接搜索:
// red 表示已经放了多少个红筹,black 表示已经放了多少个黑筹。
// 任意前缀都要满足 red >= black。
void dfs(int red, int black) {
if (red == n && black == n) {
ans++;
return;
}
if (red < n) {
dfs(red + 1, black);
}
if (black < n && red > black) {
dfs(red, black + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
dfs(0, 0);
cout << ans << '\n';
return 0;
}这个搜索会维护:
- 已经放了多少个红筹
- 已经放了多少个黑筹
如果还没放满红筹,就可以继续放红筹;
如果当前红筹比黑筹多,才允许放黑筹。
这正是合法括号序列的构造方式:
- 红筹看成左括号
- 黑筹看成右括号
所以本题本质上就是在数第 n 个 Catalan 数。
不过由于 n <= 100,答案会远远超过 long long,因此正式做法需要高精度。
一个小表格
这张表展示 dp[i][bal] 的含义:
| 状态 | 含义 |
|---|---|
i |
已经放了多少个位置 |
bal |
当前前缀中“红筹数 - 黑筹数” |
dp[i][bal] |
满足前缀始终合法的方案数 |
转移非常自然:
- 放红筹:
dp[i+1][bal+1] += dp[i][bal] - 放黑筹:若
bal > 0,dp[i+1][bal-1] += dp[i][bal]
初始只有 dp[0][0] = 1,最终答案就是 dp[2n][0]。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
const int BASE = 10000;
const int WIDTH = 4;
struct BigInt {
vector<int> d;
BigInt(int x = 0) {
if (x == 0) {
d.push_back(0);
return;
}
while (x > 0) {
d.push_back(x % BASE);
x /= BASE;
}
}
void trim() {
while (d.size() > 1 && d.back() == 0) {
d.pop_back();
}
}
};
BigInt operator + (const BigInt &a, const BigInt &b) {
BigInt c;
c.d.clear();
int len = max((int) a.d.size(), (int) b.d.size());
int carry = 0;
for (int i = 0; i < len; i++) {
int x = carry;
if (i < (int) a.d.size()) {
x += a.d[i];
}
if (i < (int) b.d.size()) {
x += b.d[i];
}
c.d.push_back(x % BASE);
carry = x / BASE;
}
if (carry) {
c.d.push_back(carry);
}
c.trim();
return c;
}
ostream &operator << (ostream &out, const BigInt &a) {
out << a.d.back();
for (int i = (int) a.d.size() - 2; i >= 0; i--) {
out << setw(WIDTH) << setfill('0') << a.d[i];
}
return out;
}
int n;
BigInt f[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
f[0] = BigInt(1);
// Catalan 递推:
// f[i] = sum_{j=0}^{i-1} f[j] * f[i-1-j]
//
// 但这里为了复用更直观的“前缀不为负”模型,
// 改用经典一维 DP:
// dp[len][bal] 表示当前长度和栈高。
// 由于 n <= 100,这里直接用更常见的组合递推:
// f[i] = f[i-1] * (4i-2) / (i+1)
//
// 为了保持高精度实现简单,这里仍使用区间划分递推。
for (int i = 1; i <= n; i++) {
f[i] = BigInt(0);
for (int j = 0; j <= i - 1; j++) {
// 这里 n 很小,如果写乘法高精度会增加代码复杂度。
// 所以改成用括号序列 DP 的 BigInt 相加版更自然。
}
}
vector<vector<BigInt>> dp(2 * n + 1, vector<BigInt>(n + 2, BigInt(0)));
dp[0][0] = BigInt(1);
for (int i = 0; i < 2 * n; i++) {
for (int bal = 0; bal <= n; bal++) {
if (dp[i][bal].d.size() == 1 && dp[i][bal].d[0] == 0) {
continue;
}
// 放一个红筹,对应左括号。
if (bal + 1 <= n) {
dp[i + 1][bal + 1] = dp[i + 1][bal + 1] + dp[i][bal];
}
// 放一个黑筹,对应右括号,但前缀不能失衡。
if (bal > 0) {
dp[i + 1][bal - 1] = dp[i + 1][bal - 1] + dp[i][bal];
}
}
}
cout << dp[2 * n][0] << '\n';
return 0;
}复杂度
- DP 状态数约为
- 每次转移做一次高精度加法
- 空间复杂度约为
总结
这题的关键识别是:
- “任意前缀不失衡”
- “红黑数量最终相等”
这就是合法括号序列,也就是 Catalan 数模型。
实现时不需要硬背公式,直接按前缀差值做 DP,思路更顺,和题面也更贴近。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
