组合

AT_abc425_e [ABC425E] Count Sequences 2 - 洛谷

核心结论:

模数是素数且需要高效计算大量组合数时,用逆元公式更优;模数非素数、组合数范围小,或只需计算部分组合数时,杨辉三角递推更稳妥。
一、用逆元公式更优的场景
逆元公式的核心是通过预处理阶乘和逆阶乘,实现 \(\binom{M}{k}\) 的 O(1) 查询,适合“高效+大范围”场景:
1.模数是素数(或能保证分母与模数互质):这是逆元存在的前提,比如常见的模数 1e9+7、998244353。
2.组合数的范围大(比如 M 可达 1e6 甚至 1e7):杨辉三角是 O(M²) 预处理,M 超过 1e4 就会超时/超内存,而逆元是 O(M) 预处理,完全适配大范围。
3.需要频繁查询不同的 \(\binom{M}{k}\):预处理后每次查询只需 3 次乘法+取模,比杨辉三角的递推更高效。
4.典型场景:ACM 中的组合数学题、计数 DP 题(如求路径数、子集数),且模数是固定素数。
二、用杨辉三角递推更优的场景
杨辉三角的核心是“无依赖+纯加法”,适合逆元用不了或范围小的场景:
1.模数非素数(或分母与模数不互质):逆元不存在,只能用递推(比如你之前的题目,模数 m 是任意给定的,可能不是素数)。
2.组合数的范围小(比如 M 不超过 5e3 或 1e4):O(M²) 预处理的时间和内存完全可控(比如 M=5e3 时,预处理数组是 5e3×5e3=2.5e7 个元素,不超内存)。
3.只需计算部分组合数,但范围集中:比如只需要 \(\binom{1000}{k}\)(k 从 1 到 1000),递推直接覆盖,无需额外预处理。
4.编程实现简单:无需理解逆元、费马小定理等概念,直接用递推式编写,不易出错。
三、关键对比(核心差异) - 时间复杂度:逆元 O(M) 预处理 + O(1) 查询,杨辉三角 O(M²) 预处理 + O(1) 查询。 - 空间复杂度:逆元 O(M)(存阶乘和逆阶乘),杨辉三角 O(M²)(存二维组合数表)。 - 适用模数:逆元仅适用于素数模数(或分母与模数互质),杨辉三角无限制。

递推式的核心逻辑:

分类讨论 “选不选某一个特定元素” 我们任选 \(i\) 个元素中的一个固定元素(比如叫 “元素 I”),所有选 \(j\) 个元素的方案,都能分成两类,且这两类方案没有重叠、覆盖所有可能:

  1. **不选元素 I*:要从剩下的 \(i-1\) 个元素中选 \(j\) 个,方案数就是 \(\binom{i-1}{j}\)。
  2. 选元素 I:已经选了 1 个(元素 A),还需要从剩下的 \(i-1\) 个元素中选 \(j-1\) 个,方案数就是 \(\binom{i-1}{j-1}\)。
    三、两类方案相加得到总方案数 根据 “分类加法计数原理”,总方案数等于两类方案数之和,即: \(\binom{i}{j} = \binom{i-1}{j} + \binom{i-1}{j-1}\)
//组合数,取模不是素数不能用逆元
//直接用递推组合数
const int N = 5e3 + 5;
int mod, f[N][N];
void solve(){
  int t; cin >> t >> mod;
    f[0][0] = 1;
    per(i, 1, N - 4)
      per(j, 0, i) f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % mod;
  while(t -- ){
    int n, m = 0, ans = 1; cin >> n; vi c(n + 1);
    per(i, 1, n)cin >> c[i], m += c[i];
    per(i, 1, n){
      ans = ans * f[m][c[i]] % mod;
      m -= c[i];
      if(!m)break;
    }
    cout << ans << endl;
  }
}

