题意:输出一个区间【m,n】内所有不含“62"and"4" 的数字个数;n<=10^7;
数位dp一般应用于:
求出在给定区间[A,B]内,符合条件P(i)的数i的个数.
条件P(i)一般与数的大小无关,而与 数的组成 有关.
具体的理论过程我就不解释了,我看了好多的文章,最后还是直接看代码注释看懂的;
所以 还是直接上代码注释:
方法1 枚举法:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int dp[10][10];//dp[i][j]表示第i位是数j时符合条件的数字数量
int d[10];//digit[i]表示n从右到左第i位是多少
void init()
{
dp[0][0] = 1;
for (int i = 1; i <= 7; ++i)//枚举第i位
for (int j = 0; j <= 9; ++j)//表示第i位是数j时符合条件的数字数量
for (int k = 0; k <= 9; ++k)//枚举第i-1位的情况
if (j != 4 && !(j == 6 && k == 2))///如果符合条件 !62 && !4
dp[i][j] += dp[i - 1][k];
}
int solve(int n)
{
int ans = 0;
int len = 0;
while (n) ///计算n的digit数组
{
d[++len] = n % 10;
n /= 10;
}
d[len + 1] = 0;//最高位 置零
//计算[0,n]区间满足条件的数字个数
for (int i = len; i >= 1; --i)///注意:从最高位开始枚举
{
for (int j = 0; j < d[i]; ++j) //枚举第i位的取值j
{
if (d[i + 1] != 6 || j != 2)//第i位取j满足条件
ans += dp[i][j];
}//第i位该状态已经不满足条件,则i位以后都不可能满足条件,结束循环
//例如 62...or 4...
if (d[i] == 4 || (d[i + 1] == 6 && d[i] == 2))
break;
}
return ans;
}
int main()
{
int m, n;
init();
while (scanf("%d%d", &m, &n) == 2)
{
if (n == 0 && m == 0) break;
printf("%d\n", solve(n + 1) - solve(m));///用[0,m]-[0,n)即可得到区间[n,m]
}
return 0;
}
二:状态记忆化法:
#include<bits/stdc++.h>
using namespace std;
int dp[20][2];//第i位的值是否为6
int d[20];//每个数位上的最大值
int dfs(int x,bool sta,bool lim)//x 位数 sta 前一位是否为6 前一位当前位置取值是否受到限制
{//lim 表示在第per位上是否有上限,即在该位的取值为0~9还是0~digit[per];
if(!x) return 1;//该状态 枚举完最后一位数 返回1;
if(!lim&&dp[x][sta]!=0) return dp[x][sta];//避免重复计算;
int len=lim?d[x]:9;//len表示在该位的最大值;
int ans=0;
for(int i=0;i<=len;i++)
{
if(i==4||sta&&i==2) continue;//当出现4,或者前一位为6,当前位为2时跳过不计;
ans+=dfs(x-1,i==6,lim&&i==d[x]/*当前是否是最高位 i 是否也是最高限制值*/);
} //i==6用来判断递归后前一位是否为6;
if(!lim) return dp[x][sta]=ans;//赋值
return ans;
}
int slove(int n)
{
int len=0;
while(n)
{
d[++len]=n%10;//计算数的每一位数;
n/=10;
}
return dfs(len,0,1);
}
int main()
{
int n,m;
memset(dp,0,sizeof dp);
while(~scanf("%d %d",&m,&n))
{
if((n+m)==0) break;
printf("%d\n",slove(n)-slove(m-1));
}
}
状态转移详解:
// pos = 当前处理的位置(一般从高位到低位)
// pre = 上一个位的数字(更高的那一位)
// status = 要达到的状态,如果为1则可以认为找到了答案,到时候用来返回,
// 给计数器+1。
// limit = 是否受限,也即当前处理这位能否随便取值。如567,当前处理6这位,
// 如果前面取的是4,则当前这位可以取0-9。如果前面取的5,那么当前
// 这位就不能随便取,不然会超出这个数的范围,所以如果前面取5的
// 话此时的limit=1,也就是说当前只可以取0-6。
//
// 用DP数组保存这三个状态是因为往后转移的时候会遇到很多重复的情况。
int dfs(int pos,int pre,int status,int limit)
{
//已结搜到尽头,返回"是否找到了答案"这个状态。
if(pos < 1)
return status;
//DP里保存的是完整的,也即不受限的答案,所以如果满足的话,可以直接返回。
if(!limit && DP[pos][pre][status] != -1)
return DP[pos][pre][status];
int end = limit ? DIG[pos] : 9;
int ret = 0;
//往下搜的状态表示的很巧妙,status用||是因为如果前面找到了答案那么后面
//还有没有答案都无所谓了。而limti用&&是因为只有前面受限、当前受限才能
//推出下一步也受限,比如567,如果是46X的情况,虽然6已经到尽头,但是后面的
//个位仍然可以随便取,因为百位没受限,所以如果个位要受限,那么前面必须是56。
//
//这里用"不要49"一题来做例子。
for(int i = 0;i <= end;i ++)
ret += dfs(pos - 1,i,status || (pre == 4 && i == 9),limit && (i == end));
//DP里保存完整的、取到尽头的数据
if(!limit)
DP[pos][pre][status] = ret;
return ret;
}