1. 程式人生 > >HDU 2089 暴力or兩種思路的數位DP

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-1][k](k=0~9 但不包括4),另外當j等於6時,不應加上dp[i-1][2],否則就是62開頭了。

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;
}