数位DP

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

位数区间先转成数值区间 [k^(l-1), k^r),再对数位和模 60 的分布做循环卷积快速幂,两次合法计数相减即得答案。

OJ: roj

题目 ID: 10025

难度:提高+/省选-

标签:数位DP矩阵快速幂计数递推python

创建: 2026-10-02 18:26

更新: 2026-10-07 11:01

形式化题目

给定正整数 l,r,kl, r, k(1⩽l⩽r1 \leqslant l \leqslant r),统计 kk 进制下位数在 [l,r][l,r] 之间、且数位和能被 4 或 5 或 6 整除的整数个数,答案模 109+710^9+7。题面里的"幸运数字 546 的倍数"只是背景故事,与所求无关。

第一步是把"位数"翻译成"数值区间":kk 进制下 nn 位数的取值范围是 [kn−1,kn)[k^{n-1}, k^n)(n⩾1n \geqslant 1,0 不算 1 位数),于是位数在 [l,r][l,r] 的数就是数值落在 [kl−1,kr)[k^{l-1}, k^r) 中的数。再注意到高位补零不改变数位和:把每个数补零到固定长度后,"数值在 [0,kn)[0,k^n) 内"与"长度恰为 nn 的数字串(允许前导零)"一一对应。记

G(n)=#{x∈[0,kn):x 的数位和被 4 或 5 或 6 整除}=长度 n 合法串的个数,G(n) = \#\{x \in [0,k^n) : x \text{ 的数位和被 } 4 \text{ 或 } 5 \text{ 或 } 6 \text{ 整除}\} = \text{长度 } n \text{ 合法串的个数},

所求就是 G(r)−G(l−1)G(r) - G(l-1):大区间 [0,kr)[0,k^r) 减去前缀 [0,kl−1)[0,k^{l-1})。

样例解释:

  • 样例 #1(1 5 2):区间 [1,32)[1,32)。二进制数位和为 4 的有 15、23、27、29、30,为 5 的有 31,共 6 个。用 GG 算:G(5)=7G(5) = 7(多出的 1 个是 0),G(0)=1G(0) = 1(空串对应数值 0),7−1=67 - 1 = 6。
  • 样例 #2(8 8 2):区间 [128,256)[128,256),最高位必为 1,剩余 7 位中放 mm 个 1,数位和 1+m∈{4,5,6,8}1+m \in \{4,5,6,8\},即 m∈{3,4,5,7}m \in \{3,4,5,7\},共 (73)+(74)+(75)+(77)=35+35+21+1=92\binom{7}{3}+\binom{7}{4}+\binom{7}{5}+\binom{7}{7} = 35+35+21+1 = 92;等价地 G(8)−G(7)=156−64=92G(8)-G(7) = 156-64 = 92。

正解

思路

朴素做法与瓶颈。 直接枚举 [kl−1,kr)[k^{l-1}, k^r) 里的每个数求各位和:区间里有 kr−kl−1k^r - k^{l-1} 个数,kk、rr 都到 101810^{18},完全不可行。退一步改成逐位 DP:记 f[n][s]=f[n][s] = 前 nn 位、数位和恰为 ss 的方案数,转移要枚举下一位的每个数字 d∈[0,k)d \in [0,k),即 f[n+1][s+d]+=f[n][s]f[n+1][s+d] \mathrel{+}= f[n][s]。三个维度同时爆炸:层数 nn 到 101810^{18},单层转移要枚举 kk 到 101810^{18} 个数字,ss 的上界 n(k−1)n(k-1) 更是天文数字。下面依次砍掉这三个维度。

观察一:判定只需要数位和模 60。 因为 4,5,64,5,6 都整除 60=lcm(4,5,6)60 = \mathrm{lcm}(4,5,6),所以"被 4/5/6 之一整除"完全由 s mod 60s \bmod 60 决定。合法余数集合 S={i:i%4=0∨i%5=0∨i%6=0}S = \{i : i\%4=0 \lor i\%5=0 \lor i\%6=0\},用容斥算出 15+12+10−3−5−2+1=2815+12+10-3-5-2+1 = 28 个。于是把状态从"精确数位和"压成"数位和模 60",只剩 60 个状态:记 distn[i]=\mathrm{dist}_n[i] = 长度 nn、数位和 ≡i(mod60)\equiv i \pmod{60} 的串数,初值 dist0=[1,0,…,0]\mathrm{dist}_0 = [1,0,\dots,0](空串)。

观察二:转移不枚举数字,只用 60 个计数。 加一位数字 dd 后状态从 ii 走到 (i+d) mod 60(i+d) \bmod 60,作用只取决于 d mod 60d \bmod 60,所以按余数分类计数:

