数位 DP 维护满足条件的数字个数、一次方和与平方和,通过完全平方公式展开实现状态转移。
OJ: roj
题目 ID: 1590
难度:提高
标签:数位 DP组合数学
创建: 2026-09-30 20:26
更新: 2026-10-07 13:50
形式化题目
定义一个正整数
的十进制表示中包含数字 ; 的各位数字之和是 的整数倍(即 ); 本身是 的整数倍(即 )。
若以上三个条件均不满足,则称
给定正整数区间
正解
思路
1. 前缀转化
区间
2. 数位 DP 状态设计
由于判断一个数是否与
- 不填数字
; - 填完所有数位后的数模
不为 ; - 各位数字和模
不为 。
在从高位到低位填数的过程中,设当前位置为
3. 平方和的展开与转移
数位 DP 的核心难点在于累加的不是个数,而是平方和。
假设当前位填入数字
我们要维护后继状态返回的三个信息:
- 符合条件的数字总个数:
- 符合条件的数字一次方和:
- 符合条件的数字平方和:
根据代数展开式:
因此,递归函数不仅需要返回合法国数个数,还需要同时返回这些数的一次和与平方和。转移时利用上述展开式向上合并即可。
代码
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-07-05 21:47
# update_at: 2026-07-05 21:47
from functools import cache
import sys
MOD = 10**9 + 7
POW10 = [pow(10, i, MOD) for i in range(21)] # 10 的次幂模表
@cache
def dfs(pos: int, num_mod: int, sum_mod: int, is_limit: bool, digits: tuple[int, ...]) -> tuple[int, int, int]:
"""返回 (合法国数个数, 一次方和, 平方和) 模 MOD。"""
if pos < 0:
return (1, 0, 0) if num_mod != 0 and sum_mod != 0 else (0, 0, 0)
limit = digits[pos] if is_limit else 9
ans_cnt, ans_sum, ans_sq = 0, 0, 0
for d in range(limit + 1):
if d == 7:
continue
nxt_cnt, nxt_sum, nxt_sq = dfs(
pos - 1,
(num_mod * 10 + d) % 7,
(sum_mod + d) % 7,
is_limit and (d == limit),
digits if is_limit and (d == limit) else (),
)
base = (d * POW10[pos]) % MOD
base_sq = (base * base) % MOD
ans_cnt = (ans_cnt + nxt_cnt) % MOD
ans_sum = (ans_sum + nxt_sum + base * nxt_cnt) % MOD
ans_sq = (ans_sq + nxt_sq + 2 * base * nxt_sum + base_sq * nxt_cnt) % MOD
return (ans_cnt, ans_sum, ans_sq)
def count(n: int) -> int:
"""计算 [1, n] 内与 7 无关的数平方和。"""
if n <= 0:
return 0
digits = tuple(int(c) for c in reversed(str(n)))
return dfs(len(digits) - 1, 0, 0, True, digits)[2]
def solve() -> None:
tokens = sys.stdin.read().split()
if not tokens:
return
it = iter(tokens)
t = int(next(it))
out = [(count(int(r)) - count(int(l) - 1)) % MOD for l, r in ((next(it), next(it)) for _ in range(t))]
sys.stdout.write("\n".join(map(str, 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-05 11:15
* update_at: 2026-10-05 11:15
*/
// 数位 DP:求区间 [L, R] 内与 7 无关的数的平方和。
// 递归回溯时同时维护合法数字的个数、一次方和、平方和,
// 用完全平方公式 (base + y)^2 = base^2 + 2*base*y + y^2 逐位向上合并。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
struct State {
ll cnt; // 合法数字个数
ll sum; // 合法数字之和
ll sq; // 合法数字平方和
};
ll pow10[20]; // pow10[i] = 10^i mod MOD
int digit[20]; // digit[i] = 上界 n 从高位起的第 i 位数字
int digit_len;
State memo[20][7][7]; // memo[pos][num_mod][sum_mod]:前缀不贴上限时的合并结果
char vis[20][7][7]; // 记忆化标记,只有 0/1,用 char 省内存
// pos: 当前要填从高位起的第 pos 位
// num_mod: 已填前缀数值模 7 的余数;sum_mod: 已填前缀数位和模 7 的余数
// limit: 已填前缀是否与上界 n 完全相同(贴上限时不能查表)
State dfs(int pos, int num_mod, int sum_mod, bool limit) {
if (pos == digit_len) {
// 所有位填完:数值模 7 和数位和模 7 都不为 0 才与 7 无关
if (num_mod != 0 && sum_mod != 0) {
State ok = {1, 0, 0};
return ok;
}
State none = {0, 0, 0};
return none;
}
if (!limit && vis[pos][num_mod][sum_mod]) {
return memo[pos][num_mod][sum_mod];
}
int up = limit ? digit[pos] : 9;
State ans = {0, 0, 0};
for (int d = 0; d <= up; d++) {
if (d == 7) {
continue; // 数中出现了数字 7,整个数一定与 7 有关
}
State nxt = dfs(pos + 1, (num_mod * 10 + d) % 7, (sum_mod + d) % 7,
limit && (d == up));
// 这一位实际贡献的权值 base = d * 10^(从低位起的位置)
ll base = pow10[digit_len - 1 - pos] * d % MOD;
ll base_sq = base * base % MOD;
// 当前层拼出的完整数 x = base + y,对每个合法后缀 y 累加
ans.cnt = (ans.cnt + nxt.cnt) % MOD;
ans.sum = (ans.sum + nxt.sum + base * nxt.cnt) % MOD;
ans.sq = (ans.sq + nxt.sq + 2 * base % MOD * nxt.sum + base_sq * nxt.cnt) % MOD;
}
if (!limit) {
memo[pos][num_mod][sum_mod] = ans;
vis[pos][num_mod][sum_mod] = 1;
}
return ans;
}
// 计算 [1, n] 中与 7 无关的数字平方和 mod MOD
ll calc(ll n) {
if (n <= 0) {
return 0;
}
digit_len = 0;
ll x = n;
while (x > 0) {
digit[digit_len] = x % 10;
digit_len++;
x /= 10;
}
// 上面的 digit[] 是低位在前,翻转成高位在前
for (int i = 0; i < digit_len / 2; i++) {
int t = digit[i];
digit[i] = digit[digit_len - 1 - i];
digit[digit_len - 1 - i] = t;
}
// 每位权值依赖当前上界的长度,每次查询前清空记忆化表
memset(vis, 0, sizeof(vis));
return dfs(0, 0, 0, true).sq;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
pow10[0] = 1;
for (int i = 1; i < 20; i++) {
pow10[i] = pow10[i - 1] * 10 % MOD;
}
int t;
cin >> t;
for (int i = 0; i < t; i++) {
ll l, r;
cin >> l >> r;
ll ans = (calc(r) - calc(l - 1) + MOD) % MOD;
cout << ans << "\n";
}
return 0;
}复杂度
- 时间复杂度:对于每个数位 DP 查询,状态数为位数
。每次转移枚举 共 种可能。单次查询运算量约在 次以内,处理 组数据耗时极低,总时间复杂度为 。 - 空间复杂度:记忆化缓存大小与递归深度均为
,空间消耗仅数千状态,完全在内存限制内。
总结
本题是数位 DP 中经典的“高阶矩转移”题型。求解平方和时不能直接单独转移平方,必须同时维护零阶项(个数