题目描述
给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。
输入格式:
输入文件中仅包含一行两个整数a、b,含义如上所述。
输出格式:
输出文件中包含一行10个整数,分别表示0-9在[a,b]中出现了多少次。
输入样例#1
1 99
输出样例#1
9 20 20 20 20 20 20 20 20 20
*注:这篇题解是我在根据这篇的基础上进行个人理解写出来的x
我也喜欢工口散散*
题目旁边的有这么几个算法标签:数位dp
、递推
、动态规划
、深度优先搜索dfs
。蒟蒻完全不知道数位dp
是什么东西,于是打开了题解,康了若久终于稍稍领会其中的奥妙(大雾
首先我们需要典型化问题,即用尽可能直观且本质的方式表述题目:
对于一个n位数:
a[n] a[n-1] …… a[3] a[2] a[1]
(a[n]
表示一个阿拉伯数字,满足a[n]≠0
)
最暴力的方法当然是穷举每一个数字然后统计每个数字出现的次数(微笑),但是作为一道省选题目,它的尊严是绝对不会允许这样的算法AC的。但是这样的思考并非毫无意义,首先我们要找出不可以优化的地方以及可以优化的地方,这些可能作为算法的基本构架
(1)
既然要统计数字,那最好需要提取每个数字,显然是从个位逐一提取,当然为了方便之后再次利用,我们显然会使用一个数组来存每一位数字,如样例。我们还可以用空余的a[0]
来存数字的长度。
long long num[20] = {0};//num[i]中i>=1,i表示位数,用来存x
num[0] = 0;//用来计数(长度length)
while(x){//将数字x存入数组
num[++num[0]] = x % 10;
x /= 10;
}
(2)
显然直接
的暴力
枚举是不可行的,那有没有什么办法可以不用枚举呢?看到递推
这个标签,蒟蒻感到confused
,这又不是数列,也不是函数,为什么就能递推
了呢?是不是对递推
的理解有问题呢?于是蒟蒻打开了baidu.com
,得到如下答案: 递推算法是一种用若干步可重复运算来描述复杂问题的方法。递推是序列计算中的一种常用算法。通常是通过计算前面的一些项来得出序列中的指定项的值。 震惊!康到第一句,蒟蒻恍然大悟,原来意思是可以用重复类似的步骤解决问题,可是这里有什么地方是可以重复类似解决的呢?我们知道,整数有这样一个性质(jiade),(int)x = x1 + x2 * 10 + x3 * 100 + ……
我们原来的思路是直接对x
进行暴力枚举得到答案,那可不可以先对x1
、x2
……进行暴力枚举,然后通过数学计算来得到最终答案呢?想到这里,接下来一步就是该如何通过数学计算得到答案,以及构造出较好的程序结构。
在这篇题解里,大牛说要用大眼观察法
,可奈何蒟蒻没有大眼
,而小眼观察法
显然是行不通的,所以蒟蒻需要想办法让自己理解。于是在某节历史课上,蒟蒻终于理解了为何可以这样,现在我可以讲的响
了。首先我们进行三次枚举:
[1]对于一位数a,a∈[0,9],a∈Z,每个数字记为k1 = 1;
[2]对于一个两位数ab,a∈[1,9],b∈[0,9],a,b∈Z
枚举每一个a,对b进行[1],得到b上出现每一个数字的次数为k1,由于a可取的值一共有9个,最终在b上每一个数出现的次数为k1 * 9
枚举每一个ax,x代表任意b,得到ax有9种,每一个数字出现1次,由于x又有10种取值,所以最终a上除0外每一个数出现的次数为1 * 10
最终得到在a和b上出现的除0数字出现的个数为k2 = k1 * 9 + 1 * 10
0出现的个数为k2` = k1 * 9
手动枚举到三位数就发现,不能直接用带k2的式子表示了,为什么呢?因为在[2]
中第二位的取值范围是[1,9]
而在[3]
中第二位的取值范围就变成[0,9]
了,这不好不方便,所以我们干脆把[2]
中第二位的取值范围变成[0,9]
,为什么可以这么做呢?因为我们这里使用的[1]
,[2]
……[n]
,本质是是对101,102,10^n进行讨论,不过没有计算最高位上的数字而已。那么修改后的枚举就变成了这样:
[1]对于一位数a,a∈[0,9],a∈Z,每个数字记为k1 = 1;
[2]对于一个两位数ab,a,b∈[0,9],a,b∈Z
枚举每一个a,对b进行[1],得到b上出现每一个数字的次数为k1,由于a可取的值一共有10个,最终在b上每一个数出现的次数为k1 * 10
枚举每一个ax,x代表任意b,得到ax有10种,每一个数字出现1次,由于x又有10^1种取值,所以最终a上每一个数出现的次数为1 * 10^1
最终得到在a和b上出现的各个数字出现的个数为k2 = k1 * 10 + 1 * 10^1
[3]对于一个三位数abc,a,b,c∈[0,9],a,b,c∈Z
枚举每一个a,对bc进行[2],(把bc这个两位数当做一个整体)得到bc上出现每一个数字的次数为k2,由于a可取的值一共有10个,最终在bc上每一个数出现的次数为 k2 * 10
枚举每一个ax,x代表任意b,得到ax有10种,每一个数字出现1次,由于x又有10^2种取值,所以最终a上每一个数出现的次数为1 * 10^2
最终得到在a,b,c上出现的各个数字出现的个数k3 = k2 * 10 + 1 * 10^2
……
通过数学归纳法仔(hu)细(si)思(luan)考(xiang),我们可以得到对于n位数的递推式的处理方法:
注:下面用f[n]
代替kn
[n]对于一个n位数abc...,a,b,c,...∈[0,9],a,b,c,...∈Z
枚举每一个a,对bc...进行[n-1],得到bc...上出现每一个数字的次数为f[n-1],由于a可取的值一共有10个,最终在bc上每一个数出现的次数为 f[n-1] * 10
枚举每一个ax,x代表任意bc...,得到ax有10种,每一个数字出现1次,由于x又有10^(n-1)种取值,所以最终a上每一个数出现的次数为1 * 10^(n-1)
最终得到在a,b,c上出现的各个数字出现的个数f[n] = f[n-1] * 10 + 1 * 10^(n-1)
得到了f[n]
的递推式,印证了递推
的标签,方向大概是对了。喜闻乐见,我们现在知道如何计算形如10^n
的次高位开始之后每一位上每一个数字出现个数的计算方法了,对于形如x * 10^n
的数只需要将结果乘以x
就可以了。
但在实际解决问题的过程中仅仅是这样还不够,因为这里只计算了不计最高位上的数字出现的次数,所以最终每次处理的时候还得处理最高位数字,很显然可以得到n
位上数字出现的次数等于10^n
那么该如何实现上述操作呢?通过观察发现,f[n]
的递推式为一个和的形式,其中一个带f[n-1]
,一个是10
的n-1
次方,两者的实现可以分别用一个数组实现,f[n-1]
在每一次递推时乘10
加到f[n]
上,后者每一次让x[n]
等于x[n-1] x 10
,然后加上即可
for(int i = num[0];i >= 1;i--){//从大位往小位处理
long long ten[20],f[20];//ten[i]=10^i;ten[i]表示i-1位数第i-1位每个数字出现几次
ten[0]=1;
for(int i = 1;i <= 13;i++){
f[i] = f[i-1]*10 + ten[i-1];//每一个数字出现的次数等于10倍上一层加上这一位在总数中出现的次数
ten[i] = 10*ten[i-1];//ten[i]=10^i的计算
}
……
……
……
}
(3)
由(2)中的内容,我们已经可以解决形如x * 10^n
的数。那么只要把任意整数拆成若干个形如x * 10^n
的子问题就可以解决。但是问题出现了,该如何拆分呢?
以一个四位数ABCD
为例子,我们将它拆成A * 1000 + B * 100 + C * 10 + D * 1
,换一种说法就是逐位处理,先处理A,再处理B,再处理C,再处理D。
for(int i = num[0];i >= 1;i--){//从大位往小位处理
……
……
//由于i位的数字不见得一样,所以需要通过i位数字与i-1位的出现个数相乘
//得到,再加上该位置该数字出现的个数,即次位数字个次位数出现的个数,
//同时对于每一位的数字之前的数字还有一部分零散的num2需要加
for(int j = 0;j <= 9;j++)
cnt[j] += f[i-1] * num[i];
for(int j = 0;j < num[i];j++)
cnt[j] += ten[i-1];
……
……
}
但是需要注意的是,处理A x 1000
时,除了f[4] * A
还应该再加上一个BCD,因为在f[4] * A
中实际上处理的数只有0 ~ A * 1000
这A * 1000 + 1
个数,如果就直接这样继续向下处理BCD,则会遗漏BCD个A,即A000 ~ ABCD
这BCD+1
个数,所以在每一次向下递推之前还得加上这些。
for(int i = num[0];i >= 1;i--){//从大位往小位处理
……
……
//所谓num2,其实是对于形如ABC的数(B为当前处理数字,A为B之前的数字串,C为B之
//后的数字串),而num2就是C,所以对num2的操作就是将存入数组的C提取出来,变整型
long long num2 = 0;
for(int j = i-1;j >= 1;j--)
num2 = num2*10 + num[j];
cnt[num[i]] += num2+1;
……
……
}
但是这样还不够,因为我们会发现最终的结果会存在形如0XXX
,00XX
,000X
,这样不能算零的数,所以我们要把这些零都减去,但这些零该怎么统计呢?很简单。既然我们知道X000
这样的数如何处理,那000X
用同样的方法处理不就好了吗?不够由于这边有一个对[i]
的循环结构,所以可以顺着之前的思想,通过ten[i-1]
来处理每一位上的0
cnt[0] -= ten[i-1];
(4)
最后一个问题,现在我们已经知道如何处理从0开始到某个数内该问题的解决办法,可是这道题不是从0开始的,这时候就要利用一个区间相减的原理了,很显然这里的答案可以进行代数相减,所以只要ans1[i] - ans2[i]
就好啦~
work(a-1,cnta);//求得a左侧的数中i出现多少次[1,a-1]
work(b,cntb);//求得b左侧的数中i出现了多少次[a,b]
for(int i = 0;i <= 9;i++)
cout << cntb[i] - cnta[i] << " ";//将结果做差得到区间内的数中i出现了多少次
AC代码
#include<iostream>
using std::cin;using std::cout;using std::endl;
long long a,b;//a,b为左右区间
long long ten[20],f[20];//ten[i]=10^i;ten[i]表示i-1位数第i-1位每个数字出现几次
//f[i]表示i位数的每一个数字总共出现f[i]次
long long cnta[20],cntb[20];
void work(long long x,long long *cnt){
long long num[20] = {0};//num[i]中i>=1,i表示位数,用来存x
num[0] = 0;//用来计数(长度length)
while(x){//将数字x存入数组
num[++num[0]] = x % 10;
x /= 10;
}
for(int i = num[0];i >= 1;i--){//从大位往小位处理
//由于i位的数字不见得一样,所以需要通过i位数字与i-1位的出现个数相乘
//得到,再加上该位置该数字出现的个数,即次位数字个次位数出现的个数,
//同时对于每一位的数字之前的数字还有一部分零散的num2需要加
for(int j = 0;j <= 9;j++)
cnt[j] += f[i-1] * num[i];
for(int j = 0;j < num[i];j++)
cnt[j] += ten[i-1];
//所谓num2,其实是对于形如ABC的数(B为当前处理数字,A为B之前的数字串,C为B之
//后的数字串),而num2就是C,所以对num2的操作就是将存入数组的C提取出来,变整型
long long num2 = 0;
for(int j = i-1;j >= 1;j--)
num2 = num2*10 + num[j];
cnt[num[i]] += num2+1;
//减去每一个零导数字,为当前位数最高位为0的数字的个数,因此减去ten[i-1]
cnt[0] -= ten[i-1];
}
}
int main(){
cin >> a >> b;
ten[0]=1;
for(int i = 1;i <= 13;i++){
f[i] = f[i-1]*10 + ten[i-1];//每一个数字出现的次数等于10倍上一层加上这一位在总数中出现的次数
ten[i] = 10*ten[i-1];//ten[i]=10^i的计算
}
work(a-1,cnta);//求得a左侧的数中i出现多少次[1,a-1]
work(b,cntb);//求得b左侧的数中i出现了多少次[a,b]
for(int i = 0;i <= 9;i++)
cout << cntb[i] - cnta[i] << " ";//将结果做差得到区间内的数中i出现了多少次
return 0;
}