• 浅谈数位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;
    }
  • 相关阅读:
    重构第四天 : 用多态替换条件语句(if else & switch)
    MSBuild 教程(2)
    为什么Nhibernate中属性和方法必须Virtual的
    重构第三天:提升方法&下移方法
    重构第二天:移动方法
    重构第一天:封装集合
    MSbuild 教程
    工程经验总结之吹水"管理大境界"
    呕心沥血之作:完美解决Informix的中文乱码问题
    万事开头难——我的蛮荒时代
  • 原文地址:https://www.cnblogs.com/zhouykblog/p/10594479.html
Copyright © 2020-2023  润新知