ct=#{d∈[0,k):d≡t(mod60)}=⌊k/60⌋+[t<k%60],0⩽t<60.c_t = \#\{d \in [0,k) : d \equiv t \pmod{60}\} = \lfloor k/60 \rfloor + [t < k \% 60], \quad 0 \leqslant t < 60.

([⋅][\cdot] 是 Iverson 括号:条件成立取 1,否则取 0。)

以真实数据里的 k=906k = 906(906=60×15+6906 = 60 \times 15 + 6)为例,看这 60 个计数从哪来:

余数 tt 0∼50 \sim 5 6∼596 \sim 59 合计
ctc_t 16 15 6×16+54×15=906=k6 \times 16 + 54 \times 15 = 906 = k ✓

每个余数先摊 ⌊k/60⌋\lfloor k/60 \rfloor 轮完整周期(每轮 60 个数各贡献一次),前 k%60k \% 60 个余数再多一个。转移于是变成只含这 60 个系数的式子:

distn+1[i′]=∑i=059distn[i]⋅c(i′−i) mod 60,\mathrm{dist}_{n+1}[i'] = \sum_{i=0}^{59} \mathrm{dist}_n[i] \cdot c_{(i'-i) \bmod 60},

即 distn+1=distn⊛c\mathrm{dist}_{n+1} = \mathrm{dist}_n \circledast c,是模 60 的循环卷积:单层只要 O(602)O(60^2) 次乘加,且与 kk 的大小无关——观察二同时消掉了"枚举 kk 个数字"这个维度。

观察三:每层转移相同且线性,用快速幂跨过 101810^{18} 层。 连续加 nn 位等价于 distn=c⊛n\mathrm{dist}_n = c^{\circledast n}(cc 自己卷 nn 次)。卷积满足结合律、单位元是 δ0\delta_0,所以用倍增求:层数不断平方、按 nn 的二进制位把层乘进结果,只需 O(log⁡n)O(\log n) 层——"层数到 101810^{18}"这个维度被对数消掉。

观察四:循环矩阵结构,不必写 60×6060\times60 矩阵乘法。 上述递推等价于矩阵 A[i′][i]=c(i′−i) mod 60A[i'][i] = c_{(i'-i) \bmod 60} 的 nn 次幂作用在 dist0\mathrm{dist}_0 上。AA 的元素只依赖下标之差,是循环矩阵,而 AnA^n 的第 0 列恰好等于 c⊛nc^{\circledast n}。所以标准的 60×6060\times60 矩阵快速幂(每层 O(603)O(60^3))可以直接换成 60 维向量的循环卷积快速幂(每层 O(602)O(60^2))——这就是代码里的 conv + pow_dist。

