Bomb
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others)Total Submission(s): 30437 Accepted Submission(s): 11649
Now the counter-terrorist knows the number N. They want to know the final points of the power. Can you help them?
The input terminates by end of file marker.
3 1 50 500
0 1 15
题目大意
给定数字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模板,如果各位大佬觉得哪里写的不好请轻点喷…