数位DP

首先我们要清楚数位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));
	}
} 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

henulmh

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

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

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

打赏作者

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

抵扣说明:

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

余额充值