数位 dp 学习心得

发布时间 2023-10-13 14:22:36作者: DZhearMins

感觉非常牛逼。最牛逼的是很多情况下要去掉前导零。

去掉前导零的方法通常是先忽略前导零的约束,最后再容斥掉有多少0。

Luogu P2602 数字计数

来自【详细解释】数字计数 ZJOJ p2602 一道练习数位DP的好题 - moye到碗里来 的博客 - 洛谷博客 (luogu.com.cn)

那么我们首先看题,对于这道题我一开始以为很水,但是当我仔细去读题之后发现事情没那么简单。其中对于这道题(递推做法)最大的难点是难以找出递推式子(废话,你写递推就只有这点难了)。为啥,因为你很难想到怎样求出第几位它的数字又多少,因为不能有前导0。但是我们发现如果不考虑是否有前导0的话,那么这道题就似乎有递推公式。

\(f[i]\) 代表在有 i 位数字的情况下,每个数字有多少个。如果不考虑前导0,你会发现对于每一个数,它的数量都是相等的,也就是 \(f[i]=f[i-1]\times 10+10^{(i-1)};\)
( 这里我推荐使用打表+大眼观察法 )

然而这个公式推出来后,你就会面临第二个难题,怎么推出我想要的答案?

我们先设数字为ABCD

A000,如果我们要求出它所有数位之和,我们会怎么求?

鉴于我们其实已经求出了0`9`,`0`99,0~999。。。上所有数字个数( \(f[i]\) , 且没有考虑前导零)我们何不把这个A000看成0000`1000`2000...A000对于不考虑首位每一个式子的数字的出现个数为 \(A\times f[3]\) 。加上首位出现也就是小于 A 每一个数都出现了 \(10^3\) 次,再加上,我们就把 A000 处理完了。

这样你以为就把第一位处理完了?不不不,首位 A 还出现了 \(BCD+1\)次呢,也就是从A000~ABCD,这个A还出现了 \(BCD+1\) 次,所以再加上这些才行呢。那么你发现,我们成功把首位代表的所有数字个数求出来了,剩下的求解与 A 完全没有任何关系,只是 BCD 的求解,于是我们发现我们已经把一个大问题,化成了一个个小问题,也即是,对于一个这样 \(n\) 位的数,把他一位位的分离开来。

当然你还需要处理前导零。你会发现它一定是 0000 , 0001 , 0002 ... 00120013... 0101 , 0102 ... 0999这样的数,一共出现了 \(10\times(i-1)+10\times(i-2)+... 10+i\) (i表示数字位数,最后加 \(i\) 是因为0000也被统计了),让 0 的统计减去这个值,那么恭喜你这道题做完了。

\(\frak{CODE}\)

#include<bits/stdc++.h>  
using namespace std;  
#define N 13  
#define ll long long  
ll teno[N],temu[N];//teno代表小于10^i的所有数的数码出现次数,temu代表10^i  
ll L,R,ans1[11],ans2[11];  
void solve(ll n,ll *ans){  
    int a[N],len=0;  
    while(n>0){  
        a[++len]=n%10;  
        n/=10;  
    }//a就是n的每一位数码,a[len]位最大,a[0]位最小  
    for(int i=len;i>=1;--i){  
        for(int j=0;j<=9;++j)  
            ans[j]+=teno[i-1]*a[i];//1XXX 2XXX 3XXX 4XXXX 5XXX 6XXX 的 XXX 部分数字出现次数  
        for(int j=0;j<a[i];++j)  
            ans[j]+=temu[i-1];//AXXX 的 A 部分数字出现次数  
        ll tmp=0;  
        for(int j=i-1;j>=1;--j)  
            tmp=tmp*10+a[j];    //(a[i])BCD 的 a[i] 在 (a[i])000 到 (a[i])BCD 之间出现了多少个a[i]            
            ans[a[i]]+=tmp+1;    //(a[i])000 还没有统计  
    }  
    for(int i=len-1;i>=1;--i){  
        ans[0]-=(len-i)*temu[i-1]*9;//00AB 这种数字要删掉先导0  
    }                            //10-99都只用删(len-2)位,100-999都只用删(len-3)位,以此类推。  
    ans[0]-=len;                //0000 还没有删除  
    return;  
}  
int main(){  
    temu[0]=1;  
    scanf("%lld %lld",&L,&R);  
    for(int i=1;i<N;++i){  
        teno[i]=teno[i-1]*10+temu[i-1];  
        temu[i]=temu[i-1]*10;  
    }  
    solve(R,ans1);solve(L-1,ans2);  
    for(int j=0;j<=9;++j)  
    printf("%lld ",ans1[j]-j[ans2]);  
    return 0;  
}