题目描述:题目链接
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others)
Total Submission(s): 13265 Accepted Submission(s): 4765
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 15HintFrom 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.
解题思路:
接触到有数位dp这样的东西,就搜出这道题,说实话,并没有觉得这道题好做,下面好好分析下题目:
这道题要求出含有49子串的数字的个数,如果想去寻找规律,则会很容易陷入各种条件判断,稍有不慎就WA了,因此在接连碰壁后打算寻找其他做法,首先最容易想到的是暴力遍历,可是题目中N的大小达到了2^63-1的数量级,也就是10^18到10^19这样的数量级,然后每次循环都得判断这个数中是否含有49字串,而且这里面有很多重复判断,例如对于1000,当循环到了49和149时,都得重复判断低二位是否含有49,并且对于多测试样例,更加剧这种重复。
这时,就可以利用数位dp,也即是先不管具体上下限,先单纯考虑不同位数下,出现49的次数,用dp[i]来表示i位数能有多少个数字有49子串,但这样是不够的,因此考虑如下:
dp[i][0] 表示i位数不含有49的个数
dp[i][1] 表示i位数不含有49,且以9开头的个数(这样多加一位的时候,如果以4开头的话,则含有49)
dp[i][2] 表示i位数含有49的个数
则可以得到下面的状态转移方程:
dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1] 减去第i位为4,其余i-1位为以9开头的不含49的数的个数
dp[i][1] = dp[i-1][0] 等于第i位为9,其余i-1位是不含49的个数
dp[i][2] = dp[i-1][2] * 10 + dp[i-1][1] 加上第i位是4,其余i-1位是以9开头的不含49的数的个数
然后是初始条件:
dp[0][0] = 1 假设位数是0的不含49的个数为1个,
dp[0][1] = dp[0][2] = 0 位数为0的含49个数为0
这样初始化的话就能满足上面条件,
这样就可以很容易求出位数是20以内这三种情况的个数,接下来的问题就是把这个结果用到这道题中,这也是挺复杂的一件事:
可以从高位到低位扫描所给的数,例如:
第 i 位是 k (k为0到9的数),则 i 位的情况有0,1,2,....,k,首先考虑0,1,...,k-1的情况,共k种,则结果含有 k * dp[i-1][2] 个含49的个数,
然后考虑第 i 位是 k 的情况,假设 i 位是 k,也就是第 i 位固定下来,接下来只需要考虑余下的 i-1位的情况,
但这样考虑还有不足,当在第 i 位之前已经有出现49的时候,那么结果还需要加上余下 i-1不含有49的情况,因为整个数已经包含了49的情况,
如果第i 位之前没有出现 49,那么只要第 i 位的数字大于4,也就是 k > 4, 那么 k-1 >= 4,所以当第 i 位是4的时候,还可以加上以9开头的不含有49的情况。
但是。。。这样还是不够的。。。
考虑这样的情况,当最后两位恰好是49的时候,前面的考虑并没有考虑进去,但这并非简单的判断如果最后两位是49就加1这么简单,因为如果在第二位的前面出现过49,那么这种情况已经包含进去了,如果第二位的数字大于4,那么这种情况也包含进去了,为了避免复杂且重复的判断,这里直接将输入的N值加1。
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
long long dp[21][3];
void init(){
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for(int i = 1; i < 21; i++){
dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1];
dp[i][1] = dp[i-1][0];
dp[i][2] = dp[i-1][2]*10 + dp[i-1][1];
}
}
int main(){
//freopen("t.txt", "r", stdin);
init();
int t;
scanf("%d", &t);
long long n;
while(t--){
scanf("%lld", &n);
n++;
int numArray[22], len = 0;
memset(numArray, 0, sizeof(numArray));
while(n){
numArray[++len] = n % 10;
n = n / 10;
}
long long res = 0;
int pre = -1;
int flag = 0;
for(int i = len; i >= 1; i--){
res += dp[i-1][2] * numArray[i];
if(flag)
res += dp[i-1][0] * numArray[i];
else if(numArray[i] > 4)
res += dp[i-1][1];
if(pre == 4 && numArray[i] == 9)
flag = 1;
pre = numArray[i];
}
printf("%lld\n", res);
}
return 0;
}
不难看出,除了前面求dp数组外(这部分可以看做常数时间,虽然对于一些小数据,它可能会成为整个程序最耗时的地方,因为它把不需要用到的位也求出来了),其余部分的时间复杂度为O(n),这里的n指输入数据的位数!!所以说效率是很快的,在hdu中的运行时间为15MS。
下面介绍一种采用dfs 递归的方法,为了与上面的做对比,这里先说明下它的时间复杂度为O(n),注意,这里的n指输入的数据,在hdu上运行的时间为31MS,但自己用一些小数据测试却是比上面的做法快,原因是他避免了对不需要用到的位数进行求解的过程。
其中,dp[i][0]表示长度为i,不以9结尾(也就是新加入的高位值不能为4)的满足条件的数的个数,
dp[i][1]表示长度为i,以9结尾(也就是新加入的高位值为4)的满足条件的数的个数;(注:dp存的值都是在没有上下限约束条件下的,也即只考虑位数)
此外,程序最主要的部分是一个dfs函数:
long long dfs(int pos, bool preIs4, bool hasCeiling)
用来从高位到低位进行dfs,返回在输入的n的0到pos位能够表示满足条件的个数,其中preIs4表示在第pos+1位的数是否为4,hasCeiling表示比pos高位的数是否都已经到达n的对应位的值,也即比pos高的位是否已经遍历完成。
#include <cstdio>
#include <cstring>
using namespace std;
long long dp[22][2];
long long cnt[22] = {1};
int numArray[22];
long long n;
void init(){
memset(dp, -1, sizeof(dp));
for(int i = 1; i < 21; i++)
cnt[i] = cnt[i-1] * 10;
}
long long dfs(int pos, bool preIs4, bool hasCeiling){
if(pos == 0)
return 0;
if(!hasCeiling && dp[pos][preIs4] >= 0)
return dp[pos][preIs4];
int upperBound = hasCeiling ? numArray[pos] : 9;
long long res = 0;
for(int i = 0; i <= upperBound; i++){
if(preIs4 && i == 9)
res += hasCeiling ? (n % cnt[pos-1] + 1) : cnt[pos-1];
else
res += dfs(pos-1, i == 4, hasCeiling && i == numArray[pos]);
}
//注意:dp中保留的是在没有任何上下界约束条件下的值
if(!hasCeiling)
dp[pos][preIs4] = res;
return res;
}
int main(){
//freopen("t.txt", "r", stdin);
int t;
init();
scanf("%d", &t);
while(t--){
scanf("%lld", &n);
int len = 0;
long long tmp = n;
while(tmp){
numArray[++len] = tmp % 10;
tmp = tmp / 10;
}
printf("%lld\n", dfs(len, false, true));
}
}