小规模演进(k=2k=2,样例 #1)。 此时 c=[1,1,0,…,0]c = [1,1,0,\dots,0],且数位和最大为 5<605 < 60,取模不改变数值,分布恰好是二项式系数:

nn \ 余数 ii 0 1 2 3 4 5 合法合计 G(n)G(n)
0 1 0 0 0 0 0 1
1 1 1 0 0 0 0 1
2 1 2 1 0 0 0 1
3 1 3 3 1 0 0 1
4 1 4 6 4 1 0 2
5 1 5 10 10 5 1 7

每行是上一行与 cc 做循环卷积的结果(这里 cc 只有 t=0,1t=0,1 两项为 1,即每个数往自己和"自己加 1"两个方向各转移一次)。合法列只统计命中 SS 的余数 0,4,50,4,5(1,2,3∉S1,2,3 \notin S)。最后一行 G(5)=1+5+1=7G(5) = 1+5+1 = 7,答案 G(5)−G(0)=7−1=6G(5)-G(0) = 7-1 = 6,与样例 #1 一致。

收尾与边界。 求 G(r)G(r)、G(l−1)G(l-1) 各做一次卷积快速幂,输出 (G(r)−G(l−1)) mod 109+7(G(r) - G(l-1)) \bmod 10^9+7。l=1l = 1 时 G(0)=1G(0) = 1 正好把数值 0 排除(0 不是 1 位数,样例 #1 已验证题面口径);k<60k < 60 时 ⌊k/60⌋=0\lfloor k/60 \rfloor = 0、ct=[t<k]c_t = [t<k];kk 是 60 的倍数时 k%60=0k\%60=0、没有 +1+1 分支——公式在这些边界都自然成立。

代码

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-02 19:05
# update_at: 2026-10-02 19:05

import sys

MOD = 10**9 + 7
M = 60  # lcm(4,5,6):数位和只要模 60,就能判断是否被 4/5/6 中某个数整除


def conv(a: list[int], b: list[int]) -> list[int]:
    """两个「数位和模 60 分布」的循环卷积:先累加、最后一次取模。"""
    acc = [0] * M
    for i, x in enumerate(a):
        if not x:
            continue  # 空档跳过,省掉一半内层循环
        for j, y in enumerate(b):
            if y:
                acc[(i + j) % M] += x * y
    return [v % MOD for v in acc]


def pow_dist(base: list[int], n: int) -> list[int]:
    """初始分布 base 做 n 次循环卷积(快速幂):恰好 n 位的数位和模 60 分布。"""
    res = [1] + [0] * (M - 1)  # 单位元:0 位、数位和为 0
    while n:
        if n & 1:
            res = conv(res, base)
        base = conv(base, base)
        n >>= 1
    return res


def count_good(step: list[int], n: int) -> int:
    """恰好 n 位(允许前导零)的 k 进制数里,数位和被 4/5/6 整除的个数。"""
    dist = pow_dist(step, n)
    return sum(v for i, v in enumerate(dist) if i % 4 == 0 or i % 5 == 0 or i % 6 == 0) % MOD


def solve() -> None:
    l, r, k = map(int, sys.stdin.buffer.read().split())

    # 一位数字 d(0 <= d < k)贡献的数位和增量 d mod 60:每个余数先摊 k//60 个,
    # 余下 k%60 个(即 0..k%60-1 这些余数)各再多一个。
    quota, extra = divmod(k, M)
    step = [quota + (1 if i < extra else 0) for i in range(M)]  # 60 项

    # l 位数到 r 位数 = 数值落在 [k^(l-1), k^r) 的数;高位补零到 r 位后数位和不变,
    # 于是答案 = 恰好 r 位的合法个数 - 恰好 l-1 位的合法个数(被减掉的是更短前缀)。
    print((count_good(step, r) - count_good(step, l - 1)) % MOD)


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-04 23:13
 * update_at: 2026-10-04 23:13
 */
// main.cpp:k 进制 l~r 位数中数位和被 4/5/6 整除的数的个数(模 1e9+7)。
// 思路:答案 = G(r) - G(l-1),G(n) 为 [0,k^n) 中合法数的个数;
// 判定只看数位和模 lcm(4,5,6)=60,一步转移是模 60 的循环卷积,
// 层数到 1e18,用卷积快速幂(循环矩阵性质,每层 O(60^2))跨过去。

#include <cstdio>

typedef long long ll;

const ll MOD = 1000000007LL;
const int M = 60; // lcm(4,5,6),数位和模 60 即可判定合法性

// 计数器按数位和模 60 的余数保存分布,固定 60 项,全局数组
ll dist[M];       // 当前分布:dist[i] = 数位和 ≡ i (mod 60) 的串数
ll base_dist[M];  // 快速幂中不断平方的底向量
ll result_dist[M]; // 快速幂累乘的结果向量
ll tmp_dist[M];   // 卷积结果的临时数组

ll l, r, k; // 输入:位数区间 [l,r],进制 k(都可能到 1e18)

// 两个「数位和模 60 分布」做循环卷积,结果存到 out
void conv(ll a[], ll b[], ll out[]) {
    for (int i = 0; i < M; ++i) out[i] = 0;
    for (int i = 0; i < M; ++i) {
        if (a[i] == 0) continue; // 空档跳过,省内层循环
        for (int j = 0; j < M; ++j) {
            if (b[j] == 0) continue;
            int pos = i + j;
            if (pos >= M) pos -= M; // 模 60,用减法代替取模更快
            out[pos] = (out[pos] + a[i] * b[j]) % MOD;
        }
    }
}

// 初始分布 src 自卷 n 次(快速幂),结果存回 src
void pow_dist(ll src[], ll n) {
    // 结果向量置为单位元:0 位、数位和为 0
    for (int i = 0; i < M; ++i) result_dist[i] = 0;
    result_dist[0] = 1;
    for (int i = 0; i < M; ++i) base_dist[i] = src[i];
    while (n > 0) {
        if (n & 1) {
            conv(result_dist, base_dist, tmp_dist);
            for (int i = 0; i < M; ++i) result_dist[i] = tmp_dist[i];
        }
        conv(base_dist, base_dist, tmp_dist);
        for (int i = 0; i < M; ++i) base_dist[i] = tmp_dist[i];
        n >>= 1;
    }
    for (int i = 0; i < M; ++i) src[i] = result_dist[i];
}

// 恰好 n 位(允许前导零)的 k 进制数里,数位和被 4/5/6 整除的个数
ll count_good(ll n) {
    // 生成一位数字的余数分布 step:每个数字 d (0<=d<k) 贡献 d mod 60,
    // 每个余数先摊 k/60 个,余下前 k%60 个余数各再多一个
    ll step[M];
    ll quota = k / M, extra = k % M;
    for (int i = 0; i < M; ++i) step[i] = quota + (i < extra ? 1 : 0);
    pow_dist(step, n);
    // 合法余数集合:被 4 或 5 或 6 整除的 i (0<=i<60),共 28 个
    ll sum = 0;
    for (int i = 0; i < M; ++i)
        if (i % 4 == 0 || i % 5 == 0 || i % 6 == 0)
            sum = (sum + step[i]) % MOD;
    return sum;
}

int main() {
    scanf("%lld %lld %lld", &l, &r, &k);
    // l 位数到 r 位数 = 数值在 [k^(l-1), k^r);高位补零不改数位和,
    // 答案 = 恰好 r 位的合法个数 - 恰好 l-1 位的合法个数(l=1 时减去数值 0)
    ll ans = (count_good(r) - count_good(l - 1) % MOD + MOD) % MOD;
    printf("%lld\n", ans);
    return 0;
}

正文与代码的对应:step 就是系数向量 cc(60 项推导式),conv 实现模 60 循环卷积(观察二),pow_dist 实现卷积快速幂(观察三),count_good(step, n) 计算 G(n)=∑i∈Sdistn[i]G(n) = \sum_{i \in S} \mathrm{dist}_n[i](观察一),solve 末行完成区间相减。

复杂度

每次循环卷积 602=360060^2 = 3600 次乘加,快速幂 ⌈log⁡2r⌉⩽60\lceil \log_2 r \rceil \leqslant 60 层,求 G(r)G(r)、G(l−1)G(l-1) 各一次:总时间 O(602log⁡r)O(60^2 \log r),空间 O(60)O(60)。与正解的 60×6060\times60 矩阵快速幂 O(603log⁡r)O(60^3 \log r) 同属 log⁡\log 层的线性递推快速幂,这里借助循环矩阵结构把每层压到平方级,复杂度严格不弱于正解。实测 10 组真实数据每例 0.016–0.052s、峰值约 15MB,对 OJ 的 1s / 128MB 限制余量很大,Python 侧没有 TLE/MLE 风险。

总结

  • 位数 → 数值区间:kk 进制 nn 位数即 [kn−1,kn)[k^{n-1}, k^n),所求区间是 [kl−1,kr)[k^{l-1}, k^r);补零不改变数位和,答案 =G(r)−G(l−1)= G(r) - G(l-1)。
  • 状态压缩:判定只看数位和模 lcm(4,5,6)=60\mathrm{lcm}(4,5,6) = 60,状态从 n(k−1)n(k-1) 级的精确和压到 60。
  • 转移计数:数字按 d mod 60d \bmod 60 分类,ct=⌊k/60⌋+[t<k%60]c_t = \lfloor k/60\rfloor + [t < k\%60],一步转移变成模 60 循环卷积。
  • 快速幂:卷积的结合律 + 倍增跨过 101810^{18} 层;循环矩阵性质让每层只需 O(602)O(60^2),总复杂度 O(602log⁡r)O(60^2 \log r)。

图示解析

从题意到最终答案的推导路线,每一步消除上一步的一个瓶颈:

flowchart TD
    A["题意:k 进制 l~r 位中数位和被 4/5/6 整除的数"] --> B["区间转化:数值 in [k^(l-1), k^r)<br/>答案 = G(r) - G(l-1)"]
    B --> C["状态压缩:只需数位和 mod 60<br/>合法余数 28 个"]
    C --> D["转移计数:按 d mod 60 分类<br/>c_t = k//60 + [t < k%60]"]
    D --> E["线性递推:每层都是同一个循环卷积<br/>模 60 卷积,每层 O(60^2)"]
    E --> F["层数 1e18:卷积快速幂 O(60^2 log n)"]
    F --> G["G(n) = 合法余数分布求和<br/>输出 (G(r) - G(l-1)) mod 1e9+7"]

每个节点对应正文的一次瓶颈消除:区间转化解决"枚举数值不可行",模 60 压缩解决"精确和状态爆炸",余数计数解决"逐个枚举数字",卷积快速幂解决"101810^{18} 层递推"。读图时按箭头顺序对照 ### 思路 的四个观察即可。