\(Description\)

题面
给定两个数\(a,b\),求\([a,b]\)内,数码\(0-9\)出现的次数

\(Solution\)

话说这道题就是P1554梦中的统计的强化版,记得这道题刚学\(OI\)的时候做过(指梦中的统计)。对于这道题而言,朴素做法只有\(30pts\),面对庞大的数据范围只能使用数位\(DP\)
这题难点在于\(1.\)设计状态,\(2.处理两个边界\)
考虑设计状态为\(dp[i][j][k]\)表示当前考虑第\(i\)位(编号方式从低位到高位),当前位置是\(j\),对于第\(i\)位是\(j\)的所有数中,\(k\)的出现次数
转移过程是\(dp[i][j][k]=\sum\limits_{g=1}^9dp[i-1][g][k]\),因为一位数后面接的有可能是\(0-9\)。需要注意的是\(j=k\)时需要加上这一位的答案,仅考虑到第\(i\)位,那么第\(i\)位的数会出现\(10^{i-1}\)次,因此答案\(+10^{i-1}\)
上面的转移过程是\(O(10^3logn)\)左右的,但可以更快(其实也不需要),每次枚举完一位记录一下\(\sum\limits_{g=1}^9dp[i-1][g][k]\)即可。
\(O(10^2logn)\)完全没问题

void pre()
{
    pow[0]=1;
    for(re int i=1;i<=12;++i) pow[i]=pow[i-1]*10;
    for(re int i=0;i<=9;++i) dp[1][i][i]=1,sums[i]=1;
    for(re int i=2;i<=12;++i)
    {
      for(re int j=0;j<=9;++j)
        for(re int k=0;k<=9;++k)
        {
           dp[i][j][k]+=sums[k];
           if(j==k) dp[i][j][k]+=pow[i-1];
        }
      for(re int j=0;j<=9;++j) sums[j]=0;//一开始写的sum[j]=0,挂掉了
      for(re int j=0;j<=9;++j)
       for(re int k=0;k<=9;++k)
         sums[k]+=dp[i][j][k];
    }

}

计数过程处理前导零的常用思路:先处理所有\(i(i<len)\)位数的答案,这个在上一篇说过
计数过程比较重要的两个边界,一个是最终边界注意提前\(+1\),这样可以考虑最后一位,还有一个是:每次逼近完一位需要先确定这一位(让他和边界的这一位相等),再考虑下一位,注意确定这一位的过程要考虑对答案的影响,因为之前逼近过程没有计算,这里很容易出错
举个例子:\(12xxx\),第二高位会逼近到\(1\),直接到下一位,但是\(2\)忽略了,因此要加入\(2\)的答案,也就是\(12xxx%1000+1\)
更多细节看代码吧

\(Code\)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define re register
#define ll long long
using namespace std;
inline ll read()
{
    ll x=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
ll a,b,pow[20],dp[20][12][12];//dp[i][j][k]表示当前是第i位,当前位是j,后面任意贡献的k有多少个
ll tmp[20],ans[20],sums[20],cnt=0;
void pre()
{
    pow[0]=1;
    for(re int i=1;i<=12;++i) pow[i]=pow[i-1]*10;
    for(re int i=0;i<=9;++i) dp[1][i][i]=1,sums[i]=1;
    for(re int i=2;i<=12;++i)
    {
      for(re int j=0;j<=9;++j)
        for(re int k=0;k<=9;++k)
        {
           dp[i][j][k]+=sums[k];
           if(j==k) dp[i][j][k]+=pow[i-1];
        }
      for(re int j=0;j<=9;++j) sums[j]=0;//一开始写的sum[j]=0,挂掉了
      for(re int j=0;j<=9;++j)
       for(re int k=0;k<=9;++k)
         sums[k]+=dp[i][j][k];//降低一点常数
    }

}
void ask(ll x)
{
    memset(tmp,0,sizeof(tmp));
    cnt=0;
    if(x==0)
    {
        tmp[0]=1;
        return;
    }
    ll t=x,now=0,last;
    while(t){t/=10;cnt++;}
    for(re int i=1;i<=cnt-1;++i)//处理前导零
     for(re int j=1;j<=9;++j)
      for(re int k=0;k<=9;++k)
       tmp[k]+=dp[i][j][k];
    now=x/pow[cnt-1];
    for(re int j=1;j<now;++j)
     for(re int k=0;k<=9;++k)
      tmp[k]+=dp[cnt][j][k];
    last=now;
    x%=pow[cnt-1];
    for(re int i=cnt-1;i>=1;--i)
    {
        tmp[last]+=x;//这里要特别判断边界,到了12xxx的第一个x要把前面2统计上,个数就是12xxx%1000+1
        //又因为我们边界预先加了一,所以个数就是12xxx%1000,比如199会处理成200,如果计算2后有多少数会计算1个,但实际这个数是不存在的,因此不用%后+1
        now=x/pow[i-1];
        for(re int j=0;j<now;++j)
         for(re int k=0;k<=9;++k)
          tmp[k]+=dp[i][j][k];
        x%=pow[i-1];
        last=now;//更新
    }
}
int main()
{
    a=read(),b=read();
    pre();
    ask(b+1);
    for(re int i=0;i<=9;++i) ans[i]=tmp[i];
    ask(a);
    for(re int i=0;i<=9;++i)
    {
        ans[i]-=tmp[i];
        printf("%lld ",ans[i]);
    }
    return 0;
}
01-01 20:37