HDU 2089 暴力or兩種思路的數位DP
不要62
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 38443 Accepted Submission(s): 13958
Problem Description 杭州人稱那些傻乎乎粘嗒嗒的人為62(音:laoer)。
杭州交通管理局經常會擴充一些的士車牌照,新近出來一個好訊息,以後上牌照,不再含有不吉利的數字了,這樣一來,就可以消除個別的士司機和乘客的心理障礙,更安全地服務大眾。
不吉利的數字為所有含有4或62的號碼。例如:
62315 73418 88914
都屬於不吉利號碼。但是,61152雖然含有6和2,但不是62連號,所以不屬於不吉利數字之列。
你的任務是,對於每次給出的一個牌照區間號,推斷出交管局今次又要實際上給多少輛新的士車上牌照了。
Input 輸入的都是整數對n、m(0<n≤m<1000000),如果遇到都是0的整數對,則輸入結束。
Output 對於每個整數對,輸出一個不含有不吉利數字的統計個數,該數值佔一行位置。
Sample Input 1 100 0 0
Sample Output 80
Author qianneng
在紙上模擬了一上午,終於通過這道題感受到了數位DP的強大。
總之就是通過記憶化的過程遞推。
思路一:
比如一個不含有不吉利數字的4位數(下面稱其為合法數),其後三位也一定是一個合法數。
我們用dp[i][j]儲存以j開頭,位數為i的合法數的數量,則:
dp[1][0] = 1;(0)
dp[1][1] = 1;(1)
dp[1][2] = 1;(2)
dp[1][3] = 1;(3)
dp[1][4] = 0;(4不合法,所以個數為0)
dp[1][5]
= 1;(5)
dp[1][6]
= 1;(6)
dp[1][7]
= 1;(7)
dp[1][8]
= 1;(8)
dp[1][9] = 1; (9)
下面來看位數為2的數
dp[2][0] = 9(00,01,02,03,05,06,07,08,09)
dp[2][1] = 9 (10, 11, 12, 13, 15, 16, 17, 18, 19)
......
dp[2][6] = 8 (特殊,因為62,64均不合法)
.......
dp[2][9] = 9
觀察可知,dp[i][j]的3值,當j不為6時,就是 dp[i-1][0] + dp[i-1][1] +dp[i-1][2] +......+dp[i-1][9] (比如求1開頭的合法兩位數,那就是在所有的合法一位數前加個1)
易得狀態轉移方程:
dp[i][j] =∑
dp[i][j]得到了,那麼怎麼得到小於一個數的所有合法數呢?
我們以623舉例:
首先這是一個3位數,而且首位為6,那麼以0,1,2,3,4,5開頭的三位合法數一定小於它,則
num(小於623的合法數) = dp[3][5]+ dp[3][4] + dp[3][3]+ dp[3][2] dp[3][1]+ dp[3][0] + ....
然而我們發現有一些以6開頭的三位數如 611 也滿足,但並沒有加上,所以我們接下來應該加上 600<= x < 623的所有合法數。
易知 所有x的首位都是6,那我們只需要得到 00<= y < 23的所有合法數,前面再加個6即可。
首先 首位為 0,1的二位合法數,前面加個6 是一定合法的,即 num += dp[2][0] + dp[2][1];
然後我們發現以2開頭的二位合法數,前面加個6就產生了62,然後可以直接去掉。
終上所述:
num(小於623的合法數) = dp[3][5]+ dp[3][4] + dp[3][3]+ dp[3][2] dp[3][1]+ dp[3][0] + dp[2][0]
+ dp[2][1];
然後我們可以歸納 當要求一個小於三位數n的所有合法數num, 其公式為:(為方便理解,我們設n為xyz,即623為x=6,y=2,z=3)
num = dp[3][0] + dp[3][1] + ... + dp[3][x-1] + dp[2][0]+dp[2][1]+...+dp[2][y-1] + dp[1][0]+dp[1][1]+....+dp[1][z-1];
而上面那個例子因為前兩位為62已經非法,無論個位是什麼都非法,所以直接丟掉了最後一個部分。
這就是數位DP的思想:
#include <bits/stdc++.h>
using namespace std;
int dp[8][10];
void init(void){
memset(dp,0,sizeof(dp));
dp[0][0] = 1;
int i,j,k;
for(i=1 ;i<8 ;i++){
for(j=0 ;j<10 ;j++){
for(k=0 ;k<10 ;k++)
if(j!=4 && !(j==6 && k == 2))
dp[i][j] += dp[i-1][k];
}
}
}
int solve(int x){
int len = 0,digit[10];
while(x){
digit[++len] = x % 10;
x /= 10;
}
digit[len+1] = 0;
int i,j,ans = 0;
for(i=len ;i>0 ;i--){
for(j=0 ;j<digit[i] ;j++){
if(j != 4 && !(digit[i+1] == 6 && j == 2))
ans += dp[i][j];
}
if(digit[i] == 4 || (digit[i+1] == 6 && digit[i] == 2))
break;
}
return ans;
}
int main(){
init();
int n,m;
while(scanf("%d%d",&n,&m) != EOF && (m||n)){
cout << solve(m+1) - solve(n) << endl;
}
return 0;
}
思路二:
我們還可以從反面思考。要求合法的數,如果我們能用DP的方法求出非法的數,總數 - 非法的數 = 合法的數。
所以我們可以設
dp[i][0] 長度為i的合法數
dp[i][1] 長度為i,最高位為2的合法數
dp[i][2] 長度為i的非法數
然後易得狀態轉移方程:
dp[i][0] = dp[i-1][0]*9 - dp[i-1][1]
(每一個合法數前加上0~9不包括4的9個數,仍為合法數,但注意若合法數的首位為2,那麼我們需要減去其前一位加上6的情況)
dp[i][1] = dp[i-1][0]
(每一個合法數前加2 ,就成了一個長度加1,首位為2的合法數)
dp[i][2] = dp[i-1][2]*10 + dp[i-1][1] + dp[i-1][0]
(每一個非法數前加上0~9的十個數字仍非法) (每一個合法數前加4也非法) (每一個首位為2的合法數前加6便成為非法)
然後初始化DP之後,我們便得到了不同下標對應不同值得dp陣列。
如何求小於n的合法數個數num呢
我們還是以n = 623進行舉例。
我們先求小於n的非法數個數 ans。
首先:ans=0(初始化)。
623為三位數,那麼以0~5開頭的任意三位非法數都小於它,所以我們先加上這一部分:
即: ans += dp[2][2]*6 (所有的兩位非法數前面加上0~5)
然後類似於思路一,ans再加上所有滿足600<= x < 623 的非法數 x 的數目,然後就是我們要的值。
這時要分成兩部分討論:
一. 若求出滿足 0<= y < 23的非法數y的數目,則其前面加上一個6,仍為非法數。
二.若求出滿足 0<= y < 23的合法數y的數目,若 y 的首位為2,前面加上6,其也為非法數。
對於上述的第一部分,又需要分成兩部分:
(1). 長度為1的非法數,其前面加上0,1,則變成了一個小於23的兩位非法數。
(2). 長度為1的合法數,其前面加上4或者本身為2前加上6,使其變成非法數。(對於此例明顯不存在這種合法數,但我們仍然需要有這個討論的思想來解決一般性的例子)
對於上述的第一部分,又需要分成兩部分:
.........
由此可見,這是一個遞迴的過程,所以我們可以用dfs 或者迴圈來實現,具體細節可見下面程式碼:(迴圈版本)
#include <bits/stdc++.h>
using namespace std;
int dp[10][3];
//dp[i][0] 長度為i的合法數
//dp[i][1] 長度為i,最高位為2的合法數
//dp[i][2] 長度為i的非法數
void init(void){
int i;
memset(dp,0,sizeof(dp));
dp[0][0] = 1;
for(i=1 ;i<7 ;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){
int len = 0,digit[10];
int tem = x;
while(x){
digit[++len] = x%10;
x /= 10;
}
digit[len+1] = 0;
int ans = 0,i;
bool flag = false;
for(i=len ;i>0 ;i--){
ans += dp[i-1][2] * digit[i];
if(flag){
ans += dp[i-1][0] * digit[i];
}
if(!flag && digit[i]>4)
ans += dp[i-1][0];
if(!flag && digit[i+1] == 6 && digit[i]>2)
ans += dp[i][1];
if(!flag && digit[i]>6)
ans += dp[i-1][1];
if(digit[i] == 4 || (digit[i+1] == 6 && digit[i] == 2)){
flag = true;
}
}
return (tem - ans);
}
int main(){
int n,m;
init();
while(scanf("%d%d",&n,&m) != EOF && (n||m)){
cout << solve(m+1) - solve(n) << endl;
}
return 0;
}
另外就是可以直接用dfs來記憶化搜尋:
#include <bits/stdc++.h>
using namespace std;
int dp[10][2],digit[10];
int dfs(int len,bool pre_6,bool limit){
if(len == 0)
return 1;
if(!limit && dp[len][pre_6] >= 0)
return dp[len][pre_6];
int i,ans = 0,num = (limit?digit[len]:9);
for(i=0 ;i<=num ;i++){
if(i==4 || (pre_6 && i==2))
continue;
ans += dfs(len-1,i==6,limit && i == num);
}
return (limit?ans:dp[len][pre_6] = ans);
}
int solve(int x){
int len = 0,i;
while(x){
digit[++len] = x%10;
x /= 10;
}
return dfs(len,false,true);
}
int main(){
int n,m;
memset(dp,-1,sizeof(dp));
while(scanf("%d%d",&n,&m) != EOF && (n||m)){
printf("%d\n",solve(m) - solve(n-1));
}
return 0;
}
如果是初學者,不能理解DP的思想也沒關係,暴力也能過,但如果資料範圍再大一點,比如m,n都是10^15左右,暴力就會超時,數位DP才是真正的正解。
暴力:
#include<stdio.h>
int f(int x);
int main(){
int i,sum=0,a[1000000];
for(i=1 ;i<1000000 ;i++){
if(f(i)){
sum++;
}
a[i]=sum;
}
int m,n,cnt;
while(scanf("%d%d",&m,&n) != EOF && (m||n)){
cnt = (n-m+1) - (a[n] - a[m-1]);
printf("%d\n",cnt);
}
return 0;
}
int f(int x){
while(x){
if(x % 10 == 4) return 1;
if(x %100 ==62) return 1;
x /=10;
}
return 0;
}