<luogu>P2602 [ZJOI2010]数字计数

这篇博客详细解析了P2602 [ZJOI2010]数字计数问题的解题思路,从暴力枚举到采用递推算法优化。博主首先介绍了问题的背景和暴力方法的局限性,然后阐述了如何通过递推思想简化问题,理解递推式,并给出了处理最高位数字的方法。接着,博主解释了如何将任意整数拆分成递推问题并处理零的情况。最后,博主提到了如何处理非从0开始的数,通过区间相减求解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

P2602 [ZJOI2010]数字计数

题目描述
给定两个正整数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进行暴力枚举得到答案,那可不可以先对x1x2……进行暴力枚举,然后通过数学计算来得到最终答案呢?想到这里,接下来一步就是该如何通过数学计算得到答案,以及构造出较好的程序结构。

在这篇题解里,大牛说要用大眼观察法,可奈何蒟蒻没有大眼,而小眼观察法显然是行不通的,所以蒟蒻需要想办法让自己理解。于是在某节历史课上,蒟蒻终于理解了为何可以这样,现在我可以讲的响了。首先我们进行三次枚举:

[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],一个是10n-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 * 1000A * 1000 + 1个数,如果就直接这样继续向下处理BCD,则会遗漏BCD个A,即A000 ~ ABCDBCD+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;
        
    ……
    ……
        
    }

但是这样还不够,因为我们会发现最终的结果会存在形如0XXX00XX000X,这样不能算零的数,所以我们要把这些零都减去,但这些零该怎么统计呢?很简单。既然我们知道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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值