题解:AcWing 889 满足条件的01序列
【题目来源】
AcWing:889 满足条件的01序列 - AcWing题库
【题目描述】
给定 \(n\) 个 \(0\) 和 \(n\) 个 \(1\),它们将按照某种顺序排成长度为 \(2n\) 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 \(0\) 的个数都不少于 \(1\) 的个数的序列有多少个。
输出的答案对 \(10^9+7\) 取模。
【输入】
共一行,包含整数 \(n\)。
【输出】
共一行,包含一个整数,表示答案。
【输入样例】
3
【输出样例】
5
【核心思想】
-
问题分析:给定 \(n\) 个 \(0\) 和 \(n\) 个 \(1\),要求排成长度为 \(2n\) 的序列,且任意前缀中 \(0\) 的个数不少于 \(1\) 的个数。所有无约束的排列数为 \(C_{2n}^n\)(从 \(2n\) 个位置中选 \(n\) 个放 \(1\)),需要从中减去不合法的排列数。这是经典的卡特兰数(Catalan Number)问题。
-
算法选择:
- 卡特兰数公式:第 \(n\) 个卡特兰数 \(\text{Cat}(n) = \frac{1}{n+1} C_{2n}^n = C_{2n}^n - C_{2n}^{n-1}\)
- 反射原理(André's Reflection Principle):计算不合法序列数的关键技巧
- 费马小定理求逆元:模数 \(p = 10^9+7\) 为质数,用快速幂计算 \((n+1)^{-1} \bmod p\) 和 \(i^{-1} \bmod p\)
-
关键步骤:
- 总排列数:从 \(2n\) 个位置中选 \(n\) 个放 \(1\),总数为 \(C_{2n}^n\)
- 不合法序列计数(反射原理):
- 一个序列不合法,当且仅当存在某个前缀使得 \(1\) 的个数比 \(0\) 多 \(1\)(即 \(1\) 比 \(0\) 多)
- 找到第一个不合法位置,将该位置之前的前缀中所有 \(0\) 和 \(1\) 互换(\(0 \leftrightarrow 1\))
- 互换后,原序列中有 \((n+1)\) 个 \(1\) 和 \((n-1)\) 个 \(0\),即与从 \(2n\) 个位置中选 \((n+1)\) 个放 \(1\) 的序列一一对应
- 因此不合法序列数 = \(C_{2n}^{n+1} = C_{2n}^{n-1}\)
- 合法序列数:\(\text{Cat}(n) = C_{2n}^n - C_{2n}^{n-1} = \frac{(2n)!}{n!(n+1)!}\)
- 计算过程(代码实现):
- 计算分子连乘:
res = (2n) \cdot (2n-1) \cdot \ldots \cdot (n+1) \bmod p - 计算分母逆元:乘以 \((1 \cdot 2 \cdot \ldots \cdot n)^{-1} \bmod p\),即乘以 \(n!\) 的逆元
- 再乘以 \((n+1)^{-1} \bmod p\)
- 最终得到 \(\text{Cat}(n) \bmod p\)
- 计算分子连乘:
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log p)\)。分子连乘 \(O(n)\),分母逆元计算中每个逆元用快速幂 \(O(\log p)\),共 \(n\) 个;若递推求逆元可优化至 \(O(n)\)
- 空间复杂度:\(O(1)\)(或 \(O(n)\) 若存储阶乘逆元数组),仅使用常数个变量
-
卡特兰数与反射原理的核心思想:
- 卡特兰数的组合意义:卡特兰数计数多种经典问题——合法的括号序列数、不交叉的弦划分、二叉搜索树个数、栈的合法出栈序列、网格路径不越过对角线等。本题即为"前缀中 \(0\) 不少于 \(1\)"的计数问题
- 反射原理:对于"不合法"序列,找到第一个违反条件的位置,将前面部分全部取反(\(0 \leftrightarrow 1\)),建立不合法序列与某个更易计数的集合之间的一一对应关系。这是一种双射(bijection)技巧,将难数的集合映射到易数的集合
- 公式变形:\(\text{Cat}(n) = \frac{1}{n+1} C_{2n}^n = C_{2n}^n - C_{2n}^{n-1}\)。前者便于编程实现(一次除法),后者便于理解(总数减不合法数)
- 适用场景:卡特兰数适用于计数各种"不跨越对角线"、"前缀合法"、"匹配不交叉"的组合问题;反射原理适用于计算格路问题中不越过某条边界的路径数
【解题思路】





【算法标签】
排列组合
【代码详解】
#include <bits/stdc++.h>
using namespace std;
typedef long long LL; // 定义 LL 为 long long 类型
const int N = 100005, mod = 1e9 + 7; // 定义常量 N 和 mod
LL infact[N]; // infact 数组存储阶乘的逆元
// 快速幂函数,计算 a^b % p
int qmi(int a, int b, int p)
{
int res = 1; // 初始化结果为 1
while (b > 0) { // 当 b 大于 0 时循环
if (b & 1) res = (LL)res * a % p; // 如果 b 的最低位为 1,更新结果
a = (LL)a * a % p; // 更新 a
b >>= 1; // 右移 b
}
return res; // 返回结果
}
int main()
{
int n; // 定义整数 n
cin >> n; // 输入整数 n
int res = 1; // 初始化结果为 1
// 计算 (2n)! / (n+1)! / n! 的模数
for (int i = 2 * n; i > n; i--) res = (LL)res * i % mod; // 计算 (2n)! / n! 的模数
infact[0] = 1; // 初始化 infact[0] 为 1
for (int i = 1; i <= n; i++) // 计算 1 到 n 的阶乘逆元
infact[i] = infact[i - 1] * qmi(i, mod - 2, mod) % mod; // 递推计算阶乘逆元
res = (LL)res * infact[n] % mod; // 计算 (2n)! / (n+1)! / n! 的模数
res = (LL)res * qmi(n + 1, mod - 2, mod) % mod; // 除以 (n+1) 的模数
cout << res << endl; // 输出结果
return 0; // 程序结束
}
#include <bits/stdc++.h>
using namespace std;
#define int long long // 使用long long防止溢出
const int mod = 1e9 + 7; // 模数
// 快速幂算法,计算 a^b mod p
int qmi(int a, int b, int p)
{
int res = 1; // 结果初始化为1
while (b) // 当指数b不为0时继续
{
if (b & 1) // 如果b的最低位是1
{
res = res * a % p; // 将当前a乘入结果
}
a = a * a % p; // a自乘
b >>= 1; // b右移一位
}
return res; // 返回a^b mod p
}
signed main() // 因为使用了#define int long long,所以用signed main
{
int n; // 输入卡特兰数的下标
cin >> n;
// 计算卡特兰数 Cat(n) = C(2n, n) / (n+1)
int a = 2 * n; // 分子是2n
int b = n; // 分母是n
int res = 1; // 存储结果
// 计算 C(2n, n) 的分子部分:从2n乘到n+1
for (int i = a; i > a - b; i--) // i从2n递减到n+1
{
res = res * i % mod;
}
// 计算 C(2n, n) 的分母部分:除以n!,即乘以每个i的逆元
for (int i = 1; i <= b; i++) // 除以1!, 2!, ..., n!
{
res = res * qmi(i, mod - 2, mod) % mod; // 费马小定理求逆元
}
// 除以 (n+1) 得到卡特兰数
res = res * qmi(n + 1, mod - 2, mod) % mod;
cout << res << endl;
return 0;
}
【运行结果】
3
5
浙公网安备 33010602011771号