目录
0.题面
1.题目分析
1.1枚举法
看到这个题,我们很容易想到暴力的写法,就是枚举a到b的每一个数字,然后统计d出现了几次。
long long CaloTime(long long a,long long b,int d)
{
long long ans = 0;
for (; a <= b; a++)
{
long long i = a;
while (i)
{
if (i % 10 == d)
ans++;
i /= 10;
}
}
return ans;
}
1.2数位dp
当然这个题暴力枚举不能通过所有数据,因为如果这个a和b的差距过大,那么O(n)的复杂度就显得太大了。我们需要找到一个更加优秀的写法。
2.数位dp原理分析
2.1计算1~n数字d出现的次数
如果我们直接计算a~b中数字d出现的次数,那么每一次的起点就会不同了。那么我们在使用dp的时候难度就会加大。所以我们可以先计算1~b中d出现的次数减去1~a-1中d出现的次数,可以计算出a~b中d出现的次数。
2.2如何计算1~n中d出现的次数
2.2.1个位次数的计算
如上图,假设d=1,我们不妨假设现在计算的位数叫做cur,我们先假设cur>1的情况。在这个情况下,我们的个位1出现的地方就在1,11,21……3451,一共出现了345次。
我们再看第二种情况,就是我们的cur==d的时候,这个2出现的情况,也可以从2~3452,所以出现的次数也是345次。
接下来是第三种情况,当cur小于d时,这时3出现的情况就不再是3~3453了,因为这时3453大于3452,从这不难推测出d等于4~9的情况与d=3一样,因为它们都大于cur,所以他们在个位出现的次数就是345-1=344次。
最后就是d=0的特殊情况,当d==0时,我们最后还是可以到3450,但是我们不能从0000开始了,因为我们给定的范围是1~n,0并不在范围内,所以0出现的次数就是345-1=344次。
2.2.2十位次数的计算
我们还是先来看cur大于d的情况,为了方便,我们这里把cur左边的数字称作left,右边的数字称作right。先来看left的取值,可以从00取到34,因为在left为00的时候right也为0达到最小值0010,满足要求,当两边都去最大值的时候,值为3419小于3452所以这些取值都是有效的。那么十位的1出现的次数就是(left+1)*10,因为这个left是从0~34且取每一个left后right有10种取法。然后我们不难发现d取2~4情况都与现在一样。
继续来看d==cur的情况,我们可以直观的看到,如果我们left取到34、right取到9就会超过3452,所以我们要分开讨论,当left==34时,right的值可以是0、1、2,当left<34时,right的值可以取0~9。那么这时d出现的次数就是left*10 + right +1
再来看cur<d的情况,这时的left就不能取到34了因为3460就已经大于3452了,所以这里left只能取到33,所以d出现的情况就是left*10。
最后来看d=0的特殊情况,这里的情况与个位一样,我们这里的left还是不能从00开始取,因为如果left是00,那么cur那一位就不可以是0了,如果cur这位是0,那这个数就不是两位数了。所以我们的left要从01开始取。我们再看right的取值,虽然这时0是特殊情况,但是d还是小于cur所以right的取值还是可以从0~9。
2.2.3百位到n位次数的计算
首先是d<cur的情况,我们就不细致分析了,d的出现次数为4*100,因为left有4种取法,right有100种方法,推广一下到n位,那就是(left+1)*pow(10,n-1) ,这里假设n=1时是个位,n=2时是十位……那么我们是不是可以把这个算式转换一下, 次数ans=left*pow(10,n-1) + pow(10,n-1)。因为这个pow(10,n-1)我们可以在循环中增加。后续代码实现时会讲。
这里我们先把pow(10,n-1)叫做pow,当d==cur时,次数就是left*pow+right+1。因为当left取3时,right可以取0~52一共53种取法,也就是right+1次。
当cur<d时,这里的left不能取到3,只能取到left-1 ,所以d出现的次数就是left*pow
当d==0时,left不可以从0开始取,所以可以取的数少一个,所以d出现的次数就是left*pow,当然d==0时d也是小于cur的,所以根据上面的式子,也就是ans=(left-1)*pow + pow;
2.2.4.代码实现
long long CaloTime(long long last_num,int d)
{
long long ans = 0;
for (long long pow = 1, tmp = last_num, left = 0,right=0, cur = 0; tmp != 0; pow *= 10, tmp /= 10)
{
left = tmp / 10;
right = last_num % pow;
cur = tmp % 10;
if (d == 0)
left--;
ans += left * pow;
if (cur > d)
{
ans += pow;
}
else if (cur == d)
{
ans += (right + 1);
}
}
return ans;
}
这里用for循环实现了pow的十倍增长,tmp是last_num的临时存储。当tmp==0时说last_num明遍历完毕,循环结束。
3.函数调用
int main()
{
long long a, b, d;
cin >> a >> b >> d;
cout << CaloTime(b,d) - CaloTime(a-1, d) << endl;
return 0;
}
因为前面说我们是把题目拆解开来解答,我们CaloTime计算的是1~n中d出现的次数,但是我们需要的是a~b中d出现的次数,那么我们就要调用两次函数一次计算1~b,一次计算1~a-1,两个答案相减就是最后我们需要的答案。
4.结语
本人第一次写算法博客哈,有啥不对的地方欢迎评论区友好交流,我也会及时做出改正!!