数位dp的写法貌似很多,记忆化搜索,dp预处理然后试填什么的,我写的一般都是后者
数位dp写着写着就偏了,感觉本质上就是个数学题。。。
The Counting Problem
求 [L,R] 内每个数码(0~9)出现的次数。
首先可以把区间的解拆成两个前缀和相减的模式,于是问题就变成了求[1,R]内每个……
众所周知这是一道数位dp,但是对于只听过概念的我来说怎么写呢
当然是愉快的分类讨论啦。
(从某种意义上来讲这是段废话不是什么题解)位数小于R的是一种情况,这可以快速计算。然后就是位数和R一样的数的贡献,这里可以在分一下,分成最高位与R相同和不相同两种情况。然后肯定要dp,状态设什么,这样,那样,过样例了!怎么wa没了,哪里错了,这个dp有问题啊,还是不对啊,??要dp干嘛用??直接算不就好了。。。
于是我调了好久好久,最终发现不用dp。。。这就变成了一道大力分类讨论数学题。。。代码又臭又长
以下才是正传。后来我觉得这样不行啊,决心痛改前非(此时已经做了几道数位dp题)。
套路是这样的,先预处理一个dp,f[i][j]表示数字位数为i的数中j出现的次数,并且允许出现前导0。然后用试填法求解
对于R,先考虑R的最高位,考虑在这一位填的数是什么,填的数必定不能大于R的最高位。如果小于最高位的话,之后的位上无论填什么都是满足条件的(因为都不会大于R),(假设k是R的位数)于是这对应的答案就是f[k-1][0~9]
否则最高位上填的数就与R的最高位相等,继续往下枚举算剩下的贡献。可以说我们枚举i(1-k),统计1~(i-1)位于R相同第i位与R不同的数带来的贡献(位数从高位往低位数)。
注意,这样的统计方式并不会统计R的贡献,所以需要特殊处理R。
写完以后发现我写挂了,不经怀疑人生(两次死在1 121上的痛楚),愉快的发现前导0挂死了我。
上面的做法不能很好的处理掉前导0 。因为比R的小的数,可能位数比它小,这就对应着在R的前若干位上填0,但是这些0是前导0,不能计入答案,而上述算法将它们计入了答案。
对于这个bug,可以人脑统计一下多算的0,然后减掉。或者说你再分情况讨论一下,讨论位数和R一样和比它小两种情况。枚举位数,强制要求其最高位不为0,计数可以在dp中在开一个数组直接预处理,位数和R一样的,就在试填的时候强制要求最高位不能填0(这样写感觉比较清晰)
人脑计算系列
#include <bits/stdc++.h>
using namespace std;
long long Pow[15],f[15][15],ans[2][15],L,R;
long long a[15];
void Prework()//dp预处理
{
Pow[0]=1;
for (int i=1;i<=12;i++)
Pow[i]=Pow[i-1]*10;
memset(f,0,sizeof(f));
for (int i=1;i<=12;i++)
for (int j=0;j<=9;j++)
{
f[i][j]=f[i-1][j]*10;
f[i][j]+=Pow[i-1];
}
}
void calc(long long N,int num)
{
int k=0;
while (N>0) {a[++k]=N%10;ans[num][a[k]]++;N/=10;}
for (int i=k;i>=1;i--)
{
for (int j=0;j<=9;j++)
ans[num][j]+=a[i]*f[i-1][j];
for (int j=0;j<a[i];j++)
if (j>0||i<k) ans[num][j]+=Pow[i-1];
for (int j=k;j>i;j--)
ans[num][a[j]]+=a[i]*Pow[i-1];
}
for (int i=max(k-2,0);i>=0;i--)
ans[num][0]-=Pow[i];//减去多算的0,我也忘了为什么
}
int main()
{
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
Prework();
scanf("%lld%lld",&L,&R);
while (L||R)
{
memset(ans,0,sizeof(ans));
calc(R,0);
calc(L-1,1);
for (int i=0;i<=9;i++)
printf("%lld ",ans[0][i]-ans[1][i]);
printf("\n");
scanf("%lld%lld",&L,&R);
}
return 0;
}