【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\n",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\n",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\n",m-n+1-amount);
    }

    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM