数位dp总结---LZM

发布时间 2023-09-07 20:39:19作者: 镜予歌

数位DP总结 BY----LZM

当你学会了数位DP,那么你会发现,在考场上,你也写不出来。

首先给出一道例题:

给出一个区间 \([l,r]\) ,求出区间内每个数字的数位和,答案 \(\bmod \ 998244353\)\(1\le l,r \le 10^{18}\)

样例输入           样例输出
24 69                411

考虑枚举从 \(l\)\(r\) ,对于每个数都拆分成数位,然后暴力的加。
复杂度很显然是 \(O(rlogr)\)爆chao,非常痛苦。

考虑优化,先想从 \(1\)\(l-1\) ,再想从 \(1\)\(r\) ,然后发现答案是一个前缀和的形式,两者相减即可得到答案。

所以我们只要有一个东西能够解决从 \(1\)\(x\) 的答案就可以轻松的求出来,这里给出方法。

假设从高位枚举到低位,对于每一个数位都可能有10个数字(0,1,2....8,9),为什么说可能呢,因为可能这个数字太大了,会超过 \(x\) ,的上界,比如在枚举 \(x=123\)时,我们已经枚举到了第二位,

我们枚举的---》 \(1 \Box \Box\)

枚举的第一个数不能超过1,同样的,第二位不能超过二。

还有一种情况,我们枚举的---》 \(0 \Box \Box\)

发现有前导零的情况,那么对于这一种我们就要记录下来。

那么怎么实现呢???

记忆化!搜!索!

我们在 \(dfs\) 中记录几个参数:

  • \(len\):代表目前从高位到低位枚举的位数
  • \(lead\):代表是否有前导零
  • \(top\):代表是否到了上界
  • \(sum\):代表此时的数位和

然后在开一个 \(4\) 维的 \(dp\) 记忆化数组。

为什么记忆化可以?!

因为 \(dp\) 数组记录的状态是唯一的,如果当前状态已经被搜过了,那么后面搜的一定是一样的。

以下给出代码。

#include<bits/stdc++.h>
#define int long long 

const int p=1e9+7;

int a[20];
int f[20][3][3][200];

int dfs(int len,int lead,int top,int sum)
{
    if(!len)return sum;//没有位数了
    if(!lead && !top && f[len][lead][top][sum]!=-1)return f[len][lead][top][sum];//状态被搜过了,直接回去
    int bound=top?a[len]:9,res=0;//如果上界到了就枚举到这一位最多能枚举到的
    for(int i=0;i<=bound;i++)res=(res+dfs(len-1,lead && !i,top && i==bound,sum+i))%p; 
    if(!lead && !top)f[len][lead][top][sum]=res;
    return res;
}

int read()//快读就是好
{
    int x=0,s=1;
    char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')s=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-48;
    return x*s;
}

signed main()
{
    memset(f,-1,sizeof(f));//记得清空
    int n=read()-1,ans=0,cnt=0;//初始n记得减1
    while(n)a[++cnt]=n%10,n/=10;//从高位到低位
    ans-=dfs(cnt,1,1,0);
    cnt=0;
    n=read();
    memset(f,-1,sizeof(f));
    while(n)a[++cnt]=n%10,n/=10;
    ans+=dfs(cnt,1,1,0);
    printf("%lld\n",(ans%p+p)%p);
    return 0;
}

接着下一道题

不含前导零且相邻两个数字之差至少为 \(2\) 的正整数被称为 \(windy\) 数。在 \(a\)\(b\) 之间,包括 \(a\)\(b\) ,总共有多少个 \(windy\) 数?

这个和上面的差不多,只是要换一下参数

记录前一个数,以及前导零和上界就可以和上一个题一样了,但是要记得一开始传参要从-2开始,因为一开始可以随便填。

#include<bits/stdc++.h>
#define int long long 

using namespace std;

const int N=30;

int n;
int a[N];
int ans;
int f[N][N][N][N];

void init(int x)
{
    memset(a,0,sizeof(a));
    n=0;
    while(x)a[++n]=x%10,x/=10;
}

int dfs(int len,int pos,int lead,int top)
{
    if(!len)return 1;
    if(!top && !lead && f[len][pos][lead][top]!=-1)return f[len][pos][lead][top];
    int bound=top?a[len]:9,res=0;
    for(int i=0;i<=bound;i++)
    {
        if(abs(pos-i)<2)continue;
        if(lead && !i)res+=dfs(len-1,-2,1,top && i==bound);
        else res+=dfs(len-1,i,0,top && i==bound);
    }
    if(!lead && !top)f[len][pos][lead][top]=res;
    return res;
}

int read()
{
    int x=0,s=1;
    char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')s=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-48;
    return x*s;
}

signed main()
{
    memset(f,-1,sizeof(f));
    int x=read();
    x--;
    init(x);
    ans-=dfs(n,-2,1,1);
    x=read();
    init(x);
    memset(f,-1,sizeof(f));
    ans+=dfs(n,-2,1,1);
    cout<<ans;
    return 0;
}

那么其他的就差不多了。

总结:记忆化搜索!

然后你就可以AK \(LSYOI\) 上面的数位DP了