【动态规划】2 数位DP 数字计数

Tips: 本篇blog讲解一道数位DP的例题 数字计数,适合数位DP的初学者


1. 题目链接

[ZJOI2010] 数字计数 - 洛谷icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P2602


2. 题意分析

本题数据范围很大l 和 r 都达到了 10^{12} 级别,很显然 O(n) 的复杂度是无法通过的。那么我们需要考虑一些数学转化的做法

我们现在考虑,虽然 l 和 r 都很大,但是 log(l) 和 log(r) 的值都很小,所以我们考虑使用数位DP进行解决


3. 思路分析

(1) 数位DP

首先考虑如何设计数位DP的状态,我们设 dp[i] 表示数字长度为 i 时每种数字的出现次数,这样我们可以递推求解 dp[i] 。

如何求解 dp[i] ? 

采用递推!因为数字长度 i 每次增加 1 ,就意味着数字新多出了一位,那么在这一位上出现 0 到 9其中一个数字的个数会变为 10^{(i-1)}+dp[i-1]*10 。因为在新增的这一位上数字个数会增加10^{i-1} ,并且后面的 (i-1) 位新增的数字个数为 dp[i-1]*10 

综合可得:

 dp[i] = dp[i-1]*10+10^{(i-1)}

所以,我们可以先提前计算出所有需要使用的 10^{i} ,可以避免重复计算

现在我们得到了每个数字长度的数字出现次数,接下来我们考虑:一个数 a 不一定正好是一个 10^{i},所以我们需要枚举每一位上的数字 a[i] ,并对此计算答案是加上其对应的 dp[i-1]*a[i] ,因为这个数字的这一位实际含义为 10^{(i-1)}*a[i] 。所以这一步我们对这个数进行数位拆分并枚举数位统计答案即可。


(2) 答案统计

由于我们进行了数位拆分,那么又产生了一个问题:前导 0 

什么是前导 0 ?

前导 0 ,即放在一个数前用来补充位数的若干个连续的0,也可以理解成是一个有前导0的数的最长全为0的前缀,通常是为了保持数字位数的统一。在本题中,拆分的时候可能产生数字较小会有多余的位置无法拆分,就会变成0,那么在计算时0的个数就会受影响。 

 所以,我们需要统计前导0个数并减去这部分对答案的贡献即可。

由于我们统计的时前 i 个数的各种数字出现次数,而最终答案是在 [l,r] 这个区间内,那么我们就可以用前缀和的思路,设前 i 个数的各种数字出现次数为 res[i] ,

那么可得

ans = res[r]-res[l-1]

最后就可以得到答案了


4. 代码实现

#include <bits/stdc++.h>
using namespace std;
long long l,r,f[17],ans1[10],ans2[10],p[17],a[17];
void solve(long long x,long long *res)
{
	long long len = 0,w = x,t = x;
	memset(a,0,sizeof(a));
	while (w)  //数位拆分
	{
		a[++len] = w % 10;
		w /= 10;
	}
	for (int i=len;i>=1;i--)
	{
		for (int j=0;j<=9;j++) //统计答案
		{
			res[j] += f[i-1]*a[i];
		}
		for (int j=0;j<a[i];j++)
		{
			res[j] += p[i-1];
		}
		t -= p[i-1]*a[i];  //处理前导0的情况
		res[a[i]] += t + 1;
		res[0] -= p[i-1]; 
	}
}
int main()
{
	cin >> l >> r;
	p[0] = 1ll;
	f[0] = 0ll;
	for (int i=1;i<=13;i++)  //f数组同上dp数组
	{
		f[i] = f[i-1]*10+p[i-1];
		p[i] = p[i-1]*10ll;  //预处理10的次方
	}
	solve(r,ans1);
	solve(l-1,ans2);
	for (int i=0;i<=9;i++)
	{
		printf("%lld ",ans1[i]-ans2[i]); //前缀和计算答案
	}
	return 0;
}

 如有疑问请在评论提出,欢迎大家批评指正,共同交流学习。

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值