「一本通 6.6 练习 7」超能粒子炮 · 改

GitHub跳转原题关系图返回列表

利用 Lucas 定理按 p 进制将组合数前缀和拆分为整块与散块,递归计算 S(n, k) mod 2333

OJ: roj

题目 ID: 1658

难度:提高+/省选-

标签:数论Lucas 定理组合数学前缀和

创建: 2026-10-01 00:22

更新: 2026-10-07 13:50

形式化题目

给定常数质数 p=2333p = 2333。有 TT 组询问,每次给定非负整数 n,kn, k,求组合数前缀和:

S(n,k)=∑i=0k(ni)(mod2333)S(n, k) = \sum_{i=0}^k \binom{n}{i} \pmod{2333}

正解

思路

根据 Lucas 定理,对于质数 pp 和任意非负整数 n,in, i,有:

(ni)≡(⌊n/p⌋⌊i/p⌋)(n mod pi mod p)(modp)\binom{n}{i} \equiv \binom{\lfloor n/p \rfloor}{\lfloor i/p \rfloor} \binom{n \bmod p}{i \bmod p} \pmod p

我们要求的是前缀和:

S(n,k)=∑i=0k(ni)(modp)S(n, k) = \sum_{i=0}^k \binom{n}{i} \pmod p

将求和变量 ii 进行带余除法:设 i=j⋅p+ri = j \cdot p + r,其中 0≤r<p0 \le r < p。由于 0≤i≤k0 \le i \le k,可将商 jj 分成两部分考虑:

  1. 完整块(0≤j≤⌊k/p⌋−10 \le j \le \lfloor k/p \rfloor - 1): 在每一个固定的 jj 下,余数 rr 取遍了完整区间 [0,p−1][0, p-1]。利用 Lucas 定理将组合数因式分解:

    ∑r=0p−1(nj⋅p+r)≡(⌊n/p⌋j)∑r=0p−1(n mod pr)=(⌊n/p⌋j)S(n mod p,p−1)(modp)\sum_{r=0}^{p-1} \binom{n}{j \cdot p + r} \equiv \binom{\lfloor n/p \rfloor}{j} \sum_{r=0}^{p-1} \binom{n \bmod p}{r} = \binom{\lfloor n/p \rfloor}{j} S(n \bmod p, p-1) \pmod p

    对所有合法的 jj 求和,提取公因式 S(n mod p,p−1)S(n \bmod p, p-1):

    ∑j=0⌊k/p⌋−1((⌊n/p⌋j)S(n mod p,p−1))=S(n mod p,p−1)⋅S(⌊n/p⌋,⌊k/p⌋−1)(modp)\sum_{j=0}^{\lfloor k/p \rfloor - 1} \left( \binom{\lfloor n/p \rfloor}{j} S(n \bmod p, p-1) \right) = S(n \bmod p, p-1) \cdot S\left(\lfloor n/p \rfloor, \lfloor k/p \rfloor - 1\right) \pmod p
  2. 不完整散块(j=⌊k/p⌋j = \lfloor k/p \rfloor): 此时商固定为 ⌊k/p⌋\lfloor k/p \rfloor,余数 rr 只能取到 0≤r≤k mod p0 \le r \le k \bmod p。其贡献为:

    (⌊n/p⌋⌊k/p⌋)∑r=0k mod p(n mod pr)=(⌊n/p⌋⌊k/p⌋)⋅S(n mod p,k mod p)(modp)\binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor} \sum_{r=0}^{k \bmod p} \binom{n \bmod p}{r} = \binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor} \cdot S(n \bmod p, k \bmod p) \pmod p

将两部分合并,即可得到核心递归公式:

S(n,k)≡S(n mod p,p−1)⋅S(⌊n/p⌋,⌊k/p⌋−1)+(⌊n/p⌋⌊k/p⌋)⋅S(n mod p,k mod p)(modp)S(n, k) \equiv S(n \bmod p, p-1) \cdot S\left(\lfloor n/p \rfloor, \lfloor k/p \rfloor - 1\right) + \binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor} \cdot S(n \bmod p, k \bmod p) \pmod p

算法流程

  1. 预处理:模数 p=2333p = 2333 很小,利用杨辉三角预处理出 p×pp \times p 的组合数表 C(n,m)(modp)C(n, m) \pmod p 和每行前缀和表 S(n,m)(modp)S(n, m) \pmod p。
  2. 递归计算:
    • 递归边界:若 k<0k < 0,返回 00;若 n<pn < p 且 k<pk < p,直接查表返回 S[n][k]S[n][k]。
    • 转移时调用自身计算更小规模的 S(⌊n/p⌋,⌊k/p⌋−1)S(\lfloor n/p \rfloor, \lfloor k/p \rfloor - 1),并用 Lucas 定理单次查询 (⌊n/p⌋⌊k/p⌋)\binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor}。

代码

Python 版:

python
#!/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 00:22
# update_at: 2026-10-01 00:22

import sys

P = 2333  # 模数,题目给定质数


