• 数位dp入门(内容一样,新版格式)


    顾名思义,数位dp,是一种用来计数的dp,就是把一个数字拆成一个一个数位去统计

    如果现在给你一道题,需要你求在区间[l,r]内满足条件的解的个数,我们很容易想到去暴力枚举,但要是数据范围太大这种办法就行不通了,这时候数位dp就派上了用场,所谓数位就是把一个数拆成一个一个进制位,然后逐一比较看是否满足题目要求,这其实也是一种暴力方法,只不过时间复杂度小了很多

    那么到底要如何做呢?下面我们来看一道例题

    HDU2089

    概括一下题目意思

    就是给你一个区间[n,m],要你求区间内不含"62"或"4"的数字的个数,如8134(含4),21262455(含62)均不满足题意,而61342这种"6"和"2"并不连在一起的数字则满足题意

    直接统计对于暴力枚举很好求,但是对于数位dp并不容易,所以我们还需要用到差分的思想,即统计0到b+1(注意不是b,至于为什么后面会讲)和0到a的满足条件的个数,再两者相减

    进一步化简,求 (0)(i) 位数不含 (4)(62) 的个数

    (i=1),求 (0)~(9) 的满足条件的个数

    (i=2),求 (0)~(99) 的满足条件的个数

    (i=3),求 (0)~(999) 的满足条件的个数

    (i=4),求 (0)~(9999) 的满足条件的个数

    ...

    (dp[i][0]) 表示 (i) 位数中幸运数的个数

    (dp[i][1]) 表示 (i) 位数中以 (2) 开头的幸运数的个数

    (dp[i][2]) 表示 (i) 位数中非幸运数的个数

    那么,就有以下的递推公式


    (dp[i][0]=dp[i-1][0] imes9-dp[i-1][1])

    表示前i-1位数字中的幸运数前面加上除4以外的0~9的其他数字,共9个,还要减去前i-1位数字中的以2开头的幸运数加上这一位以6开头的数字的个数


    (dp[i][1]=dp[i-1][0])

    表示前i-1位数字中的幸运数加上这一位的2


    (dp[i][2]=dp[i-1][2] imes10+dp[i-1][1]+dp[i-1][0])

    表示前面已经不合法的数字这一位无论放什么都不合法,所以0~9随便放,加上前i-1位数字中的以2开头的幸运数加上这一位的6,再加上前i-1位数字中的幸运数加上这一位的4的个数


    初始值 (dp[0][0]=1),其他均为 (0)

    根据初始值和递推公式,我们就能得到从 (0) 到任意 (i) 位数字的吉利数字的个数。

    找到 (0) ~ (n) 的吉利数字的个数

    我们先求出 (0) ~ (n) 之间非吉利数字的个数,用总数减去即可。那,非吉利数字的个数怎么求呢?


    用具体的数字举例来说吧:设 (n = 583626)

    (digit[10]) 记录 (n+1)每一位对应的数字,此例中有 (6) 位数字(令 (cnt = 6) 表示数字位数),分别是

    	digit[6] = 5
    
    	digit[5] = 8
    
    	digit[4] = 3
    
    	digit[3] = 6
    
    	digit[2] = 2
    
    	digit[1] = 7
    
    	digit[0] = 任意数字,占位用的
    

    (sum) 记录非吉利数字的个数,初始化为 (0)

    需要一个(bool)(flag),记录是否出现了非吉利数字。初始化为 (false), 未出现。

    我们从数字的最高位起进行判断: (digit[6] = 5).

    我们要求 (0) ~ (499999) 之间非吉利数的个数。


      首先:加上(0) ~ (99999) 中所有非吉利数字前面添加0~4的任意一个数字的情况 (sum += dp[5][2] imes digit[6])

      其次:(5) 大于 (4),故我们要加上 (0) ~ (99999) 中所有吉利数字前面添加 (4) 的情况 (sum += dp[5][0])

    接着,判断第(5)(digit[5] = 8),即判断 (500000) ~ (579999) 之间的非吉利数字的个数,其实就是判断 (0) ~ (79999) 之间的,前面的数字不是(6)就没有什么用

      首先:加上(0) ~ (9999) 中所有非吉利数字前面添加 (0)~(7) 的任意一个数字的情况 (sum += dp[4][2] imes digit[5])

      其次:(8) 大于 (4),故我们要加上 (0)~(9999) 中所有吉利数字前面添加4的情况 (sum += dp[4][0])

      此外:(8) 大于 (6),故我们要加上 (0)~(9999) 中所有以(2)开头的吉利数字前添加(6)的情况 (sum += dp[4][1])

    接着,判断第 (4)(digit[4] = 3),即判断 (580000 ~ 582999) 之间的非吉利数字的个数,其实就是判断 (0) ~ (2999) 之间的

      首先:加上 (0) ~ (999)中所有非吉利数字前面添加 (0)~(2)的任意一个数字的情况 (sum += dp[3][2] imes digit[4])

      其次:(2) 小于 (4),没有需要特别考虑的

      此外:(2) 小于 (6),没有需要特别考虑的

    接着,判断第 (3)(digit[3] = 6),即判断 (583000) ~ (583599) 之间的非吉利数字的个数,其实就是判断(0) ~ (599)之间的

      首先:加上 (0) ~ (99) 中所有非吉利数字前面添加 (0)~(5) 的任意一个数字的情况 (sum += dp[2][2] imes digit[3])

      其次:(6) 大于 (4),故我们要加上 (0) ~ (99) 中所有吉利数字前面添加4的情况 (sum += dp[2][0])

    接着,判断第 (2)(digit[2] = 2),即判断 (583600 ~ 583619) 之间的非吉利数字的个数,其实就是判断0 ~ 19之间的,

      首先:加上 (0) ~ (9) 中所有非吉利数字前面添加 (0)~(1) 的任意一个数字的情况 (sum += dp[1][2] imes digit[2])

      其次:(2)小于(4),没有需要特别考虑的

      此外:(2)小于(6),没有需要特别考虑的


      但是,需要注意的是,这里判断的数字出现了 (62),我们要把(flag)标识为(true.)

    最后,判断第 (1)(digit[1] = 7), 判断 (583620 ~ 583626) 但是这里 (flag)(true) 了,表示前面的数字里面已经包含了非吉利数字,所以后面需要把所有的数字情况都加入到非吉利里面。(正是因为每次判断的数字末尾都比该位的数字少1,所以最开始要记录 (n + 1) 的值)

    (sum += digit[1] imes dp[0][2] + digit[1] imes dp[0][0])

    Code

    #include<bits/stdc++.h>
    #define in(i) (i=read())
    using namespace std;
    int read() {
        int ans=0,f=1; char i=getchar();
        while(i<'0'||i>'9') {if(i=='-') f=-1; i=getchar();}
        while(i>='0'&&i<='9') {ans=(ans<<3)+(ans<<1)+i-'0'; i=getchar();}
        return ans*f;
    }
    int dp[10][3],digit[15];
    void init() {
        memset(dp,0,sizeof(dp));
        dp[0][0]=1;
        for(int i=1;i<=8;i++) {
            dp[i][0]=dp[i-1][0]*9-dp[i-1][1];
            dp[i][1]=dp[i-1][0];
            dp[i][2]=dp[i-1][2]*10+dp[i-1][1]+dp[i-1][0];
        }
    }
    int solve(int x)
    {
        memset(digit,0,sizeof(digit));
        int cnt=0,tmp=x;
        while(tmp) {
            digit[++cnt]=tmp%10;
            tmp/=10;
        }
        digit[cnt+1]=0; int flag=0,ans=0;
        for(int i=cnt;i>=1;i--) {
            ans+=digit[i]*dp[i-1][2];
            if(flag) ans+=digit[i]*dp[i-1][0];
            else {
                if(digit[i]>4) ans+=dp[i-1][0];
                if(digit[i]>6) ans+=dp[i-1][1];
                if(digit[i+1]==6 && digit[i]>2) ans+=dp[i][1];
            }
            if(digit[i]==4 || (digit[i+1]==6 && digit[i]==2)) flag=1;
        }
        return x-ans;
    }
    int main()
    {
        int a,b; init();
        while(1) {
            in(a);in(b);
            if(!a && !b)  break;
            cout<<solve(b+1)-solve(a)<<endl;
        }
        return 0;
    }
    

    最后说那个 (b+1) 的情况,我们看到代码中有判断 (digit[i]>4)(digit[i]>6) 等类似的语句,我们处理第 (i)位时,实际上是处理 (0~digit[i]-1),即 ([ 0,digit[i] )) ,而把 (digit[i]) 放到下一次去判断,但我们处理个位时,最后一个是不会去统计的,所以我们把统计的范围(+1),即为([ 0,digit[i]+1 )Rightarrow[ 0,digit[i] ]),所以就有了 (solve(b+1)-solve(a)) 这样的语句.(还是高二 (dalao) Navi-Awson告诉我的,(\%\%\%))

    数位(dp)记忆化搜索写法

    Code

    #include<bits/stdc++.h>
    #define in(i) (i=read())
    using namespace std;
    typedef long long lol;
    lol read() {
        lol ans=0,f=1;    
        char i=getchar();
        while(i<'0'|| i>'9') {if(i=='-') f=-1; i=getchar();}
        while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
        return ans*f;
    }
    lol a,b;
    lol dp[19][11];
    lol bit[19];
    lol dfs(lol len,lol pre,lol limit) {
        if(!len) return 1;//如果搜到最后一位了,返回下界值1
        if(!limit && dp[len][pre]) return dp[len][pre];//记忆化部分
        lol maxn=limit?bit[len]:9;//求出最高可以枚举到哪个数字
        lol ans=0;
        for(lol i=0;i<=maxn;i++) {
            if(i!=4 && !(pre && i==2))//如果这一位不为4并且上一位不为6且这一位不为2
                ans+=dfs(len-1,i==6,limit && i==maxn);//满足条件
        }
        if(!limit) dp[len][pre]=ans;//如果没有限制,代表搜满了,可以记忆化,否则就不能
        return ans;
    }
    lol solve(lol a) {
        memset(bit,0,sizeof(bit));
        lol k=0;
        while(a) {//取出数字的每一位
            bit[++k]=a%10;
            a/=10;
        }
        return dfs(k,0,1);
    }
    int main()
    {
        //freopen("number.in","r",stdin);
        //freopen("number.out","w",stdout);
        lol t; in(t);
        for(int i=1;i<=t;i++) {
            lol a,b; in(a);in(b);
            cout<<solve(b)-solve(a-1)<<endl;//差分思想
        }
        return 0;
    }
    

    应用

    放几道题目上来

    [SCOI2009]windy数 代码

    [HNOI2010]计数

    [ZJOI2010]数字计数 题解

  • 相关阅读:
    Thread.currentThread().getName() ,对象实例.getName() 和 this.getName()区别
    CentOS7.7 yum安装新版git
    CentOS使用epel安装不同版本php-fpm
    ubuntu16.04安装mysql5.6
    阿里云Confluence无法发送邮件修复
    windowserver 2012安装openssh
    linux增加history时间戳
    SQL Server 2008R2各个版本,如何查看是否激活,剩余可用日期?
    nginx增加访问验证
    mysql5.6和5.7的权限密码设置
  • 原文地址:https://www.cnblogs.com/real-l/p/11852453.html
Copyright © 2020-2023  润新知