题解:AcWing 889 满足条件的01序列

【题目来源】

AcWing:889 满足条件的01序列 - AcWing题库

【题目描述】

给定 \(n\)\(0\)\(n\)\(1\),它们将按照某种顺序排成长度为 \(2n\) 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 \(0\) 的个数都不少于 \(1\) 的个数的序列有多少个。

输出的答案对 \(10^9+7\) 取模。

【输入】

共一行,包含整数 \(n\)

【输出】

共一行,包含一个整数,表示答案。

【输入样例】

3

【输出样例】

5

【核心思想】

  1. 问题分析:给定 \(n\)\(0\)\(n\)\(1\),要求排成长度为 \(2n\) 的序列,且任意前缀中 \(0\) 的个数不少于 \(1\) 的个数。所有无约束的排列数为 \(C_{2n}^n\)(从 \(2n\) 个位置中选 \(n\) 个放 \(1\)),需要从中减去不合法的排列数。这是经典的卡特兰数(Catalan Number)问题。

  2. 算法选择

    • 卡特兰数公式:第 \(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\)
  3. 关键步骤

    • 总排列数:从 \(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\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log p)\)。分子连乘 \(O(n)\),分母逆元计算中每个逆元用快速幂 \(O(\log p)\),共 \(n\) 个;若递推求逆元可优化至 \(O(n)\)
    • 空间复杂度:\(O(1)\)(或 \(O(n)\) 若存储阶乘逆元数组),仅使用常数个变量
  5. 卡特兰数与反射原理的核心思想

    • 卡特兰数的组合意义:卡特兰数计数多种经典问题——合法的括号序列数、不交叉的弦划分、二叉搜索树个数、栈的合法出栈序列、网格路径不越过对角线等。本题即为"前缀中 \(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
posted @ 2026-06-23 11:24  团爸讲算法  阅读(5)  评论(0)    收藏  举报