1. Problem - D - Codeforces

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define pii pair<int, int>
#define vi vector<int>
#define vii vector<vi>
#define lowbit(x) (x & (-x))
const int N = 1e5 + 7, mod = 1e9 + 7;
int qmi(int a, int b){
    int res = 1;
    while(b){
        if(b & 1)res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res;
}
int csum(int n, int a){
    int res = 1;
    for(int i = 1; i <= a; ++i){
        res = res * (n-i+1) % mod;
        res = res * qmi(i, mod-2) % mod;
    }
    return res;
}
void solve(){
    int a, b, k; cin >> a >> b >> k;
    int n0 = (k * (a - 1) % mod + 1) % mod;
    int m0 =k*csum(n0, a)%mod*(b-1)% mod + 1;
    cout << n0 << " " << m0%mod << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    cin >> T;
    while(T--) solve();
    return 0;
}

E - Fruit Lineup

快速幂 + 逆元 + 预处理

记 N=A+B+C+D 表示水果总数

可以发现位置关系一定是:

|----A----|     |--D--|
|-------B------|
           |-----C----|

要计算方案数,很明显要枚举水果边界的位置,由于苹果约束最多,所以考虑枚举苹果的边界,即最靠右的苹果,记其位置为 i,那么所有苹果处于 [1,i],因为钦定第 i 个位置必放,所以放苹果方案数为 $$\binom{i-1}{A-1}$$。并且由于香蕉都处于苹果右边,所以香蕉的位置处于 [i+1,N],往里面填 C 个香蕉,方案数是 $$\binom{N-1}{C}$$(CN−i​)。此时我们发现,苹果和香蕉已经选好了,而由于橙子和葡萄的位置关系确定,所以只能将其按顺序插到剩余的空位里,所以只要确定苹果和香蕉就确定整个顺序,于是方案数就是 $$\sum_{i=A}^{\min(A+B, N-C)} \binom{i-1}{A-1} \times \binom{N - i}{C}$$。然后考虑 i 的范围,首先由于苹果要放得下所以 i≥A,并且前面的空位要橙子来填补,只有 B 个橙子,所以最多空 B 个位,i≤A+B,而且右边的香蕉要能够放下,所以 i≤N−C+1,综上就是 A≤i≤min(A+B,N−C+1),所以答案就是:

\[\sum_{i=A}^{\min(A+B, N-C+1)} \binom{i-1}{A-1} \times \binom{N - i}{C} \]

(指从A - 1中挑选i- 1个, N - i 中挑选C个)
时间复杂度 O(N)。
我们知道阶乘的性质:(i+1)! = (i+1) × i!
两边同时取模 MOD 的逆元(逆元的本质是 a × a⁻¹ ≡ 1 (mod MOD)),根据逆元的乘法性质 (a×b)⁻¹ ≡ a⁻¹ × b⁻¹ (mod MOD),可得:
[(i+1)!]⁻¹ ≡ (i+1)⁻¹ × (i!)⁻¹ (mod MOD)
整理后得到逆阶乘的递推关系:
(i!)⁻¹ ≡ (i+1) × [(i+1)!]⁻¹ (mod MOD)
用代码中的变量表示就是:
invf[i] = (i+1) × invf[i+1] % MOD

为什么这能避免快速幂?

  1. 快速幂的作用:仅用于计算最大的逆阶乘 invf[n] = (n!)⁻¹(通过 qmi(fact[n], MOD-2) 计算,只需要一次)。
  2. 递推的作用:有了 invf[n] 后,根据上述公式,invf[n-1] 可以由 invf[n] × n 推导而来,invf[n-2] 可以由 invf[n-1] × (n-1) 推导而来,以此类推。
  3. 时间复杂度差异:
    • 每次调用快速幂的时间复杂度是 O(log mod),如果正向计算 n 个逆元,总时间是 O(n * log mod)。
    • 逆推法只需一次快速幂,后续都是 O(1) 的递推,总时间是 O(n + log MOD),效率大幅提升。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define pii pair<int, int>
#define vi vector<int>
#define vii vector<vi>
#define lowbit(x) (x & (-x))
const int mod = 998244353;
const int N = 4e6;
int f[N], invf[N];
int qmi(int a, int b){
    int res = 1;
    while(b){
        if(b & 1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res;
}
void init(int n){
    f[0] = 1;
    for(int i = 1; i <= n; ++i)
        f[i] = f[i - 1] * i % mod;
    invf[n] = qmi(f[n], mod - 2);//逆着推,只需调用一次快速幂
    for(int i = n - 1; i >= 0; --i) invf[i] = invf[i + 1] * (i + 1) % mod;
}
int C(int n, int k){
    if(k < 0 || k > n)return 0;
    return f[n] * invf[k] % mod * invf[n - k] % mod;
}
// int C(int n, int k){
//     int res = 1;
//     for(int i = 1; i <= k; ++ i){
//         res = res * (n - i + 1) % mod;
//         res = res * qmi(i, mod - 2);
//     }
//     return res;
// } //会超时要预处理
void solve(){
    int a, b, c, d, ans = 0;;
    cin >> a >> b >> c >> d;
    int n = a + b + c + d;//加起来可能达到4e6
    init(n);
    for(int i = a; i <= min(a + b, n - c); ++i){
        ans = (ans + C(i - 1, a - 1) * C(n - i, c) % mod) % mod;
    }
    cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}
posted @ 2025-12-03 21:07  Glosie  阅读(31)  评论(0)    收藏  举报