浅谈数位DP

备注:作者非常菜,有不足之处请发表于讨论区,谢谢!

求出在给定区间 [A,B] 内,符合条件 f(i) 的数 i 的个数。条件 f(i) 一般与数的大小无关,而与数的组成有关

由于数是按位$dp$,数的大小对复杂度的影响很小

这里我们使用记忆化搜索实现数位$dp$。本质上记搜其实就是$dp$

$[l,r]$

对于 $[l,r]$ 区间问题,我们一般把他转化为两次数位dp,即找 $[0,r]$ 和 $[0,l-1]$ 两段,再将结果相减就得到了我们需要的 $[l,r]$

模板(摘自大佬的博客

一、前导0标记 $ ext{lead}$
由于我们要搜的数可能很长,所以我们的直接最高位搜起

举个例子:假如我们要从 $[0,1000]$ 找任意相邻两数相等的数

显然 $111,222,888$等等是符合题意的数

但是我们发现右端点 $1000$ 是四位数

因此我们搜索的起点是 $0000$ ,而三位数的记录都是 $0111,0222,0888$ 等等

而这种情况下如果我们直接找相邻位相等则 $0000$ 符合题意而 $0111,0222,0888$ 都不符合题意了

所以我们要加一个前导$0$标记

如果当前位 $lead=1$ 而且当前位也是0,那么当前位也是前导0, $pos+1$ 继续搜;
如果当前位 $lead=1$ 但当前位不是0,则本位作为当前数的最高位, $pos+1$ 继续搜;(注意这次根据题意st或其他参数可能发生变化)
当然前导 $0$ 有时候是不需要判断的,上述的例子是一个有关数字结构上的性质,0会影响数字的结构,所以必须判断前导0;而如果我们研究的是数字的组成(例如这个数字有多少个 $1$ 之类的问题),0并不影响我们的判断,这样就不需要前导0标记了。总之,这个因题而异,并不是必须要标记(当然记了肯定是不会出错的)

二、最高位标记 $ ext{limit}$
我们知道在搜索的数位搜索范围可能发生变化;

举个例子:我们在搜索 $[0,555]$ 的数时,显然最高位搜索范围是 $0$ ~ $5$ ,而后面的位数的取值范围会根据上一位发生变化:

当最高位是 $1$ ~ $4$ 时,第二位取值为 $[0,9]$ ;
当最高位是 $5$ 时,第二位取值为 $[0,5]$ (再往上取就超出右端点范围了)
为了分清这两种情况,我们引入了 $ ext{limit}$ 标记:

若当前位 $limit=1$ 而且已经取到了能取到的最高位时,下一位 $limit=1$ ;
若当前位 $limit=1$ 但是没有取到能取到的最高位时,下一位 $limit=0$ ;
若当前位 $limit=0$ 时,下一位 $limit=0$ 。
我们设这一位的标记为 $limit$ ,这一位能取到的最大值为 $res$ ,则下一位的标记就是 $i==res$ && $limit$ ( $i$ 枚举这一位填的数)

三、 $ ext{dp}$ 值的记录和使用
最后我们考虑dp数组下标记录的值

本文介绍数位dp是在记忆化搜索的框架下进行的,每当找到一种情况我们就可以这种情况记录下来,等到搜到后面遇到相同的情况时直接使用当前记录的值。

dp数组的下标表示的是一种状态,只要当前的状态和之前搜过的某个状态完全一样,我们就可以直接返回原来已经记录下来的dp值。

再举个例子

假如我们找 $[0,123456]$ 中符合某些条件的数

假如当我们搜到 $1000$ 时,dfs从下返上来的数值就是当前位是第 $5$ 位,前一位是 $0$ 时的方案种数,搜完这位会向上反,这是我们可以记录一下:当前位第 $5$ 位,前一位是 $0$ 时,有这么多种方案种数

当我们继续搜到 1010??1010?? 时,我们发现当前状态又是搜到了第 55 位,并且上一位也是 00 ,这与我们之前记录的情况相同,这样我们就可以不继续向下搜,直接把上次的dp值返回就行了。

注意,我们返回的dp值必须和当前处于完全一样的状态,这就是为什么dp数组下标要记录 $pos,pre$ 等参量了。

最重要的来了————————————————————

接着上面的例子,范围 $[0,123456]$

如果我们搜到了 $1234$ ,我们能不能直接返回之前记录的:当前第 $5$ 位,前一位是 $4$ 时的dp值?

答案是否定的

我们发现,这个状态的dp值被记录时,当前位也就是第 $5$ 位的取值是 $[0,9]$ ,而这次当前位的取值是 $[0,5]$ ,方案数一定比之前记录的dp值要小。

当前位的取值范围为什么会和原来不一样呢?

如果你联想到了之前所讲的知识,你会发现:现在的 $limit=1$ ,最高位有取值的限制。

因此我们可以得到一个结论:当 $limit=1$ 时,不能记录和取用dp值!

类似上述的分析过程,我们也可以得出:当 $lead=1$ 时,也不能记录和取用dp值!

p.s.当然没有这么绝对的说……因题而异的说……

以上就是计划搜索的完整步骤。

ll dfs(int pos,int pre,int st,……,int lead,int limit)//记搜
{
    if(pos>len) return st;//剪枝
    if((dp[pos][pre][st]……[……]!=-1&&(!limit)&&(!lead))) return dp[pos][pre][st]……[……];//记录当前值
    ll ret=0;//暂时记录当前方案数
    int res=limit?a[len-pos+1]:9;//res当前位能取到的最大值
    for(int i=0;i<=res;i++)
    {
        //有前导0并且当前位也是前导0
        if((!i)&&lead) ret+=dfs(……,……,……,i==res&&limit);
        //有前导0但当前位不是前导0,当前位就是最高位
        else if(i&&lead) ret+=dfs(……,……,……,i==res&&limit); 
        else if(根据题意而定的判断) ret+=dfs(……,……,……,i==res&&limit);
    }
    if(!limit&&!lead) dp[pos][pre][st]……[……]=ret;//当前状态方案数记录
    return ret;
}
ll part(ll x)//把数按位拆分
{
    len=0;
    while(x) a[++len]=x%10,x/=10;
    memset(dp,-1,sizeof dp);//初始化-1(因为有可能某些情况下的方案数是0)
    return dfs(……,……,……,……);//进入记搜
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%lld%lld",&l,&r);
        if(l) printf("%lld",part(r)-part(l-1));//[l,r](l!=0)
        else printf("%lld",part(r)-part(l));//从0开始要特判
    }
    return 0;
}

