zoukankan      html  css  js  c++  java
  • re:从零开始的数位dp

    起源:唔,,前几天打cf,edu50那场被C题虐了,决定学学数位dp。(此文持续更新至9.19)

    ps:我也什么都不会遇到一些胡话大家不要喷我啊。。。

    数位dp问题:就是求在区间l到r上满足规定条件的数的个数。

    ex1:hdu3555

      题意:给你n,求从一到n中有多少个数不包含“49”。(t<=1e4,n<=2^63-1)

      首先数位dp顾名思义就是对数位进行dp嘛,所以dp数组的第一维我们用来保存数字的位数,第二位我们用来判定当前位是否为4,

    所以就是  dp[20][2];  这个样子。 在这之前我们先考虑一下常规的搜索思路,一件非常显然的事情,在[1,1000]和[1001,2000]以及所有类似区间里符合要求的数都是一样的,这样我们就可以通过记忆化的方式来保存某些结果。

    先给出solve函数

    1 ll solve(ll num){
    2     int k = 0;//记录数位
    3     while(num){
    4         k++;
    5         digit[k]=num%10;
    6         num/=10;
    7     }
    8     return dfs(k,false,true);
    9 }
    View Code

    这个很好理解嘛,保存这个数各位上的数字。然后我们就可以进行记忆化搜索了,

    dp[20][2]:表示 1.有4的时候有几个含有49, 2.没有4的时候,有几个含有49。

    ll dfs(int len,bool if4,bool limit){
    //当前是第几位,上一位是否是4,上一位是否是上界
    if(len==0)//统计完了直接返回1
    return 1;
    if(!limit&&dp[len][if4])//不是上界并且这种情况已经统计过
    return dp[len][if4];
    ll cnt=0,up_bound=(limit?digit[len]:9);//up_bound是当前位能满足的最大值,如果上一位是上界的话,当前位最大只能取到当前位的数字,如果不是,当前位可以从0取到9
    for(int i=0;i<=up_bound;i++){
    if(if4&&i==9)
    continue;//上一位是4并且这一位是9,GG了啊
    cnt+=dfs(len-1,i==4,limit&&i==up_bound);//上一位是上界的情况下我们才会考虑这一位是否是上界
    }
    if(!limit)//不是上界,属于通用的情况,我们进行赋值
    dp[len][if4]=cnt;
    return cnt;
    }
    最后结果差分一下就好。

    ex2:hdu2089
    和上道题几乎一样,条件是没有“4”并且没有“62”,
    这时候我们掏出上一道题的板子了嘛肯定要,只需要在判断时加入一句话就行,在代码中加上注释了,就不做多解释了
    #include <bits/stdc++.h>
    using namespace std;
    int n,m;
    int digit[10];
    int dp[10][2];
    int dfs(int len,bool if6, bool limit){
        if(len==0)
            return 1;
        if(!limit&&dp[len][if6])
            return dp[len][if6];
        int cnt = 0,up_bound = (limit?digit[len]:9);
        for(int i=0;i<=up_bound;i++){
            if(i==4)//如果遇到四就直接GG
                continue;
            if(if6&&i==2)
                continue;
            cnt+=dfs(len-1,i==6,limit&&i==up_bound);
        }
        if(!limit)
            dp[len][if6]=cnt;
        return cnt;
    }
    int solve(int num){
        int k = 0;//记录数位
        while(num){
            k++;
            digit[k]=num%10;
            num/=10;
        }
        return dfs(k,false,true);
    }
    
    int main(){
        while (scanf("%d%d",&n,&m)&&(n+m)) {
            cout << solve(m) - solve(n - 1) << endl;
        }
    }
    View Code

      

      ex3:codeforces1036C,也就是EDU50的C题嘛,一道非常简单的板子题,(问题是我当时还没听说过数位dp,,真的,,不然就可以骑学长了,,哭)

      条件是 零的个数不大于3个。

      很显然我们只要把dp数组的第二维开到3就可以了嘛,,然后就是套板子,, 我这里把0的情况单独拿出来了,因为1到9都可以一起考虑嘛

      

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    int digit[20];
    ll dp[20][4];
    ll dfs(int len,int not0,bool limit){
        if(len==0)//完了
            return 1;
        if(!limit&&dp[len][not0])//已经统计过
            return dp[len][not0];//
        ll cnt=0,up_bound=(limit?digit[len]:9);//
        cnt+=dfs(len-1,not0,limit&&digit[len]==0);//
        for(int i=1;i<=up_bound;i++){
            if(not0==3)
                continue;
            cnt+=dfs(len-1,not0+1,limit&&i==up_bound);
        }
        if(!limit)
            dp[len][not0]=cnt;
        return cnt;
    }
    ll solve(ll num){
        int k = 0;//记录数位
        while(num){
            k++;
            digit[k]=num%10;
            num/=10;
        }
        return dfs(k,0,true);
    }
    int t;ll l,r;
    int main(){
        ios::sync_with_stdio(false);
        cin>>t;
        while (t--){
            cin>>l>>r;
            cout<<(solve(r)-solve(l-1))<<endl;
        }
    }
    View Code

    ex4:

      hdu3652

      条件:包括“13”并且能被13整除

      首先我们想到用余数来分类嘛,然后结合最初的板子,开出来的dp数组就是这样的 dp[17][17][3];//分别是数位,余数,对于“13”的三种状态(包括1,包括13,啥都没有)

      还有取模的那个地方需要稍微理解一下,剩下的也就是板子了

      

     1 #include <bits/stdc++.h>
     2 using namespace std;
     3 int digit[17];
     4 int dp[17][17][3];
     5 int dfs(int len,int mod,int have,int limit){
     6     if(len==0)
     7         return mod==0&&have==2;
     8     if(!limit&&dp[len][mod][have])
     9         return dp[len][mod][have];
    10     int cnt = 0,up_bound=(limit?digit[len]:9);
    11     for(int i=0;i<=up_bound;i++){
    12         int mod_ = (mod*10+i)%13;
    13         int tmp = have;
    14         if(have==0&&i==1)
    15             tmp = 1;
    16         if(have==1&&i!=1)
    17             tmp = 0;
    18         if(have==1&&i==3)
    19             tmp = 2;
    20         cnt+=dfs(len-1,mod_,tmp,limit&&i==up_bound);
    21     }
    22     if(!limit)
    23         dp[len][mod][have] = cnt;
    24     return cnt;
    25 }
    26 
    27 int solve(int num){
    28     int k = 0;
    29     while (num){
    30         k++;
    31         digit[k] = num%10;
    32         num/=10;
    33     }
    34     return dfs(k,0,0,1);
    35 }
    36 int n;
    37 int main(){
    38     ios::sync_with_stdio(false);
    39     while (scanf("%d",&n)!=EOF){
    40 //    scanf("%d",&n);
    41         cout<<solve(n)<<endl;
    42     }
    43 }
    View Code

    ex5: codeforces 55 d,  是上一道题略微进化版   条件“这个数能被他的每个非零位的最小公倍数整除”;

    首先我们还是想到对模数来分类,这是非常显然的嘛,除此之外,我们需要保存“最小公倍数”,因为转移的时候必须要用到之前的,这样子我们开出来的数组是 dp[20][2520][2520],好恭喜你MLE on test 1,这时就需要一些奇淫技巧。我们会发现,lcm的个数非常有限吧,实际上只有48个,所以我们可以离散化嘛。然后就与上一题非常相似了,我们保存 “presum”和“prelcm”,进行记忆化搜索即可。

    为什么可以%2520呢,

    • 首先我们能够知道如果这个数能够整除它的每个数位上的数字,那么它一定能够整除他们的最小公倍数,是充要的。
    • 那么我们定义状态dp[i][j][k]代表i位在任意组合下,得到的所有数位的数字的最小公倍数为j,且该数%2520为k的方案数。
    • 我们可以知道任意多个1-9之间的数的公倍数最大不会超过2520,而且他们都是2520的约数,所以(一个数%2520)能够被该数所有数位的数字的最小公倍数整除,那么该数就能整除自己每个数位上的数字。
    #include <cstdio>
    using namespace std;
    typedef long long ll;
    int t;ll l,r;
    const int mod = 2520;
    ll dp[20][mod][50];
    int ind[mod+5];
    int digit[20];
    void init(){
        int cnt = 0;
        for(int i=1;i<=mod;i++){
            if(mod%i==0)
                ind[i]=cnt++;
        }
    }
    
    int gcd(int a, int b){
        return b==0?a:gcd(b,a%b);
    }
    
    int lcm(int a, int b){
        return a/gcd(a,b)*b;
    }
    ll dfs(int len,int presum,int prelcm,bool limit){
        if(len==0)
            return presum%prelcm==0;
        if(!limit&&dp[len][presum][ind[prelcm]])
            return dp[len][presum][ind[prelcm]];
        ll cnt = 0;int up_bound = limit?digit[len]:9;
        for(int i=0;i<=up_bound;i++){
            int nowsum = (presum*10+i)%mod;
            int nowlcm = prelcm;
            if(i!=0)
                nowlcm = lcm(nowlcm,i);
            cnt+=dfs(len-1,nowsum,nowlcm,limit&&i==up_bound);
        }
        if(!limit)
            dp[len][presum][ind[prelcm]] = cnt;
        return cnt;
    }
    ll solve(ll num){
        int k = 0;
        while (num){
            k++;
            digit[k]=num%10;
            num/=10;
        }
        return dfs(k,0,1,1);
    }
    
    int main(){
        init();
        scanf("%d",&t);
        while (t--){
            scanf("%I64d%I64d",&l,&r);
            printf("%I64d
    ",solve(r)-solve(l-1));
        }
    }
    View Code

    ex6:poj 3286  求区间里包含多少个零

    我随手一写竟然过了。网上题解貌似没看到和我的写法一样的。所以讲的详细些,还是套板子,dfs里的四个参数分别表示 位数,零的个数,是否前导零,是否上界

    然后就很简单了嘛。注意到l可以取到0,然后我们的dfs对于零来说是没有计算在内的,所以要加上1。

    #include <iostream>
    using namespace std;
    typedef long long ll;
    int digit[20];
    int dp[20][20];
    ll l,r;
    ll dfs(int len,int count,bool zero,bool limit){
        if(len==0)
            return count;
        if(!zero&&!limit&&dp[len][count])
            return dp[len][count];
        ll cnt=0;int up_bound=limit?digit[len]:9;
        cnt+=dfs(len-1,count+(zero?0:1),zero,limit&&digit[len]==0);
        for(int i=1;i<=up_bound;i++){
            cnt+=dfs(len-1,count,false,limit&&i==up_bound);
        }
        if(!limit&&!zero)
            dp[len][count]=cnt;
        return cnt;
    }
    ll solve(ll num){
        int k = 0;
        while (num){
            k++;
            digit[k]=num%10;
            num/=10;
        }
        return dfs(k,0,1,1);
    }
    int main(){
        ios::sync_with_stdio(false);
        while (1) {
            cin >> l >> r;
            if(l==-1||r==-1)
                return 0;
            if (l == 0)
                cout << solve(r) + 1 << endl;
            else
                cout << solve(r) - solve(l - 1) << endl;
        }
    }
    View Code

    ex7:poj2282&&bzoj1833&&luogu2602&&zjoi1010

    求区间里包含“0,1,2,3,4,5,6,7,8,9”的个数分别是多少

    和ex6一毛一样嘛,,一个很直观的思路就是我们把ex6跑十遍吧,嗯正解就是这样(囍的不行),我觉着求“1,2,3,4,5,6,7,8,9”还要比“0”简单咯,不用考虑前导0,然后把代码稍微一改就可以了。哦对了!他这个鬼输入竟然l还能大于r,所以要判断swap一下,,,我一开始测样例输出了一堆负数让我受到了很大的惊吓。。。

      

    #include <iostream>
    #include <cstring>
    using namespace std;
    typedef long long ll;
    int digit[10];
    int dp[10][10][10];
    int ans[10][2];
    ll l,r;
    ll dfs0(int len,int count,bool zero,bool limit){
        if(len==0)
            return count;
        if(!zero&&!limit&&dp[len][count][0])
            return dp[len][count][0];
        ll cnt=0;int up_bound=limit?digit[len]:9;
        cnt+=dfs0(len-1,count+(zero?0:1),zero,limit&&digit[len]==0);
        for(int i=1;i<=up_bound;i++){
            cnt+=dfs0(len-1,count,false,limit&&i==up_bound);
        }
        if(!limit&&!zero)
            dp[len][count][0]=cnt;
        return cnt;
    }
    ll dfs(int len,int count,int num,bool limit){
        if(len==0)
            return count;
        if(!limit&&dp[len][count][num])
            return dp[len][count][num];
        ll cnt = 0;int up_bound=limit?digit[len]:9;
        for(int i=0;i<=up_bound;i++){
            cnt+=dfs(len-1,count+(i==num),num,limit&&i==up_bound);
        }
        if(!limit)
            dp[len][count][num]=cnt;
        return cnt;
    }
    void solve(ll num,int ind){
        int k = 0;
        while (num){
            k++;
            digit[k]=num%10;
            num/=10;
        }
        ans[0][ind]=dfs0(k,0,1,1);
        for(int i=1;i<=9;i++)
            ans[i][ind]=dfs(k,0,i,true);
    }
    void init(){
        memset(dp,0, sizeof(dp));
        memset(digit,0, sizeof(digit));
    }
    int main(){
        ios::sync_with_stdio(false);
        while (cin>>l>>r&&l&&r) {
            if(l>r)
                swap(l,r);
            init();
            solve(r,0);
            init();
            solve(l-1,1);
            for(int i=0;i<10;i++)
                cout<<ans[i][0]-ans[i][1]<<" ";
            cout<<endl;
        }
    }
    View Code

    ex8:8102上海大都会赛J题 

    能被各位数字和整除 数据范围(N<=1e12)

      首先我们应该会求 “被1整除”,“被二整除”,“被三整除”这样子的吧,然后神妙的战法就出现了,我们可以枚举各位数字和,,,12*9=128,也就是跑100来遍就行了吧。。。

    初始化要初始化成-1,因为可能有很多状态本身就没有符合条件的数。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    int t;ll n;
    int digit[17];
    ll dp[17][120][120];//第i位,之前数位之和位j,对某个mod余数为k的满足条件的个数
    int nowsum;
    ll dfs(int len, int sum,int mod,bool limit){
        if(len==0)
            return (sum==nowsum&&mod==0);
        if(!limit&&dp[len][sum][mod]!=-1)
            return dp[len][sum][mod];
        ll cnt = 0;int up_bound=limit?digit[len]:9;
        for(int i=0;i<=up_bound;i++){
            if(sum+i>nowsum)
                break;
            cnt+=dfs(len-1,sum+i,(mod*10+i)%nowsum,limit&&i==digit[len]);
        }
        if(!limit)
            dp[len][sum][mod]=cnt;
        return cnt;
    }
    ll solve(ll num) {
        int k = 0;
        while (num) {
            k++;
            digit[k] = num % 10;
            num /= 10;
        }
        ll res = 0;
        for(int i=1;i<=9*k;i++) {
            nowsum = i;
            memset(dp,-1, sizeof(dp));
            res += dfs(k,0,0, true);
        }
        return res;
    }
    
    int main(){
        ios::sync_with_stdio(false);
        cin>>t;
        int cas = 0;
        while (t--){
            cin>>n;
            cout<<"Case "<<++cas<<": "<<solve(n)<<endl;
        }
        return 0;
    }
    View Code

       

  • 相关阅读:
    c 语言学习第一天
    react+antd 使用脚手架动态修改主题色
    用 vitePress 快速创建一个文档项目
    react懒加载,减少首屏加载时间
    react-devtools安装以及使用中的问题
    react为什么不用数组的下标来绑定key
    三种js缓存(笔记)
    关于浏览器自动记住登录帐号和密码后, 一些输入框会显示帐号密码信息
    H5 Hybrid开发-前端资源本地化方案纪要
    webpack + ts 配置路径别名无死角方法总结
  • 原文地址:https://www.cnblogs.com/MXang/p/9629526.html
Copyright © 2011-2022 走看看