首先我们要清楚数位dp解决的是什么问题:
求出在给定区间 [A,B] 内,符合条件 f(i) 的数 i 的个数。条件 f(i) 一般与数的大小无关,而与数的组成有关。
这里我们使用记忆化搜索实现数位dp。本质上记搜其实就是dp,下文会重点介绍dp值的使用和记录
记搜过程
从起点向下搜索,到最底层得到方案数,一层一层向上返回答案并累加,最后从搜索起点得到最终答案。
对于 [l,r] 区间问题,我们一般把他转化为两次数位dp,即找 [0,r] 和 [0,l-1] 两段,再将结果相减就得到了我们需要的 [l,r]
【细节分析】
一、前导0标记lead
由于我们要搜的数可能很长,所以我们的直接最高位搜起
举个例子:假如我们要从 [0,1000] 找任意相邻两数相等的数
显然 111,222,888 等等是符合题意的数
但是我们发现右端点 1000 是四位数
因此我们搜索的起点是 0000 ,而三位数的记录都是 0111,0222,0888 等等
而这种情况下如果我们直接找相邻位相等则 0000 符合题意而 0111,0222,0888 都不符合题意了
所以我们要加一个前导0标记
如果当前位 lead=1 而且当前位也是0,那么当前位也是前导0, pos+1 继续搜;
如果当前位 lead=1 但当前位不是0,则本位作为当前数的最高位, pos+1 继续搜;(注意这次根据题意st或其他参数可能发生变化)
当然前导 0 有时候是不需要判断的,上述的例子是一个有关数字结构上的性质,0会影响数字的结构,所以必须判断前导0;而如果我们研究的是数字的组成(例如这个数字有多少个 1 之类的问题),0并不影响我们的判断,这样就不需要前导0标记了。总之,这个因题而异,并不是必须要标记(当然记了肯定是不会出错的)
二、最高位标记limit
我们知道在搜索的数位搜索范围可能发生变化;
举个例子:我们在搜索 [0,555] 的数时,显然最高位搜索范围是 0 ~ 5 ,而后面的位数的取值范围会根据上一位发生变化:
当最高位是 1 ~ 4 时,第二位取值为 [0,9] ;
当最高位是 5 时,第二位取值为 [0,5] (再往上取就超出右端点范围了)
为了分清这两种情况,我们引入了 limit 标记:
若当前位 limit=1 而且已经取到了能取到的最高位时,下一位 limit=1 ;
若当前位 limit=1 但是没有取到能取到的最高位时,下一位 limit=0 ;
若当前位 limit=0 时,下一位 limit=0 。
我们设这一位的标记为 limit ,这一位能取到的最大值为 res ,则下一位的标记就是 i==res && limit (i 枚举这一位填的数)
例题:求a~b中不包含49的数的个数. 0 < a、b < 2 * 10^9, 求a-b中不包含49的数的个数. 0 < a、b < 2*10^9, 当数据范围特别大时, 暴力求解是没有办法做的,这里考虑用dp方法来解决。
此题有无前导零对答案无影响,所以不需要判断
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
//问:在区间[a,b]中不含49的数字有多少个
int a, b, shu[20], dp[20][2]; //shu数组存储各位上的数 dp[i][1] 表示首位为4,剩余数字长度为i的数字的个数
// dp[i][0] 意思是首位为除4外任意一个数字时,剩余数字长度为i的数字的个数。不是所有不为4的数剩余数字长度为i的数字的个数之和 如dp[1][0]=9,dp[1][1]=8
int dfs(int len,bool if4,bool limit) //len 遍历的每一位(...千,百,十,个),len=1表示个位.len=2表示十位... if4 此位的前一位是否为4, shangxian, 此位数是否有上限, 例如前两位是1, 2, 第三位个位有上限7
{ //然后开始在len位上遍历数字
if (len==0)
return 1;
if (!limit&&dp[len][if4]!=0) //记忆化搜索这里可以直接返回结果,
return dp[len][if4]; //如对数字127,当前两位为1,0和 1,1 的时候由于最后一位没有limit,故结果相同, 但与1,2情况不同,因为为1,2时第三位有limit了
int cnt=0,maxx;
if(limit)
maxx=shu[len]; //有上限的话最大取shu[len]的值
else
maxx=9; //没上限的话最大取9
for(int i=0;i<=maxx;i++) //上限记录到了maxx中,从0开始依次取
{
if(if4&&i==9)。
continue; //如果上一位值为4且当前位值为9,直接进入下一循环
cnt+=dfs(len-1,i==4,limit&&i==maxx); //只有 当前有限制 且 现在已经达到了上限(maxx)才能构成下一位有限制
} //由于在这个循环中len没有变,只是前一位数字变了,所以在进入下一个dfs函数,如果没有限制则返回值与之前得到的就相同,dp[len][if4]。
if(!limit)
dp[len][if4]=cnt; //如果有限制,那么就不能记忆化,否则记忆的是个错误的数.例如前两位1, 2, 由于第三位有上限7不能保存这个结果, 而1, 0, 1, 1 结果相同, 无上限, 记忆化保留结果
return cnt;
}
int solve(int x)
{
memset(shu,0,sizeof(shu));
int k=0;
while(x)
{
shu[++k]=x%10; //保存a,b的数
x/=10;
}
return dfs(k,false,true);//从最高位开始
}
int main()
{
scanf("%d%d",&a,&b);
printf("%d\n",solve(b)-solve(a-1));
return 0;
}
例题:求1到n内,出现13并且能被13整除的数的个数。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int dp[15][15][2][2],n,shu[20];//dp[i][j][k][l]表示 i是位,j是之前的余数 ,k是之前一位是否为1,l是是否已出现13
int dfs(int len,int rem,bool if1,bool occur13,bool limit)
{
if(len==0)
return rem==0&&occur13; //如果余数为0并且已经出现过13,那么就返回1
if(!limit&&dp[len][rem][if1][occur13]!=0)
return dp[len][rem][if1][occur13];
int maxx,cnt=0;
if(limit)
maxx=shu[len];
else
maxx=9;//通过有没有限制来判断这一位的最大取值
for(int i=0;i<=maxx;i++)
{
cnt+=dfs(len-1, (rem*10+i)%13, i==1, occur13||if1&&i==3, limit&&i==maxx);
} // 下一位, 判断余数, 判断前一位是否为1, 判断13是否已经出现过或者是在此出现,判断limit
if(!limit)
dp[len][rem][if1][occur13]=cnt;//没有限制的话就记忆性递归
return cnt;
}
int solve(int n)
{
int k=0;
while(n)
{
shu[++k]=n%10;
n/=10;
}
return dfs(k,0,false,false,true);
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
memset(dp,0,sizeof(dp));
memset(shu,0,sizeof(shu));
printf("%d\n",solve(n));
}
}