1. 问题分析
我们需要统计在一个区间 [l, r]
内所有满足以下条件的数的平方和之和:
- 该数的每一位数字不能为
7
。 - 该数的数位之和模
7
不为0
。 - 该数自身模
7
的余数不为0
。
具体来说,要求得出符合条件的数字的平方和的和,即在 [l, r]
范围内所有符合条件的数字的平方和累加值。
2. 算法设计与实现
使用 数位动态规划 方法解决这个问题。数位 DP 的主要思想是逐位处理数,并根据特定条件进行状态转移。
关键变量与结构
a[N]
:用于存储数x
的每一位,便于按位递归。f[N][N][N]
:定义的三维 DP 数组,用于存储中间结果,避免重复计算。其中:f[pos][val][sum]
表示从高到低枚举到第pos
位时,当前数模7
的余数为val
,数位和模7
的余数为sum
的状态。- 其中
F
结构体f[pos][val][sum]
包含了三个变量:s0
、s1
、s2
,分别表示符合条件的数字个数、这些数字的一次幂和、平方和。
pow10[N]
:存储10
的幂次,以计算每一位对当前数值的实际贡献。
结构体 F
struct F
{
LL s0, s1, s2;
F(): s0(0ll), s1(0ll), s2(0ll){}; //无参构造函数
F(LL _0, LL _1, LL _2): s0(_0), s1(_1), s2(_2){}; //含参构造函数
void operator += (const F& t) //重载+=,用于信息合并
{
s2 = (s2 + t.s2) % MOD;
s1 = (s1 + t.s1) % MOD;
s0 = (s0 + t.s0) % MOD;
}
} f[N][N][N];
s0
:符合条件的数字个数。s1
:符合条件的数字的一次幂和(位数和)。s2
:符合条件的数字的平方和。
核心递归函数 dp
dp(pos, val, sum, op)
通过递归的方式从高位到低位进行搜索,并判断当前数是否符合条件。
-
递归终止条件:
if (!pos) { if (val && sum) return {1, 0, 0}; else return {0, 0, 0}; }
当
pos == 0
时(即递归到底,所有位处理完成),如果满足val != 0
且sum != 0
(即当前数满足题目条件),返回{1, 0, 0}
表示找到一个符合条件的数。 -
记忆化处理:
if (!op && ~f[pos][val][sum].s0) return f[pos][val][sum];
如果
op == 0
(不再严格对齐上界)且f[pos][val][sum]
已计算过,则直接返回缓存值,避免重复计算。 -
状态转移与结果合并:
递归处理每一位数字,更新符合条件的数字的计数、一次幂和和二次幂和。具体步骤:- 遍历每一位数字
i
,且i != 7
(因为题目要求数位上不能包含7
)。 - 递归计算下一位的结果
t
。 - 使用
k
表示当前位对数值的实际贡献:k = i * 10^(pos-1) % MOD
。
然后更新
s1
和s2
:t.s2 = (t.s2 + 2ll * k % MOD * t.s1 % MOD) % MOD; t.s2 = (t.s2 + k * k % MOD * t.s0 % MOD) % MOD; t.s1 = (t.s1 + k * t.s0 % MOD) % MOD; res += t;
s2
更新:使用2 * k * s1
和k^2 * s0
更新平方和。s1
更新:直接将当前位贡献k
加到一次幂和s1
中。
一次幂和
s1
和平方和s2
的推导假设当前我们在构造一个数,通过给它增加一个数字
i
来构成新的数,这个数在原有数的基础上增加了一位,并且这位的实际贡献是k = i * 10^{pos-1} % MOD
。这里pos
表示当前位的位置,10^{pos-1}
是该位置上的位权(即这位数字对整体数值的影响),MOD
是取模操作来保证计算不溢出。t.s1
的更新:一次幂和首先考虑一次幂和的更新(即新的符合条件的数字的位数和)。
- 假设
t.s0
表示符合条件的数字个数。 - 每个符合条件的数字会增加
k
的贡献到位数和中。
因此:
t . s 1 = ( t . s 1 + k × t . s 0 ) m o d M O D t.s1 = (t.s1 + k \times t.s0) \mod MOD t.s1=(t.s1+k×t.s0)modMOD
- 这里
k * t.s0
表示当前位k
对所有符合条件数字的位数和的贡献。 - 累加上原来的
t.s1
,得到当前的位数和。
t.s2
的更新:平方和平方和
t.s2
的更新更为复杂,因为我们需要考虑新加入的位对平方和的影响,这涉及到一次幂和s1
和当前位的平方贡献。平方和的更新公式可以通过以下步骤推导:
- 设
t.s1
是已经累加的位数和(一次幂和)。 - 新加入的位
k
对所有符合条件数字的平方和产生影响。
平方和的更新公式可以分解成两个部分:
- 一次幂和对平方和的影响:根据二次展开公式
(a + b)^2 = a^2 + 2ab + b^2
,当我们给数字增加一个位k
时,平方和中会增加2 * k * s1
的项。 - 新位的平方贡献:即
k^2 * s0
,因为当前位数k
本身也会对平方和产生贡献。
因此,完整的平方和更新公式为:
t . s 2 = ( t . s 2 + 2 × k × t . s 1 + k 2 × t . s 0 ) m o d M O D t.s2 = (t.s2 + 2 \times k \times t.s1 + k^2 \times t.s0) \mod MOD t.s2=(t.s2+2×k×t.s1+k2×t.s0)modMOD
实际代码的推导与解释
代码中每一行的具体含义如下:
t.s2 = (t.s2 + 2ll * k % MOD * t.s1 % MOD) % MOD;
2ll * k % MOD * t.s1 % MOD
:表示2 * k * t.s1
,即新位k
对平方和的影响。这部分来源于(a + b)^2 = a^2 + 2ab + b^2
的展开式中的2ab
部分。
t.s2 = (t.s2 + k * k % MOD * t.s0 % MOD) % MOD;
k * k % MOD * t.s0 % MOD
:表示k^2 * t.s0
,即新位k
自身的平方对平方和的贡献。这部分来源于(a + b)^2
展开式中的b^2
项。
t.s1 = (t.s1 + k * t.s0 % MOD) % MOD;
k * t.s0 % MOD
:表示新位k
对一次幂和的贡献。
- 遍历每一位数字
-
返回与缓存:
return op ? res : f[pos][val][sum] = res;
如果当前受上界限制(
op == 1
),不缓存,否则将结果res
存入f
数组以便复用。
辅助函数 calc
LL calc(LL x)
{
memset(f, -1, sizeof f); al = 0;
for ( ; x; x /= 10) a[ ++ al] = x % 10;
return dp(al, 0, 0, 1).s2;
}
calc(x)
用来计算从 1
到 x
的符合条件数字的平方和。通过 dp
递归获取最终的 s2
值。
主函数 main
int main()
{
pow10[0] = 1;
for (int i = 1; i < 20; i++) pow10[i] = 10ll * pow10[i - 1] % MOD;
cin >> T;
while (T --)
{
cin >> l >> r;
cout << ((calc(r) - calc(l - 1)) % MOD + MOD) % MOD << endl;
}
return 0;
}
main
函数中:
- 初始化
pow10
数组,用于快速计算位权。 - 对每组区间
[l, r]
,通过calc(r) - calc(l - 1)
获取[l, r]
区间符合条件数字的平方和之和。结果取模处理防止负值。