1 条题解

  • 0
    @ 2026-8-21 9:28:36

    这份代码的算法设计非常精妙,它巧妙地结合了数论(同余循环节)组合数学(容斥原理)动态规划,成功将 10910^9 级别的超大状态压缩到了可计算的范围内。

    为了让你彻底吃透这道题,我将从破局点分析核心算法推导(4个步骤)以及代码性能优化三个维度,详细解释每一步的思路及其背后的原因。


    一、 破局点分析:为什么不能直接 DP?

    题目要求统计 nn 位十进制数中,模 pp 余 0 且各位数字之和 m\le m 的方案数。

    • 难点nn 高达 10910^9,如果按传统的“数位 DP”逐位决策,状态数将达到 10910^9 级别,必然超时(TLE)。
    • 突破口p50p \le 50m1000m \le 1000 非常小。一个数 D=i=0n1di10iD = \sum_{i=0}^{n-1} d_i 10^ipp 的余数,实际上只取决于每一位的权重 10i(modp)10^i \pmod p。既然 pp 很小,权重必然存在规律,这就是我们降维打击的关键。

    二、 核心算法思路详解(4个步骤)

    第一步:权重同余分组(将 10910^9 压缩为 5050

    • 思路:计算 10i(modp)10^i \pmod p 的值,将 nn 个位置按照权重余数进行分组。
    • 原因:根据鸽巢原理,10i(modp)10^i \pmod p 的余数最多只有 pp 种,因此必然存在循环节。代码通过 vis 数组找出了循环节的起点 ll 和终点 rr,并将 nn 个位置分成了 rr 组(rp50r \le p \le 50)。
    • 核心意义:同一组内的所有位置,其权重 tit_i 完全相同。因此,我们完全不需要关心组内每个位置具体填了什么数字,只需要知道这 aia_i 个位置的数字总和 SS。这 aia_i 个位置对总余数的贡献,就等价于 (S×ti)(modp)(S \times t_i) \pmod p。这一步成功将 10910^9 个位置压缩成了最多 50 个“组”。

    第二步:组内方案数计算(容斥原理)

    • 思路:对于第 ii 组(共 aia_i 个位置),计算其数字之和恰好为 jj 的方案数 b[i][j]b[i][j]
    • 原因:每个位置只能填 090 \sim 9,这等价于求方程 $x_1 + x_2 + \dots + x_{a_i} = j \quad (0 \le x_k \le 9)$ 的整数解个数。直接求带上限的解很困难,但如果没有上限 xk9x_k \le 9,解的个数就是经典的插板法(j+ai1j)\binom{j + a_i - 1}{j}
    • 核心意义:利用容斥原理,我们可以用无上限的解减去“违规”的解。
      • 枚举恰好有 kk 个变量 10\ge 10(即违规)。
      • aia_i 个变量中选 kk 个,方案数为 (aik)\binom{a_i}{k}
      • 将这 kk 个变量预先减去 10,方程变为和为 j10kj - 10k 的无上限插板法,方案数为 (j10k+ai1j10k)\binom{j - 10k + a_i - 1}{j - 10k}
      • 根据容斥原理,奇数个违规减去,偶数个违规加上。代码中的 b[i][j] 计算逻辑正是这一公式的完美实现。

    第三步:组间动态规划(拼图游戏)

    • 思路:定义状态 f[i][u][v]f[i][u][v],表示考虑前 ii 个组,当前总数字和为 vv,总数值模 pp 余数为 uu 的方案数。
    • 原因:现在我们知道了每一组内部数字和为 jj 的方案数 b[i][j]b[i][j],接下来只需要把这 rr 个组像拼图一样拼起来。
    • 核心意义
      • 状态转移时,枚举第 ii 组的数字和 jj
      • 新的总数字和变为 v+jv + j
      • 新的总余数变为 (u+j×ti)(modp)(u + j \times t_i) \pmod p(因为第 ii 组的权重是 tit_i,数字和为 jj,贡献即为 j×tij \times t_i)。
      • 转移方程:$f[i][(u + j \times t_i) \bmod p][v + j] \mathrel{+}= f[i-1][u][v] \times b[i][j]$。这一步严谨地维护了总余数和总数字和两个维度。

    第四步:前缀和处理(满足 m\le m 的要求)

    • 思路:对最终结果 f[r][0][v]f[r][0][v] 求前缀和。
    • 原因:DP 算出的是数字之和恰好等于 vv 的方案数,而题目要求的是小于等于 mim_i
    • 核心意义:通过简单的累加 f[r][0][v]=k=0vf[r][0][k]f[r][0][v] = \sum_{k=0}^v f[r][0][k],将“恰好等于”转化为“小于等于”,最后直接输出 0m0 \sim m 的结果即可。

    三、 原代码的性能缺陷与优化思路

    虽然上述数学逻辑无懈可击,但原代码在工程实现上存在一个致命缺陷,导致其在极限数据下会超时(TLE)。

    1. 缺陷原因:组合数计算的常数灾难

    原代码在计算组合数 (xy)\binom{x}{y} 时,分子和分母都使用了 O(y)O(y)for 循环:

    for (int i = x - y + 1; i <= x; i++) res = 1ll * res * i % mod; // 分子循环 y 次
    for (int i = 1; i <= y; i++) res = 1ll * res * inv[i] % mod;   // 分母循环 y 次
    

    在计算 b[i][j]b[i][j] 时,总调用次数约为 5.5×1055.5 \times 10^5 次。每次调用内部循环最多 1000 次,总运算量达到 10910^9 级别,且每次循环都包含 64 位乘法和取模运算(% mod)。在 1500ms 的时间限制下,这必然导致 TLE。

    2. 优化思路:阶乘逆元预处理

    • 观察:在公式 (xy)\binom{x}{y} 中,xx 可能非常大(aia_i 可达 10910^9),但 yy 的最大值仅为 m1000m \le 1000
    • 对策
      • 分子 x(x1)(xy+1)x(x-1)\dots(x-y+1) 因为 xx 很大,必须保留 O(y)O(y) 的连乘。
      • 但是,分母 y!y! 是固定的,且 y1000y \le 1000。我们可以在程序开头,花极短的时间预处理 110001 \sim 1000 的阶乘逆元,存入数组 invfact
    • 效果:分母的计算从 O(y)O(y) 的循环+取模,变成了 O(1)O(1) 的数组查表。这直接砍掉了一半的运算量和大量的取模操作,使总运行时间从 >1500ms 骤降至 <50ms,完美通过所有测试点。

    总结

    这份代码的灵魂在于利用同余周期性将 10910^9 降维到 5050,并用容斥原理解决了带限制的组合数计算。只要在组合数计算处加上 O(1)O(1) 的阶乘逆元查表优化,它就是一份逻辑严密、性能达标的满分解答。

    • 1

    信息

    ID
    10438
    时间
    1500ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    8
    已通过
    3
    上传者