问题:求某个区间[L,R]中满足某种限制条件的数的个数。
暴力的思路:我们可以枚举[L,R]区间内部所有的数,依次判断每个数是否合法,若合法,就记下。
显然,如果数据范围较大,达到1018甚至更大的级别的时候,这种暴力的算法无论时间还是空间都无法
解决此类问题,所以我们引入数位DP。
所谓数位DP,就是换一种枚举的方式,这种枚举的方式是基于数位(个位,百位,千位...)进行的,与
数的大小没有任何关系。
一般记忆化搜索版本要比数组递推版好实现。
从一道题来引入
(HDU2089)不要62
题意:给定区间,求区间内部不含连续的62和4的数。
数据范围 l,r<=10000000;
我们考虑从最高位开始一位一位的进行枚举,运用记忆化搜索,如果枚举到最后一位所有位都合法,我们就找到了一个数。
思考,前一位什么情况会对后一位的枚举产生影响:
1,这位的前一位是6,这一位枚举了2,那么后面再枚举的情况都不再合法,我们去掉这种情况
2,这一位枚举到4,后面枚举的情况都不合法,我们去掉这种情况。
3,这一位枚举到了范围的最高位,那么下一位枚举的范围不能是0-9,而只能是0-这位的边界。
*在数位DP的记忆化搜索中,记忆化的是穷举0-9的情况,最高位单独枚举并不会产生太大的复杂度。
*数位DP运用到了前缀和的思想,我们统计1-R的答案,在统计1-L-1的答案,相减即为答案。
代码实现来入门数位DP
首先,我们要了解每一位在受到限制的情况下(即前一位枚举到最高位时)枚举的最高位。因此我们要把区间右端点拆分。
1 int pos=0;
2 while(k)
3 {
4 num[++pos]=k%10;
5 k/=10;
6 }
然后就到了数位DP的模板精华部分。
1 inline int dfs(int pos,int pre,int sta,bool limit)//pos:位数,pre:前驱,sta前一位是否是6,若不是则可以记忆化,否则因
2 //这一位2不能取而不能记忆化 limit:判断当前位的枚举上限
3 {
4 if(pos==0) return 1;//表示找到了最低位仍合法证明找到了一个数
5 if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];//记忆化不是上限的0-9过程
6 int up,sum=0;
7 if(!limit) up=9; else up=num[pos];//判断这位的枚举上限
8 for(int i=0;i<=up;i++)//枚举这一位的所有可能
9 {
10 if(i==4) continue;//去掉4的情况
11 if(pre==6&&i==2) continue;//去掉前驱是6,这位是2的情况
12 sum+=dfs(pos-1,i,i==6,limit&&(i==num[pos]));//搜
13 }
14 if(!limit) dp[pos][sta]=sum;
15 return sum;
16 }
类似的题还有windy数对于入门可以练习下。
HDU2089完整代码
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[20][2],num[20];
inline int dfs(int pos,int pre,int sta,bool limit)
{
if(pos==0) return 1;
if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
int up,sum=0;
if(!limit) up=9; else up=num[pos];
for(int i=0;i<=up;i++)
{
if(i==4) continue;
if(pre==6&&i==2) continue;
sum+=dfs(pos-1,i,i==6,limit&&(i==num[pos]));
}
if(!limit) dp[pos][sta]=sum;
return sum;
}
inline int solve(int k)
{
int pos=0;
while(k)
{
num[++pos]=k%10;
k/=10;
}
return dfs(pos,0,0,true);
}
int main()
{
int m,n;
memset(dp,-1,sizeof(dp));
while(1)
{
scanf("%d%d",&m,&n);
if(m==0&&n==0) return 0;
printf("%d\n",solve(n)-solve(m-1));
}
return 0;
}