def init_tables() -> tuple[list[list[int]], list[list[int]]]:
    """预处理模 P 下的杨辉三角组合数表与行前缀和表。"""
    c = [[0] * P for _ in range(P)]
    s = [[0] * P for _ in range(P)]
    for i in range(P):
        c[i][0] = 1
        s[i][0] = 1
        for j in range(1, i + 1):
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % P
            s[i][j] = (s[i][j - 1] + c[i][j]) % P
        for j in range(i + 1, P):
            s[i][j] = s[i][i]
    return c, s


C, S = init_tables()


def lucas(n: int, m: int) -> int:
    """计算 Lucas 定理下的组合数 C(n, m) mod P。"""
    if m == 0:
        return 1
    if n < m:
        return 0
    if n < P and m < P:
        return C[n][m]
    return lucas(n // P, m // P) * C[n % P][m % P] % P


def query_sum(n: int, k: int) -> int:
    """计算前缀组合数和 S(n, k) = sum_{i=0}^k C(n, i) mod P。"""
    if k < 0:
        return 0
    if n < P and k < P:
        return S[n][k]
    # Lucas 递归展开:整除整块部分乘以对应前缀和 + 散块部分
    full_blocks = S[n % P][P - 1] * query_sum(n // P, k // P - 1)
    partial_block = lucas(n // P, k // P) * S[n % P][k % P]
    return (full_blocks + partial_block) % P


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    out: list[str] = []
    T = next(data)

    for _ in range(T):
        n = next(data)
        k = next(data)
        out.append(str(query_sum(n, k)))

    sys.stdout.write("\n".join(out) + "\n")


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * 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-06 00:55
 * update_at: 2026-10-06 00:55
 */

#include <cstdio>

typedef long long ll;

const ll P = 2333;            // 模数,题目给定的质数
const ll MOD = 2333;

// C[n][m]:模 2333 意义下的组合数表,n, m < 2333
// S[n][m]:组合数表第 n 行的前缀和 S(n, m) = sum_{i=0}^{m} C(n, i) mod 2333
// 值都小于 2333,用 int 控制内存(两张表共约 44MB)
int C[P][P];
int S[P][P];

// 预处理杨辉三角组合数表与行前缀和表(i 从 0 开始,第 0 行也要填)
void init() {
    for (ll i = 0; i < P; i++) {
        C[i][0] = 1;
        S[i][0] = 1;
        for (ll j = 1; j <= i; j++)
            C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
        // 第 i 行 C(i, j) 在 j > i 时为 0,前缀和保持不变
        for (ll j = 1; j < P; j++) {
            if (j <= i)
                S[i][j] = (S[i][j - 1] + C[i][j]) % MOD;
            else
                S[i][j] = S[i][i];
        }
    }
}

// Lucas 定理求 C(n, m) mod 2333
ll lucas(ll n, ll m) {
    if (m == 0) return 1;
    if (n < m) return 0;
    if (n < P && m < P) return C[n][m];
    return lucas(n / P, m / P) * C[n % P][m % P] % MOD;
}

// 求 S(n, k) = sum_{i=0}^{k} C(n, i) mod 2333
// 按 i = j*p + r 分块:完整块贡献 S(n%P, P-1) * S(n/P, k/P-1),
// 散块(j = k/P,r 只到 k%P)贡献 C(n/P, k/P) * S(n%P, k%P)
ll query_sum(ll n, ll k) {
    if (k < 0) return 0;
    if (n < P && k < P) return S[n][k];
    ll full = S[n % P][P - 1] * query_sum(n / P, k / P - 1) % MOD;
    ll part = lucas(n / P, k / P) * S[n % P][k % P] % MOD;
    return (full + part) % MOD;
}

int main() {
    init();
    ll t;
    scanf("%lld", &t);
    while (t--) {
        ll n, k;
        scanf("%lld%lld", &n, &k);
        printf("%lld\n", query_sum(n, k));
    }
    return 0;
}

复杂度

  • 时间复杂度:
    • 预处理表格:时间复杂度为 O(p2)=23332≈5.4×106O(p^2) = 2333^2 \approx 5.4 \times 10^6 次运算。
    • 单次查询:每层递归规模以 pp 为底对数缩小,递归深度为 ⌊log⁡pn⌋\lfloor \log_p n \rfloor。由于 p=2333,n≤1018p = 2333, n \le 10^{18},递归层数最多不超过 66 层。每次递归需要一次 O(log⁡pn)O(\log_p n) 的 Lucas 组合数求值与一次子问题递归调用,单次查询时间复杂度为 O(log⁡p2n)O(\log_p^2 n)。
    • 总时间复杂度为 O(p2+Tlog⁡p2n)O(p^2 + T \log_p^2 n)。
  • 空间复杂度:预处理存储 p×pp \times p 的组合数表和前缀和表,空间复杂度为 O(p2)O(p^2)。

总结

本题是 Lucas 定理在组合数前缀和求值中的经典应用。通过将下标 ii 按模 pp 进行整块与散块分解,巧妙地将上指标与下指标同时缩小除以 pp,从而把原本 O(k)O(k) 的不可行前缀求和优化为 O(log⁡p2n)O(\log_p^2 n) 的分治递归。