数位DP详解

时间:2023-03-10 05:44:35
数位DP详解

算法使用范围

在一个区间里面求有多少个满足题目所给的约束条件的数,约束条件必须与数自身的属性有关

下面用kuangbin数位dp的题来介绍

例题  不要62

题意:在一个区间里面求出有多少个不含4和62的数

做法:平常我们的做法肯定是从L枚举到R,然后数位上一个一个的判断,但是如果是范围过大的话我们不可以预处理存储,只能每次去枚举,这样肯定会超时

所以我们使用数位DP 记忆化搜索,我们dfs去枚举每一位

数位部分

例如 2567

因为千位是2的时候我的百位只能从0-5,1的时候百位却是可以0-9,所以这点我们需要判断

第一位   0-2

第二位   第一位==2?5:9

第三位   第一位==5&&第二位==6?6:9

第四位   第一位==5&&第二位==6&&第三位==6&&第四位==7?7:9

上面也就是数位的过程,数位我们就省去了每一个数去拆分查询的复杂度,大概是复杂度/10

dfs参数

虽然参数比较灵活,但是有几个固定的参数,做多了数位DP的题也会觉得这个参数其实是一个套路了

第一个参数

从上述的的枚举,我们可以看出我们首先要把这个数拆分到数组里面,然后dfs每位每位的枚举

ans 代表的是当前枚举到(个,十,百,千...)位了

第二个参数

从上述来看,每到一位我都需要知道前面是不是上边界的值,从而来确定我的当前位的枚举范围

flag 代表的数前一位是否还是处于上边界

第三个参数

iszero 用来判断前导0,有些题目会有前导零的要求,比如说求二进制的数位的时候他前面

第四个参数

issix 比如说这题的话,那么我就要判断前面一位是否是6,那么当前位是2的话那么我就可以忽略了,这个参数要灵活变通

dp部分

dp的主要目的就是尽量把大部分重复问题的东西通过巧妙的存储保留下来,下次可以直接访问,大大减少复杂度

dp[ans][issix]

数组的用途解析(注意!!!)

ans代表剩余的位数,issix代表是否是6

注意的是我们必须保存的是和数的属性相关的东西,比如求数不包括62的,我们求1-1000和1-10000以内和数自己都是没关系的,他每次依然会被计数

现在我知道 1-1000的62的个数 说明这是前面是没有6的情况

我要求42000-50000之间有多少个

我们就会发现其实42000-43000其实和1-1000其实是一样的,43000-50000也是一样的道理,所以我们可以用,这是前缀没有6的情况 dp[ans][0]

我要求60000-63000之间有多少个

因为前面有6了,所以我们这个我们最开始用不到1-1000内62的个数这个条件,我们跑出60000-61000的结果时

我们就相当于知道了dp[ans][1]的值,后面61000-63000我们都可以使用

因为有前面是6和不是6的情况,所以我们列要开两种情况

列标这里的作用比较灵活

比如我们要求这个数的数位和能否被10整除

那么我们就要开10,分别对应前缀%10对后面的影响

总之,前缀提供啥,后面要根据前缀的情况来给出不同情况的值,前缀必须没有后效性,前缀不用知道后缀就可以满足情况

小优化

有些题它可能是多组输入,我们就可以把memset(dp,-1,sizeof(dp))放多组输入外面

回到开始说的必须是数的属性,永远不会变,所以我们计算后的数组可以保留到下一组数据使用

当然如果是把上列说的%10改成输入%q那就不行了

#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[][];
int a[];
int sum=;
int dfs(int len,bool issix,bool flag)//len代表当前枚举的位,issix代表前面是否是6,flag代表是否还是处于上边界
{
if(len==-) return ;//依题意来决定是1还是0,这题只是计算没有62的,所以我们直接返回1,如果题目是数位和是否%10==0就不一样了
int up=flag?a[len]:;//决定你枚举的上界是多少
if(!flag&&dp[len][issix]!=-) return dp[len][issix];//如果不是处于上边界并且之前求过了值的话可以直接返回
int tmp=;
for(int i=;i<=up;i++)
{
if(i==) continue;
if(issix&&i==) continue;
tmp+=dfs(len-,i==,flag&&i==a[len]);//上次是上边界,如果这次也是的话,那么下一位也是
}
if(!flag) dp[len][issix]=tmp;//求了值用dp存下,下次可以使用
return tmp;
}
int suan(int x)
{
int pos=;
while(x)
{
a[pos++]=x%;
x/=;
}
return dfs(pos-,,true);
}
int main()
{
int l,r;
memset(dp,-,sizeof(dp));
while(scanf("%d%d",&l,&r)!=EOF)
{
if(l==&&r==) break;
printf("%d\n",suan(r)-suan(l-));//利用前缀和来求固定区间的个数
}
}

这里讲一下为什么都是!flag的时候来进行操作

首先看这句

if(!flag&&dp[len][issix]!=-1) return dp[len][issix];
因为如果我现在知道了1-1000的个数
我现在要求2100-2500
那么求到2???的时候需不需要返回呢,答案是不行,
因为你返回的值里面还包括了 2500-3000的值,所以需要加!flag
再看这句
if(!flag) dp[len][issix]=tmp;
同样如果我求了到了2000-2500的值的时候我需不要把他当1-1000的值进行存储呢
也不行,因为你烧了2500-3000这一段没算
最后再推荐一篇大牛的博客
https://blog.****.net/jk211766/article/details/81474632