题目类型
数位DP题目往往呈现出如下题目特征:
给定一个闭区间[L,R], 求这个区间中满足某个条件的数的总数量
题目示例
某人命名了一种不降数, 这种数字必须满足从左到右各位数字呈非下降关系, 如123, 446
现在大家决定玩一个游戏, 指定一个整数闭区间[a,b], 问这个区间内有多少个不降数.
解决思路
1.转换范围
首先把统计[L,R]范围内满足条件的数字转化成统计[0,N]内满足条件的数字数量.则ans [L,R] = ans[0,R] - ans[0,L-1]
这样就把双边界改为了只需要考虑上边界
(有人这里使用1作为下界也可以,我更习惯于使用0)
2.将数字长度按最大边界补全前导0
这一步的功能在于建立统一数据格式,便于在算法中连续推进.这个应该比较好理解, 处理定长数据肯定比处理变长数据容易些.
3.从高位到低位枚举,建立初始dfs框架
枚举过程中,我们分别要记录
1.当前枚举到哪一位置 (这个不用过多解释)
2.前面一位的数字是多少 (前面一位数字, 决定了当前位置的最小值, 在这里是因为题目要求如此, 因此需要记录, 不同题目记录的可能有变化)
3.这一位可以填写哪些数字 (取决于前一位是否达到最大值, 如果取到最大值, 则只能填写大于等于前值 且 不超过最大值的范围内数字; 否则 可以取到大于等于前值 且 不超过9的范围内数字)
举个例子, 比如 123 这个数字, 当我们进行到十位时, 如果百位取的是0, 则十位可以取到[0,9]; 如果百位取的是1, 则十位可以取到[1,2] . 因为边界是123, 不能超过这个数字.
实际操作中呢, 因为前一位数字我们已经记录了, 因此用一个布尔值记录前一位是否取到最大值, 来作为选择依据
一个简单的dfs示例如下:
int dfs(int cur,int pre,bool flag){
//如果超过数位size, 说明已经找到一个答案, 则返回1
if(cur>=m){
return 1;
}
int res = 0;
for(int i=pre;i<=9;i++){ //因为不小于前一位,所以从前一位数字开始循环
if(flag){ // 如果前一位取到最大值
if(i>max_nums[cur]){ //当前选择超过最大值,则直接跳出
break;
}
res += dfs(cur+1,i,flag&&(i==max_nums[cur])); //向下一数位跳动
}
else{
res += dfs(cur+1,i,false); //前一位未取到最大值,则随意蹦跶
}
}
return res;
}
4.dp概念引入
dp的主要思想是空间换时间, 利用存储一些计算过的数据, 节省后续重复时间
因此在这里, 我们引入记忆化存储空间, 记录dp[cur][pre]的结果,表示到达cur位置, 前一位置取pre的结果数.
为什么是这样使用, 而不是像传统dp那样使用呢,因为数位dp的使用有限制条件,在前一位未取到最大值时可以用. 因此需要加以判断, 这里是通过dfs记忆化搜索的形式便于理解.
int dfs(int cur,int pre,bool flag){
if(cur>=m){
return 1;
}
int res = 0;
//新引入内容,若前一位未取到最大值,且之前计算过,则直接返回
if(!flag&&dp[cur][pre]!=-1)
return dp[cur][pre];
for(int i=pre;i<=9;i++){
if(flag){
if(i>max_nums[cur]){
break;
}
res += dfs(cur+1,i,flag&&(i==max_nums[cur]));
}
else{
res += dfs(cur+1,i,false);
}
}
//记忆化存储
if(!flag)
dp[cur][pre] = res;
return res;
}
这里需要重点理解一下,为什么是这样写
5.初始状态的设置
示例输入 1, 19 进行计算
int m = 2;
vector<int> max_nums={0,0};
vector<vector<int>> dp(m,vector<int>(10,-1));
int lres = dfs(0,0,true);
max_nums={1,9};
int rres = dfs(0,0,true);
return rres-lres;
注意, 初始状态一定是true, 因为-1位的最大值就是0, 这样后续有效开展计算.
代码和分析是自己写的, 如果有错误也请及时指出, 大家互相交流.
最后附一个别人写的, 更接近与传统dp写法的版本, 大家综合学习吧
点击这里跳转