HDU 3555 Bomb 数位dp入门模板题 dfs形式详解

Bomb

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)
Total Submission(s): 30437    Accepted Submission(s): 11649


Problem Description
The counter-terrorists found a time bomb in the dust. But this time the terrorists improve on the time bomb. The number sequence of the time bomb counts from 1 to N. If the current number sequence includes the sub-sequence "49", the power of the blast would add one point.
Now the counter-terrorist knows the number N. They want to know the final points of the power. Can you help them?
 

Input
The first line of input consists of an integer T (1 <= T <= 10000), indicating the number of test cases. For each test case, there will be an integer N (1 <= N <= 2^63-1) as the description.

The input terminates by end of file marker.
 

Output
For each test case, output an integer indicating the final points of the power.
 

Sample Input
  
  
3 1 50 500
 

Sample Output
  
  
0 1 15
Hint
From 1 to 500, the numbers that include the sub-sequence "49" are "49","149","249","349","449","490","491","492","493","494","495","496","497","498","499", so the answer is 15.
 

Author
fatboy_cw@WHU
 

题目大意

给定数字49和N,让我们求一下在 [ 0 , N ] 区间内的所有数字中,有多少个数字中带有“49”,如149 249等都是符合条件的。


1、数位dp思路

数位dp有很多种考察形式,但是思路基本不变,都是在从0到N的区间中寻找符合条件的数字有几个。
所以数位dp的核心思想,就是分情况讨论。

我们假设要找在 0 到 abcdefg 中 2出现的次数(abcdefg分别为7位十进制数中的每一位),且当前我们已经枚举到了xxx2yyy(0 <= xxx2yyy <=abcdefg),则有如下情况:

第一种情况:
  • 0 <= xxx <= abc-1 也就是说xxx2yyy一定小于abcdefg,则yyy可以从0取到999,当前情况下答案为1000*abc
第二种情况:
  • xxx == abc 则如果d < 2 那么当前位是取不到2的,也就是说这种情况下符合条件的个数为0
  • xxx == abc 则如果d = 2 那么yyy只能从0到efg中取,则当前情况下符合条件个数为efg+1
  • xxx == abc 则如果d > 2那么yyy可以从0取到999,则当前情况下符合条件的个数为1000

以上便是数位dp的核心思想了,但是这个思想距离dfs的实现方法还有很大一段距离。

2、dfs枚举

首先我们可以肯定的是,枚举+判断0到N区间的每个数肯定会超时,但是我们不妨先来说一说如何使用dfs枚举。

令dfs每一层为最大区间的每一位数,假设我们需要枚举从0到abcdef中的每个数,且我们已经枚举到了xxxyyy,那么在枚举的时候会有两种情况:

第一种情况:
  • xxx < abc,那么剩下的每一位,我们都可以让他们从0到999进行枚举
  • 比如 123456,如果我们前三位已经枚举到了 110,那么后三位不管怎么取都会小于,这个数都小于123456,故需要从0到999进行枚举。
第二种情况
  • xxx == abc,那么剩下的每一位,我们都只能让他们从0枚举到def
  • 同上

那么仅针对枚举,我们就可以写出代码了

int bits[20];
int cnt = 0;
void dfs(int pos,bool limit){
	if (pos == -1)	return;
	int Max = limit?bits[pos]:9;		//根据前面的枚举数字判断当前位的枚举范围
	for(int i = 0; i <= Max; i++){
		dfs(pos-1, limit && i == Max)	//判断当前位及之前位枚举是否已经到了最大值
	}
}

void divide(long long n){				//保存每一位
	while(n){
		bits[cnt++] = n % 10;
		n /= 10;
	}
	dfs(cnt-1, true);
}

3、判断及记忆化

判断

现在我们可以枚举从0到N的每一位了,但是还无法判断当前的枚举情况是否符合条件。既然我们是从高位到低位进行的递归,那么我们是不是可以在递归的时候进行判断,并传递参数。我们在dfs枚举的基础上添加一个参数,让他来负责判断当前位之前的情况。

  • statment = 0 为当前位之前没有出现过49
  • statment = 1 为当前位的前一位为4
  • statment = 2 为当前位之前出现过完整的49

有了以上的参数,我们就可以判断当前枚举情况下是否出现过49了。

记忆化

一直没有提及的速度问题终于到来了,如果仅仅是以上方法的话,虽然是o(n)的时间复杂度,但n一般都会取到很大,所以还是会超时。我们可以使用记忆化的方式来进行优化。
不难发现,如果当前所处数位和状态是一样的,且后续枚举都是从0到9(即当前数位以前的枚举情况并未到达最大值),那么答案就是一样的。如果之前的枚举情况已经到达了最大值,那么后续的答案与就是一种特殊情况了,需要单独进行计算。故我们可以使用dp[20][3]数组来存储每一种位置和状态的答案,并用limit参数来判断当前数位是否可以使用之前计算的答案。


以下便是这道数位dp的dfs形式代码

int dp[20][3]
int bits[20];
int cnt = 0;

long long dfs(int pos, int statement, bool limit){
	if (pos == -1)	
		return statement == 2?1:0;
	if (!limit && dp[pos][statement] != -1) 	//记忆化
		return dp[pos][statement];
	int Max = limit?bits[pos]:9;		//根据前面的枚举数字判断当前位的枚举范围
	for(int i = 0; i <= Max; i++){
		if(statement == 2 || (statemen == 1 && i == 9)	//判断当前是否可以组成一个49
			dfs(pos-1, 2, limit && i == Max)	
		else if (i == 4)	
			dfs(pos-1, 1, limit && i == Max)
		else
			dfs(pos-1, 0, limit && i == Max)		
	}
	return dp[pos][statemen] = ans;
}

void divide(long long n){				//保存每一位
	while(n){
		bits[cnt++] = n % 10;
		n /= 10;
	}
	dfs(cnt-1, true);
}
写在最后

一开始学习数位dp的时候只学到了思想,没学到实现方法,后来查这道题的题解,发现都写的不是很明白,比如查到的题解都是在最后返回的时候要加一个if (!limit) dp[pos][statment] = ans; 当时是怎么想也想不出来这个东西的必要性,最后发现也确实是没必要,这才想着写这么一篇有可能详细过头了的数位dp模板,如果各位大佬觉得哪里写的不好请轻点喷…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值