数位DP(Bomb)

简单的说一下什么叫做数位DP,就是通过对数的每一位来进行递推来求得一个答案,这个答案按照传统的暴力枚举法是必TLE无疑的,下面就来看一道经典的题目

Bomb

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.

题意翻译:

给出一个数 n,求出1~n中所有带有49的数,比如说149就是一个合法的状态,419就是不合法的状态,n的范围是1 <= N <= 2^63-1

思路

就算是暴力枚举每个数的话,最坏的情况是18位,10*(1e18)一定超时,这个复杂度估计王校长的100w的电脑也不会在一天之内跑出来吧哈哈哈,现在的思路就是开一个数组
f[ i ][ j ][flag] 表示的是处理到第i位,前一个数是j,前面有没有出现“49”这个数字,后面的数能任意选的方案数,后面的数能任意选表示的是后面的数无论选什么都不会越过1~n这个界限,就是这里是用到了记忆化的思想,就是说如果这个数已经被更新过了,但是下一次还是枚举到了这个状态,那么就没有必要再算一遍了,现在举个栗子,我们的n是498873,然后现在咱们第一次处理到f[5][7][1],然后现在的状态是4907_,这个_就是咱们需要填数字的位置,然后现在这个f[5][7][1]这个状态是没有被更新的,没有被更新过的初始值都是-1,所以咱们算出来了这个状态的值是x,下一次当咱们回溯到第三层,循环进行了一次,这时候就是4917_了,现在咱们要求的状态是f[5][7][1],现在很明显这个状态的值已经不是-1了,而是x,现在咱们这个数是后面可以任意选的,因为前缀4917已经比n的前缀4988小了,所以后面再怎么选也不会比这个n大了,咱们是存了后面任意选时这个状态的值的,所以直接返回这个x就行,小小的一波剪枝,这个记忆化搜索就是这个算法的精髓,然后就枚举这个位数上的所有可能的数字,注意这个能不能任意选的问题,就是前面已经能任意选了,这个就选啥也行了,如果前面不能任意选了,这个地方只要取不到n在这个位置的数就行,大概具体的思路就是这样,自己的一些见解,下面请看代码

#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
LL f[30][10][2],a[200];
LL dp(int pos,int pre,int flag1,int flag2){
//pos代表的是下标,pre是前一个数字是什么,flag1为1代表前面已经出现了49,flag2为1代表能任意选

	if(pos == 0) return flag1;//因为咱们的下标是从1开始的,到了这里了已经枚举完了最后一个数,
							  //想一下他是从枚举最后一个数过来的,这个pre就是最后一个数,假如
							  //最后一个数0~9都符合,是不是这时候最后一个位数的dp应该返回的是10
							  //因为最后一个位数的ans都是通过加这个函数的返回值进行更新的,所以
							  //说当前面已经存在49的时候应该返回1,因为此时的数是合法的,当不
							  //存在49的时候应该返回0,这个时候flag1就要发挥作用了,没错!就是
							  //返回的flag1
							  
	if(f[pos][pre][flag1] != -1 && flag2) return f[pos][pre][flag1];
	//这个就是那个记忆化搜索的精华部分了
	LL ans = 0;
	int x = flag2?9:a[pos];//当这位数字能任意选的时候,他的上界就是最大值9,当这位数字不能任意选
						   //的时候,就是n在这个位置上的数
						 
	for(int i=0;i<=x;i++) ans += dp(pos-1,i,flag1 || (pre==4&&i==9),flag2 || i<x);
	//后面递归的部分:pos-1表示的是下一位,i是后一位数的前一个数,第三个变量就是以前有无49和现在
	//有无49或起来,最后能不能任意选就是如果以前的状态能任意选,选了这个数以后的所有情况都可以任意选
	//如果以前的状态不能任意选,那选完这个数后,如果这个数没到最大界限的话后面能任意选,如果到了界限
	//的话后面就不能任意选
	if(flag2) f[pos][pre][flag1] = ans;//因为后面的能任意选了,而且这个地方没有被更新过就把他更新了
	return ans;
}
LL solve(LL n){
	int tot = 0;
	while(n){//记录n的每一位数字是什么
		a[++tot] = n%10;
		n/=10;
	}
	return dp(tot,0,0,0);//初始值这个地方就根据具体含义设置,一般除了下标其他都是0
}
int main(){
	int t;
	cin>>t;
	while(t--){
		memset(f,-1,sizeof f);
		LL n;//这个地方一定要开long long,因为题目中说了范围
		cin>>n;
		cout<<solve(n)<<endl;
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇智波一打七~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值