• 【ACM】不要62 (数位DP)


    题目:http://acm.acmcoder.com/showproblem.php?pid=2089

    杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
    杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
    不吉利的数字为所有含有4或62的号码。例如:
    62315 73418 88914
    都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
    你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。

    思路:写了一个蛮力算法,直接超时了。之后各种想不出来,上网搜答案。结果发现有专门的解法,叫数位DP。之后看答案看了2个小时,那50行代码翻来覆去看了好久,终于看明白了。

    唉,大神们写代码的时候注释都太精简了,像我这种没学过数位DP的看得很痛苦啊。

    下面解析一下:

    题目会给出两个数字 m 和 n,我们要找到 【m, n】区间内,不含4与62的数字的个数。

    ①我们把问题拆解为两个部分, 分别求0 ~ m - 1 和 0 ~ n 之间的不含4与62的数字的个数,然后相减。

    ②但是0~n中的不含4与62的值求解也很复杂,所以我们先进一步化简,求0到 i 位数的不含4和62的数字个数。

    比如:

           i = 1,即求 0 ~ 9 中不含4和62的数字个数

           i = 2,即求 0 ~ 99 中不含4和62的数字的个数

           i = 3,即求 0 ~ 999 中不含4和62的数字个数

           i = 4,即求 0 ~ 9999 中不含4和62的数字的个数

             ..... 以此类推

    用dp[i][0] 来存储 0 到 i 位数字中不含4和62的数字个数,即幸运数

    用dp[i][1] 来存储 0 到 i 位数字中以 2 开头的幸运数。

    用dp[i][2] 来存储 0 到 i 位数字中的非幸运数,即包含4或者62的数字。

    那么,可以用下面的递推公式

          dp[i][0] = dp[i - 1][0] * 9 - dp[i - 1][1]   // i 位数字中的幸运数个数 = (i - 1)位幸运数字前面加上0 - 9 中除去4以外的9个数字 - 以2开头的(i - 1)位幸运数字前面加上了6.

         dp[i][1] = dp[i - 1][0]   // 0到 i 位数字中以2开头的幸运数 = 0到 i 位数字中所有的幸运数字前面加上2

         dp[i][2] = dp[i - 1][2] * 10 + dp[i - 1][0] + dp[i - 1][1]  //0到 i 位的非吉利数 = 0到 i - 1 位的非吉利数前面加上0-9的任何数字 + i-1位的吉利数字前面加上了4 + i-1位以2开头的吉利数字前面加上了6.

    初始值:  dp[0][0] = 1  dp[0][1] = dp[0][2] = 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] * 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] * 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] * digit[4]

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

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

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

      首先:加上0 ~ 99中所有非吉利数字前面添加0~5的任意一个数字的情况 sum += dp[2][2] * 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] * digit[2]

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

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

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

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

          sum += digit[1] * dp[0][2] + digit[1] * dp[0][0]

    总结一下,这部分的算法如下:

    int flag=0,ans=0;
        for(int i=cnt;i>0;i--){
            ans+=digit[i]*dp[i-1][2];   //由上位所有非吉利数推导
            if(flag)     //之前出现非吉利的数字
                ans+=digit[i]*dp[i-1][0];
            else{
                if(digit[i]>4)   //出现4
                    ans+=dp[i-1][0];
                if(digit[i]>6)   //出现6
                    ans+=dp[i-1][1];
                if(digit[i+1]==6 && digit[i]>2)  //出现62
                    ans+=dp[i][1];
            }
            if(digit[i]==4 || (digit[i+1]==6 && digit[i]==2))
                flag=1;
        }

    整体的代码如下:

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    
    using namespace std;
    
    int dp[10][3];
    
    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];   //在不含不吉利数62和4的首位分别补除了4的9个数字,减去在2前面补6的个数
            dp[i][1]=dp[i-1][0];        //在不含不吉利数在首位补2
            dp[i][2]=dp[i-1][2]*10+dp[i-1][0]+dp[i-1][1];   //各种出现不吉利数的情况
        }
    }
    
    int Solve(int x){
        int digit[15];
        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>0;i--){
            ans+=digit[i]*dp[i-1][2];   //由上位所有非吉利数推导
            if(flag)     //之前出现非吉利的数字
                ans+=digit[i]*dp[i-1][0];
            else{
                if(digit[i]>4)   //出现4
                    ans+=dp[i-1][0];
                if(digit[i]>6)   //出现6
                    ans+=dp[i-1][1];
                if(digit[i+1]==6 && digit[i]>2)  //出现62
                    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(~scanf("%d%d",&a,&b)){
            if(a==0 && b==0)
                break;
            printf("%d
    ",Solve(b+1)-Solve(a));
        }
        return 0;
    }

    网上有更简洁的代码,用dfs和状态转移做的,我没看懂。

    http://blog.csdn.net/dgq8211/article/details/9296953

    #include <stdio.h>
    #include <string.h>
    #include <algorithm>
    
    using namespace std;
    
    int dp[8][2],digit[8];
    
    int dfs(int len,bool state,bool fp)
    {
        if(!len)
            return 1;
        if(!fp && dp[len][state] != -1)
            return dp[len][state];
        int ret = 0 , fpmax = fp ? digit[len] : 9;
        for(int i=0;i<=fpmax;i++)
        {
            if(i == 4 || state && i == 2)
                continue;
            ret += dfs(len-1,i == 6,fp && i == fpmax);
        }
        if(!fp)
            dp[len][state] = ret;
        return ret;
    }
    
    int f(int n)
    {
        int len = 0;
        while(n)
        {
            digit[++len] = n % 10;
            n /= 10;
        }
        return dfs(len,false,true);
    }
    
    int main()
    {
        int a,b;
        memset(dp,-1,sizeof(dp));
        while(scanf("%d%d",&a,&b),a||b)
        {
            printf("%d
    ",f(b)-f(a-1));
        }
        return 0;
    }

    第三种可以通过的方法是暴力打表,这个比较简单

    http://www.cnblogs.com/zqxLonely/p/4092259.html

    #include <stdio.h> 
    #include <string.h>
    
    int flag[1000001];
    
    int main(){
        int n;
        int m;
        int i;
        int temp;
        int amount;
        
        memset(flag,0,sizeof(int)*1000001);
        for(i=1;i<=1000000;i++){
            temp=i;
            while(temp){
                if(temp%10==4 || temp%100==62){
                    flag[i]=1;
                    break;
                }
                temp/=10;
            }
        }
        
        while(1){
            scanf("%d%d",&n,&m);
            
            if(n==0 && m==0)    
                break;
            
            amount=0;    
            for(i=n;i<=m;i++){
                if(flag[i]==1)
                    amount++;
            }
            
            printf("%d
    ",m-n+1-amount);
        }
    
        return 0;
    }
  • 相关阅读:
    《ucore lab1 exercise2》实验报告
    《ucore lab1 exercise1》实验报告
    《Tsinghua os mooc》第21~22讲 文件系统
    《Tsinghua os mooc》第17~20讲 同步互斥、信号量、管程、死锁
    《Tsinghua os mooc》第15~16讲 处理机调度
    《Tsinghua os mooc》第11~14讲 进程和线程
    《Tsinghua oc mooc》第8~10讲 虚拟内存管理
    bzoj3188 [Coci 2011]Upit(分块)
    P4514 上帝造题的七分钟(二维树状数组)
    bzoj3156 防御准备(斜率优化)
  • 原文地址:https://www.cnblogs.com/dplearning/p/4719375.html
Copyright © 2020-2023  润新知