Tips: 本篇blog讲解一道数位DP的例题 数字计数,适合数位DP的初学者
1. 题目链接
[ZJOI2010] 数字计数 - 洛谷https://www.luogu.com.cn/problem/P2602
2. 题意分析
本题数据范围很大, 和
都达到了
级别,很显然
的复杂度是无法通过的。那么我们需要考虑一些数学转化的做法
我们现在考虑,虽然 和
都很大,但是
和
的值都很小,所以我们考虑使用数位DP进行解决
3. 思路分析
(1) 数位DP
首先考虑如何设计数位DP的状态,我们设 表示数字长度为
时每种数字的出现次数,这样我们可以递推求解
。
如何求解 ?
采用递推!因为数字长度 每次增加
,就意味着数字新多出了一位,那么在这一位上出现
到
其中一个数字的个数会变为
。因为在新增的这一位上数字个数会增加
,并且后面的
位新增的数字个数为
综合可得:
所以,我们可以先提前计算出所有需要使用的 ,可以避免重复计算。
现在我们得到了每个数字长度的数字出现次数,接下来我们考虑:一个数 不一定正好是一个
,所以我们需要枚举每一位上的数字
,并对此计算答案是加上其对应的
,因为这个数字的这一位实际含义为
。所以这一步我们对这个数进行数位拆分并枚举数位统计答案即可。
(2) 答案统计
由于我们进行了数位拆分,那么又产生了一个问题:前导 。
什么是前导
?
前导
,即放在一个数前用来补充位数的若干个连续的0,也可以理解成是一个有前导0的数的最长全为0的前缀,通常是为了保持数字位数的统一。在本题中,拆分的时候可能产生数字较小会有多余的位置无法拆分,就会变成0,那么在计算时0的个数就会受影响。
所以,我们需要统计前导0个数并减去这部分对答案的贡献即可。
由于我们统计的时前 个数的各种数字出现次数,而最终答案是在
这个区间内,那么我们就可以用前缀和的思路,设前
个数的各种数字出现次数为
,
那么可得
最后就可以得到答案了
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;
}
如有疑问请在评论提出,欢迎大家批评指正,共同交流学习。