携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情
数位dp是一类比较特殊的问题,模板性很强,主要解决区间计数问题。一般情况下题目会询问区间[L,R]中满足某种性质的数有多少个。
首先以HDU3555 Bomb为例,题意大概是寻找1-n中包含"49"的数字个数,例如249、4900等都是符合题意的数字。考虑暴搜枚举每一个数,时间复杂度大概是O(log10(n)*n),显然会超时。但是如果按位暴搜,枚举每一位可能出现的数字,时间复杂度大概是O(10^(log10(n))),比上一个暴搜快了不少,但依旧会超时。以n=2345为例,再来考虑按位枚举过程,首先令第一位为0,之后几位不确定,此时的数字为0???(用?代替不确定的位置),另外由于'0'小于n的第一位'2',后面3位都可以任取每一个数(0-9)且不会导致超出最大范围。于是我们暴搜出来此时的情况总数:0049、0149、0249......0490、0491......0499这20个数,当我们第一位枚举到1,此时的数字为1???,由于'1'还是小于'2',后3位依然可以任取,于是我们暴搜出来此时的情况总数:1049、1149、1249......1490、1491......1499这20个数,与刚才搜索到的数仅第一位不同,于是可以记录下当前状态能获得的合法方案数,保存在一个数组里,当下次遇到相同的状态时直接返回答案即可。一般前导零标志可以放入dp数组,且会加速记忆化搜索,但任选标志一定不能放入dp数组!
具体代码如下(可看作模板理解记忆):
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
//本模板以HDU3555 Bomb为例
int dp[70][10][2];
//dp[i][j][1]记录第i位之前数字已经确定(不包含第i位)且上一位数字为j且已搜到49,之后的数字可以任取(第i位也可任取)得到的合法数字个数
//dp[i][j][0]记录第i位之前数字已经确定(不包含第i位)且上一位数字为j且未搜到49,之后的数字可以任取(第i位也可任取)得到的合法数字个数
int a[70];
int dfs(int pos, int pre, int flag1, int flag2)//本质就是个记忆化+暴搜
{
//pos表示本次处理的数位
//pre记录上一位数值
//flag1一般与题目具体要求有关,需要灵活设置,这里表示之前是否搜到过49
//flag2表示该位是否可以任选,弥补前三维不能准确描述状态的缺陷,也可以加入dp数组作为第四维
if(pos == 0)//如果到达递归边界
return flag1;//这里根据题目要求灵活改变
if(flag2 && dp[pos][pre][flag1] != -1)//如果当前位不受限制且已经记录了答案
return dp[pos][pre][flag1];//直接返回答案
//开始本次计数
int up = flag2 ? 9 : a[pos];//当前位置能达到的上界
int ans = 0;//记录计数结果
for(int i = 0; i <= up; i++)//枚举该位取值
ans+=dfs(pos-1, i, (pre==4&&i==9)||flag1, i<up||flag2);//flag1需要灵活改变,其余变量的改变基本是模板
if(flag2)//只有满足该位任取才能记录答案,否则会被不能任取时的答案刷新,可能影响之后返回的答案
//例如输入12345,处理到02_??时可以任选,直接返回dp[3][2][0],但处理到120??时如果再对dp赋值就改变了dp[3][2][0]
//虽然对本组数据不会有影响,但计算下组数据可能会出现错误
dp[pos][pre][flag1] = ans;
return ans;
}
int solve(int x)
{
int pos=0;
do
{
a[++pos]=x%10;
x/=10;
}while(x);//拆位,用do_while循环是考虑到x为0的情况
return dfs(pos,0,0,0);
}
signed main()
{
int T;
cin >> T;
memset(dp, -1, sizeof dp);//初始化一次,之后多组数据都可以利用dp记录的数据
while(T--)
{
int n;
cin >> n;
cout << solve(n) << endl;
}
return 0;
}
相关例题:
-
Classy Numbers CF1036C
要求得到[L,R]区间内非零位置小于等于3的数字个数,可以处理出[1,n]合法数字个数,用类似前缀和思想得到[L,R]区间内合法数字个数,另外需要注意flag1记录之前找到的非'0'的个数。
-
不要62 HDU2089
要求得到[L,R]区间内不包含"62"且不含'4'的数字个数,类似上面的例题。
-
B-number HDU3652
要求得到[1,n]区间内包含"13"且是13的倍数的数字个数,考虑dp数组多开一维记录到当前位置的余数,并作为dfs的参数,在dfs过程中动态更新(为什么这样做自行思考)。