1 条题解
-
0
思路与算法
用F(t)返回1..t中十类数字各自出现次数,最终逐项计算F(b)-F(a-1)。
从高位向低位做数位DP,状态包含剩余位位置pos、是否已经出现有效数位started,以及是否仍贴上界tight。每个状态同时返回ways(可形成的正整数数量)和count[0..9](这些数在尚需构造部分中的数字出现次数)。
枚举当前数字d后,先累加子状态的ways和count;若当前数字已经是有效数字,还要让count[d]增加child.ways,因为每个子状态方案都各贡献一次当前的d。尚未开始且放0时,不统计这个补位0。结束时,已开始则ways=1,否则ways=0,排除整数0。只缓存非tight状态。
正确性说明
每个数位选择确定一组互不重叠的后缀方案。子状态的count准确统计后缀中出现的数字;当前有效数字在每个后缀方案中恰出现一次,增加child.ways正好补齐这一位的贡献。未开始的补位0不属于实际十进制表示,所以不计数。
按剩余位数归纳,每个状态返回的方案数和十类次数都正确。tight只保留不超过上界的前缀,结束条件只计正整数,故F(t)准确统计1..t。逐项相减便得到a..b的十类出现次数。
复杂度
设十进制位数L≤13。每个非受限状态由位置和started决定,每次枚举10个数字并合并10项统计,总时间O(L×10²),空间O(L×10)。程序使用20×2个Stats缓存对象,启动时初始化各自10项计数;每次前缀统计清空40个ready标志,这些初始化成本均已包含在上述界内。
易错点
数字0的出现次数不能把前导补0算进去。必须同时保留方案数ways,否则无法正确加入当前位的贡献。端点可达10^12,统计结果可能超过32位,使用long long。输出顺序固定为0到9,共10个整数;F(0)应全为0。
参考代码(C++11)
#include <bits/stdc++.h> using namespace std; using ll = long long; struct Stats { ll ways; array<ll, 10> count; Stats() : ways(0) { count.fill(0); } }; vector<int> digit; Stats memo[20][2]; bool ready[20][2]; Stats solve(int pos, bool started, bool tight) { if (pos < 0) { Stats base; base.ways = started; return base; } if (!tight && ready[pos][started]) return memo[pos][started]; Stats answer; int cap = tight ? digit[pos] : 9; for (int d = 0; d <= cap; ++d) { bool now = started || d != 0; Stats child = solve(pos - 1, now, tight && d == cap); answer.ways += child.ways; for (int v = 0; v < 10; ++v) answer.count[v] += child.count[v]; if (now) answer.count[d] += child.ways; } if (!tight) { ready[pos][started] = true; memo[pos][started] = answer; } return answer; } Stats count(ll n) { if (n <= 0) return Stats(); digit.clear(); while (n) { digit.push_back(n % 10); n /= 10; } memset(ready, 0, sizeof(ready)); return solve(digit.size() - 1, false, true); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); ll a, b; cin >> a >> b; Stats high = count(b), low = count(a - 1); for (int d = 0; d < 10; ++d) cout << high.count[d] - low.count[d] << (d == 9 ? '\n' : ' '); }
- 1