题意
给定两个正整数 和 ,求在 中的所有整数中,每个数码(digit)各出现了多少次。
样例
输入 | 输出 |
1 99 | 9 20 20 20 20 20 20 20 20 20 |
思路
本题是正儿八经的数位DP板子题。对于区间,我们可以转化为,所以关键就是要求,而说到这里数位DP就坐不住了!而所谓数位DP正是会者不难难者不会的东西,所以我们直接来到数位DP的环节吧!这里采用记忆化搜索来实现数位DP。
在记忆化搜索前,我们要先对这个数进行一些处理——包括拆分成若干数组成的数组并反转它(这这样可以直接从高位开始搜起,不用拐太多弯路):
long long part(long long x) {
len = 0;
while (x) {
a[++len] = x % 10;
x /= 10;
}
memset(dp, -1, sizeof(dp));
reverse(a + 1, a + 1 + len);
return dfs(1, 0, 1, 1);
}
搜索就直接从高位往低位搜:
long long dfs(int pos, long long ans, int limit, int lead) {
if (pos > len)
return ans; //不能再搜下去了
long long u = 0;
int res = limit ? a[pos] : 9; //如果无最高位限制,那么就可以取到9
for (int i = 0; i <= res; ++i) {
//有前导0且当前位是0
if ((i == 0) && lead)
u += dfs(pos + 1, ans, limit && (i == res), 1);
else
u += dfs(pos + 1, ans + (i == digit), limit && (i == res), 0);
}
return u; //记忆化
}
那怎么个“记忆化”呢?打表数数时可以发现我们数了很多重复的部分,比如11-20里的5的出现次数和21-30的出现次数一样,或者说我们在数11-100的时候其实除了数了一次十位,还数了好多次个位(而这是没有必要的!)那么可以找一个dp数组来记录本次数数环节得到的答案,dp的状态便是本次搜索的有关参数(多加点参数有利于不认错!),并且在检测到记忆的时候直接返回对应的值,可以大大减少搜索时间!于是搜索过程变成了这样:
long long dfs(int pos, long long ans, int limit, int lead) {
if (pos > len)
return ans; //不能再搜下去了
if (dp[pos][ans][limit][lead] != -1)
return dp[pos][ans][limit][lead]; //检测到记忆
long long u = 0;
int res = limit ? a[pos] : 9; //如果无最高位限制,那么就可以取到9
for (int i = 0; i <= res; ++i) {
//有前导0且当前位是0
if ((i == 0) && lead)
u += dfs(pos + 1, ans, limit && (i == res), 1);
else
u += dfs(pos + 1, ans + (i == digit), limit && (i == res), 0);
}
return (!limit && !lead) ? dp[pos][ans][limit][lead] = u : u; //记忆化
}
代码里有两个不可忽视的细节:lead和limit标记!
Lead标记
lead标记即前导0标记,这个标记可以使代码不会搜到000001 000003 001002 这样的数,其原理变是在发现自己是最高位时不计入,并且给后续的数也打上”前导0标记“。
Limit标记
limit标记记录目前数字是否已经搜到了”极限“(即原来的数对应位的最大值)。假设对97进行搜索,那么最高位是9,我们可以直接从1到9遍历一次,往下的时候就会习惯性地来一发记忆化:1开头的时候个位有1个8,2开头的时候个位有1个8,3开头的时候......8开头的时候个位有1个8,9开头的时候个位有1个8。于是悲剧就发生了——而悲剧就发生在我们在搜到9的时候还继续看作一般情况,因此我们需要这个标记来知道该位的搜索范围,也同时避免了盲目记忆化带来的问题。
最后就是实现环节啦~
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int len, digit;
long long dp[20][20][2][2]; // dp[pos][last][limit][lead]
int a[25];
long long l, r;
long long dfs(int pos, long long ans, int limit, int lead) {
if (pos > len)
return ans; //不能再搜下去了
if (dp[pos][ans][limit][lead] != -1)
return dp[pos][ans][limit][lead]; //检测到记忆
long long u = 0;
int res = limit ? a[pos] : 9; //如果无最高位限制,那么就可以取到9
for (int i = 0; i <= res; ++i) {
//有前导0且当前位是0
if ((i == 0) && lead)
u += dfs(pos + 1, ans, limit && (i == res), 1);
else
u += dfs(pos + 1, ans + (i == digit), limit && (i == res), 0);
}
return (!limit && !lead) ? dp[pos][ans][limit][lead] = u : u; //记忆化
}
long long part(long long x) {
len = 0;
while (x) {
a[++len] = x % 10;
x /= 10;
}
memset(dp, -1, sizeof(dp));
reverse(a + 1, a + 1 + len);
return dfs(1, 0, 1, 1);
}
int main() {
cin >> l >> r;
for (int i = 0; i <= 9; ++i) {
digit = i;
cout << part(r) - part(l - 1) << " ";
}
return 0;
}