洛谷2602 bzoj1833 ZJOI2010 数字计数 数位dp

2 篇文章 0 订阅

题目链接
题意:给你两个数 a a b,让你求区间 [a,b] [ a , b ] 当中 09 0 − 9 每一个数字出现在多少次。

题解:
这种题目应该不难看出是数位dp题目。拿到这道题的第一感觉可能会发现答案中很可能有某些数字的出现次数是一样的,再仔细考虑一下的话可以发现,假如位数确定了,那么所有 i i 位数中1 9 9 的出现次数是一样的。

那么我们会想,为什么0的出现次数和别的数不一样呢?我们发现原因是一个数的最高位不能是 0 0 ,所以对于所以i位数, 1 1 9都可以做最高位,所以就比 0 0 要多10i1次。但是这样会导致 0 0 比较难算,我们一个常用的思路是想办法把特殊情况也转化成一般情况,所以我们会想到如果最高位可以是0,那么对于所有 i i 位数,0 9 9 每个数字出现的次数是一样的,那么我们假设在过程中允许出现前导0,就可以让所有数字的出现次数相同了。

那么我们接下来要做的就是求出所有数据范围内可以含前导 0 0 i位数的每个数字的出现次数。我们设我们对于 i i 位数,答案是dp[i],那么dp题我们肯定会去考虑递推关系,我们发现这个答案与 i1 i − 1 位数的答案有关,对于之前的每一个 i1 i − 1 位数,在多一位时都会出现 10 10 次,也就是在原数前面分别加上一个 0 0 9的数,而还会因为它们每个数作为最高位出现了 10i1 10 i − 1 次,不难发现作为最高位出现的这些次数在少一位的情况下肯定是没有统计过的,所以不存在重复统计。因此递推关系是 dp[i]=dp[i1]10+10i1 d p [ i ] = d p [ i − 1 ] ∗ 10 + 10 i − 1

我们答案是要求 [a,b] [ a , b ] 的个数,这种数位dp题常转化为用 [0,b] [ 0 , b ] 的答案减去 [0,a1] [ 0 , a − 1 ] 的答案。但是我们求出来的是没有上界并且可以有前导 0 0 的情况,那么怎么统计实际的答案呢?我们把数拆成许多段来统计答案。设我们现在要求的数n是个 m m 位的数,我们设它第i位的数字是 num[i] n u m [ i ] ,那么我们可以把这个数字分成若干部分求。

第一部分是 [0,10m11] [ 0 , 10 m − 1 − 1 ] ,比如 235 235 就是求 [0,99] [ 0 , 99 ] 的答案, 3122 3122 就是求 [0,999] [ 0 , 999 ] 的答案。这一部分的答案要分两类,把 09 0 − 9 分为 0 0 和非0数字,分类的原因是 0 0 在实际做的时候不能做最高位。0的情况是它不可以做最高位,所以答案只能是在少一位的基础上当前位填上 19 1 − 9 九种情况,所以是 m1i=1dp[i1]9 ∑ i = 1 m − 1 d p [ i − 1 ] ∗ 9 。这里之所以乘 dp[i1] d p [ i − 1 ] 是因为当前的位填了一个非 0 0 的数字,所以即使之前有前导0加上当前位之后也会变成一个合法数字。对于非 0 0 的情况,当前答案不仅可以是由少一位的答案在当前位填上19九种情况,还有它本身作为最高位出现的 10i1 10 i − 1 次,所以是 m1i=1dp[i1]9+10i1 ∑ i = 1 m − 1 d p [ i − 1 ] ∗ 9 + 10 i − 1

第二部分是 [10m1,num[m]10m11] [ 10 m − 1 , n u m [ m ] ∗ 10 m − 1 − 1 ] ,比如 3355 3355 就会在这一部分统计 [1000,2999] [ 1000 , 2999 ] ,而 135 135 在这一部分无需统计。对于 09 0 − 9 的每一个数字,答案都有是 num[m]1 n u m [ m ] − 1 个数做最高位时的情况乘少一位时的情况,也就是 dp[m1](num[m]1) d p [ m − 1 ] ∗ ( n u m [ m ] − 1 ) 。对于小于 num[m] n u m [ m ] 的非零数,他们还可以做最高位,因此它们的答案还要加上 10m1 10 m − 1