下面讲几道例题:

P4127 [AHOI2009]同类分布

给出两个数$a,b$,求出$[a,b]$中各位数字之和能整除原数的数的个数。 

$1 ≤ a ≤ b ≤ 10^{18}$


本题记录前面的数字的和,同时还要知道最后产生的数字是否整除和。

• 记录各位数字的和比较容易,共9*18个状态。关键是如何知道已经产生 的数位构成的数字是否整除最后的总和,比较麻烦。考虑求模,整除的 模数为0,可以记录已经产生的数字的模数,但是又不知道最后的数字总 和是多少,这个模数怎么记录?可以这样定义:

• F[20][200][200][200]->f[i][sum][m][mod]表示到第i位,前面数字总和为sum ,%mod的余数为m,然后枚举mod即可计算。

• 这样可以解决问题,但是计算内存发现:需要1G的内存,只好想办法压 缩空间,因为mod是要在程序中枚举的,所以不必记录这一维状态,这 样空间就足够了

• F[20][200][200]->f[i][sum][m]表示到第i位,前面数字总和为sum,%mod的 余数为m,然后枚举mod即可计算

见代码

// luogu-judger-enable-o2
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
LL f[20][205][205];
LL l,r;
int a[205];
//sum 数字和
//mo %MOD后的值
LL dfs(int MOD,int len,int sum,int mo,int shangjie){
    if (len==0){
        if (MOD==sum&&mo==0) return 1;
        return 0;
    }
    if (shangjie==0&&f[len][sum][mo]!=-1) return f[len][sum][mo];
    int maxn=(shangjie)?a[len]:9;
    LL ans=0;
    for (int i=0;i<=maxn;++i)
        ans+=dfs(MOD,len-1,sum+i,(mo*10+i)%MOD,shangjie&&(i==maxn));
    if (shangjie==0) return f[len][sum][mo]=ans;
    return ans;
}
LL calc(LL x){
    int ln=0;
    while (x){
        a[++ln]=x%10;
        x/=10;
    }
    LL ans=0;
    for (int i=1;i<=(ln*9);++i){
        memset(f,-1,sizeof(f));
        ans+=dfs(i,ln,0,0,1);
    }
    return ans;
}
int main(){
    scanf("%lld%lld",&l,&r);
    printf("%lld
",calc(r)-calc(l-1));
    return 0;
}

CF908G New Year and Original Order

给$n<=10^{700}$
,问1到n中每个数在各数位排序后得到的数的和。


考虑到原问题很难做,我们转换一下问题
举个例子:假设第$i$位填$3$,那么他的贡献的$3*10^i$,考虑把这个贡献拆开
$3ge3,3ge2,3ge1$我们在这一位填$1,2,3$时都记上$10^i$的贡献
那么我们就只要记这一位填大于等于某个数的方案数就好了
考虑按照套路设$dp[i][j][k][l]$表示前$i$位有$j$位的数字大小大于等于$k$,是否严格小于$n$的方案数
枚举第$i+1$位填$p$
$f[i+1][j+(p>=k)][k][l|(p<a_{i+1})]=sum f[i][j][k][l]$
然后实际上假设前$n$位我们$j$位数字大于等于$k$的方案数是$sum=f[n][j][k][0]+f[n][j][k][1]$

这个对答案的贡献的是$sum*underbrace{111ldots11}_{j ext{个}1}$

#include <cstdio>
#include <algorithm>
#include <cstring>
#define P 1000000007
using namespace std;
char st[1000];
int loop[1000],a[1000],dp[705][705][10][2];
void Add(int &x,int y){
    x=x+y;
    x-=(x>=P)?P:0;
}
int main(){
    scanf("%s",st+1);
    int n=strlen(st+1);
    loop[1]=1;
    for (int i=2;i<=(n+1);++i) 
        loop[i]=(10ll*loop[i-1]+1)%P;
    for (int i=1;i<=n;++i) 
        a[i]=st[i]-'0';
    for (int i=0;i<=9;++i) 
        dp[0][0][i][0]=1;
    for (int i=0;i<n;++i)
    for (int j=0;j<=i;++j)
    for (int k=1;k<=9;++k){
        int l=0;
        for (int p=0;p<=a[i+1];++p)
            Add(dp[i+1][j+(p>=k)][k][l|(p<a[i+1])],dp[i][j][k][l]);
        l=1;
        for (int p=0;p<=9;++p)
            Add(dp[i+1][j+(p>=k)][k][l|(p<a[i+1])],dp[i][j][k][l]);
    }
    int ans=0;
    for (int j=1;j<=9;++j)
        for (int i=1;i<=n;++i)
            Add(ans,(1LL*dp[n][i][j][0]*loop[i])%P),
            Add(ans,(1LL*dp[n][i][j][1]*loop[i])%P);
    printf("%d
",ans);
    return 0;
} 

P3413 SAC#1 - 萌数

辣鸡蒟蒻SOL是一个傻逼,他居然觉得数很萌!

好在在他眼里,并不是所有数都是萌的。只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。

现在SOL想知道从l到r的所有整数中有多少个萌数。

由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。


只需对例如101,11的回文数列分开讨论即可,其余此题与之前类似

#include <cstdio>
#include <algorithm>
#include <cstring>
#define P 1000000007
using namespace std;
typedef long long LL;
const int N=1005;
int dp[N][10][10][2];
int a[N];
char aa[N],bb[N];
LL dfs(int len,int last,int last_last,bool yes,bool shangjie,bool qian0){
    if (len==0) return yes;
    if (last!=-1&&last_last!=-1&&shangjie==0&&qian0==0&&dp[len][last][last_last][yes]!=-1)
        return dp[len][last][last_last][yes];
    int maxn=shangjie?a[len]:9;
    LL ans=0;
    for (int i=0;i<=maxn;++i)
        ans=(ans+ dfs( len-1,i,qian0?-1:last,
            yes||( (i==last&&qian0==0)||(i==last_last&&qian0==0) ),
            shangjie&&(i==maxn),qian0&&(i==0) ) )%P;
    if (last!=-1&&last_last!=-1&&shangjie==0&&qian0==0) 
        dp[len][last][last_last][yes]=ans;
    return ans;
}
LL calc(char *s,bool flag){
    int len=strlen(s+1);
    for (int i=len;i;--i) a[len-i+1]=s[i]-'0';
    if (flag){
        --a[1];
        int i=1;
        while (a[i]<0)
            a[i]+=10,a[i+1]--,++i;
    }
    return dfs(len,-1,-1,0,1,1);
}
int main(){
    memset(dp,-1,sizeof(dp));
    scanf("%s",aa+1);
    scanf("%s",bb+1);
    printf("%lld
",((calc(bb,0)-calc(aa,1))%P+P)%P);
    return 0;
}
原文地址:https://www.cnblogs.com/zhouykblog/p/10594479.html