題目: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; }