第三部分就是求 [num[m]10m1,n] [ n u m [ m ] ∗ 10 m − 1 , n ] 。首先我们设余数 rest=nnum[m]10m1 r e s t = n − n u m [ m ] ∗ 10 m − 1 ,也就是要求的数 n n 去掉最高位之后的数。那么就有rest+1个小于等于 n n 的数是由num[m]作最高位的,那么 num[m] n u m [ m ] 的答案要加上 rest+1 r e s t + 1 ,例如 1002 1002 1000 1000 之间有 3 3 个最高位是1的四位数,而不是 2 2 个,所以不要忘了加一。然后我们依次枚举余下数字的这m1个数位,分别计算对答案的贡献。每次余数 rest r e s t 要减去之前的最高位,然后让当前第 i i 位的数字num[i]的答案加上 rest+1 r e s t + 1 。然后由于当前位不是最高位了,所以可以填 0 0 了,那么每个小于num[i]的数字都可以作 10i1 10 i − 1 次最高位,而 09 0 − 9 每一个前面又都可以填 num[i] n u m [ i ] 个数,所以答案增加 dp[i1]num[i] d p [ i − 1 ] ∗ n u m [ i ] ,这里虽然不能填 num[i] n u m [ i ] ,但是可以填 0 0 ,少一个多一个最终乘的数字不变。

这样我们就可以求出来[0,b] [0,a1] [ 0 , a − 1 ] 09 0 − 9 各出现了多少次,那么我们用每个数字在 [0,b] [ 0 , b ] 中出现的次数减去对应数字在 [0,a1] [ 0 , a − 1 ] 出现的次数就是最终答案。

PS:据说这题在bzoj上最后可能因为空格而PE?反正我习惯性的加了个换行就一遍过了。

代码:

#include <bits/stdc++.h>
using namespace std;

long long a,b,cnta[10],cntb[10],ten[14],dp[14];
//ten[i]记录10的i次方,dp[i]表示前i位包含前导0的情况下每个数字出现的次数
//cnta[i]表示i在[0,a]中出现的次数,cntb表示在[0,b]中出现的次数
void solve(long long n,long long *cnt)
{
    if(n==0)
    return;
    int num[15],m=0;//num记录每一位的数字,m记录n有多少位
    long long rest=n;//记录每次减去x*10^y之后的余数 
    while(n)
    {
        num[++m]=n%10;
        n/=10;
    } 
    for(int i=1;i<m;++i)//先处理出前m-1位对答案的影响 
    {
        cnt[0]+=dp[i-1]*9;//不能有前导0,所以0不能加10^(i-1)
        for(int j=1;j<=9;++j)
        cnt[j]+=dp[i-1]*9+ten[i-1]; 
//除掉当前位是0的情况,所以乘9,只要现在这一位不是0,之前就算有前导0也是合法的(如1001,当前是1,之前是001)       
    }
    //先处理最高位上小于最高位数字的那些数对答案的贡献 
    for(int i=1;i<num[m];++i)
    cnt[i]+=ten[m-1];
    for(int i=0;i<=9;++i)//最高位不可能是0,所以num[m]-1不可能是负数 
    cnt[i]+=dp[m-1]*(num[m]-1);
    //处理余数的贡献 
    rest-=num[m]*ten[m-1];
    cnt[num[m]]+=rest+1;
//注意要+1,因为还有后缀全是0一个数
//例如1000-1002的rest=2,但是有1000、1001、1002、1003三个数,所以是2(rest)+1=3   
    for(int i=m-1;i>=1;--i)
    {
        rest-=num[i]*ten[i-1];//每次去掉之前的最高位
        for(int j=0;j<num[i];++j)
        cnt[j]+=ten[i-1];
        for(int j=0;j<=9;++j)
        cnt[j]+=dp[i-1]*num[i];
//不能是num[i],但是可以是0,少一种又多一种,总数不变,所以乘num[i]而不是num[i]-1
        cnt[num[i]]+=rest+1;
    }
} 
int main()
{
    scanf("%lld%lld",&a,&b);
    ten[0]=1;
    for(int i=1;i<=13;++i)
    {
        dp[i]=dp[i-1]*10+ten[i-1];
        ten[i]=ten[i-1]*10;
    } 
    solve(b,cntb);
    solve(a-1,cnta);
    for(int i=0;i<=9;++i)
    printf("%lld ",cntb[i]-cnta[i]);
    printf("\n");
    return 0;
} 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值