简单数位dp(从1~n 数字d出现的次数)

本文详细解析了如何通过数位动态规划解决题目中的计数问题,介绍了枚举法和数位dp的对比,以及如何计算1~n中每个数字d出现的次数,包括个位、十位、百位到n位的计算方法,并给出了相应的代码实现和函数调用策略。
摘要由CSDN通过智能技术生成

目录

0.题面

1.题目分析

1.1枚举法    

1.2数位dp

2.数位dp原理分析

2.1计算1~n数字d出现的次数

2.2如何计算1~n中d出现的次数

2.2.1个位次数的计算

2.2.2十位次数的计算

2.2.3百位到n位次数的计算

2.2.4.代码实现

3.函数调用

4.结语


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.结语

        本人第一次写算法博客哈,有啥不对的地方欢迎评论区友好交流,我也会及时做出改正!!

  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值