位数区间先转成数值区间 [k^(l-1), k^r),再对数位和模 60 的分布做循环卷积快速幂,两次合法计数相减即得答案。
OJ: roj
题目 ID: 10025
难度:提高+/省选-
标签:数位DP矩阵快速幂计数递推python
创建: 2026-10-02 18:26
更新: 2026-10-07 11:01
形式化题目
给定正整数
第一步是把"位数"翻译成"数值区间":
所求就是
样例解释:
- 样例 #1(
1 5 2):区间。二进制数位和为 4 的有 15、23、27、29、30,为 5 的有 31,共 6 个。用 算: (多出的 1 个是 0), (空串对应数值 0), 。 - 样例 #2(
8 8 2):区间,最高位必为 1,剩余 7 位中放 个 1,数位和 ,即 ,共 ;等价地 。
正解
思路
朴素做法与瓶颈。 直接枚举
观察一:判定只需要数位和模 60。 因为
观察二:转移不枚举数字,只用 60 个计数。 加一位数字
(
以真实数据里的
| 余数 |
合计 | ||
|---|---|---|---|
| 16 | 15 |
每个余数先摊
即
观察三:每层转移相同且线性,用快速幂跨过
观察四:循环矩阵结构,不必写 conv + pow_dist。
小规模演进(
| 0 | 1 | 2 | 3 | 4 | 5 | 合法合计 |
|
|---|---|---|---|---|---|---|---|
| 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 |
每行是上一行与
收尾与边界。 求
代码
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++ 版(同一算法):
/**
* 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 就是系数向量 conv 实现模 60 循环卷积(观察二),pow_dist 实现卷积快速幂(观察三),count_good(step, n) 计算 solve 末行完成区间相减。
复杂度
每次循环卷积
总结
- 位数 → 数值区间:
进制 位数即 ,所求区间是 ;补零不改变数位和,答案 。 - 状态压缩:判定只看数位和模
,状态从 级的精确和压到 60。 - 转移计数:数字按
分类, ,一步转移变成模 60 循环卷积。 - 快速幂:卷积的结合律 + 倍增跨过
层;循环矩阵性质让每层只需 ,总复杂度 。
图示解析
从题意到最终答案的推导路线,每一步消除上一步的一个瓶颈:
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 压缩解决"精确和状态爆炸",余数计数解决"逐个枚举数字",卷积快速幂解决"### 思路 的四个观察即可。