数位DP,顾名思义,是在个位,十位,百位,千位…….这些数的数位上进行的DP,它其实就是一种暴力枚举+记忆化搜索。
数位DP一般用来解决要求找出某个区间内,满足要求的数有多少个之类的问题,在这里我拿UVA - 11038来举例。这道题就是给两个整数,然后要你求出这两个整数之间的数有多少个0,比如说1到10之间有1个0,100到110之间有12个0。这是一道最基础的数位DP题,我们可以令f(a)为从0到a有多少个0,那么这道题的结果就可以表示为f(r)-f(l-1),然后我们就只需要求f(a),我们先从最高位开始枚举,比如说f(415),那么百位可以是0,1,2,3,4,当百位是0和4的时候都需要特殊考虑,如果百位是0那就表示我们目前枚举的是一个两位数,有前导零,我们可以定义一个bool lead来判断,如果百位是4,那么我们的十位数只能取0,1,因为不能超过上限,但如果百位数不是4,那么我们的十位数就可以取0到9中的任意一个,我们可以定义一个bool limit来判断。因为我们是按位枚举,所以说我们需要定义一个int pos表示当前处理的位数,有了这三个变量我们还差最后一个,也就是DP题所特有的状态变量,不同题目的状态变量是不一样的,这题因为要计算0的个数,我们可以定义状态变量为int state表示从最高位数到当前位数为止有多少个非前导0,有了这四个变量我们就可以开始写数位DP啦,每一个数位DP都需要这四个变量,状态变量在不同的题目里是不一样的,但前三个基本不会变。
下面这个模板我借鉴了WH dalao的,做了许多小改动。
typedef long long LL;
int a[20];
LL dp[20][state];//不同题目状态不同
LL dfs(int pos,/*state变量*/, bool lead/*前导零*/, bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
//递归边界,既然是按位枚举,最低位是1,那么pos==0说明这个数我枚举完了
if (pos == 0) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
//第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
if (!limit && !lead && dp[pos][state] != -1) return dp[pos][state];
//常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应的。
int up = limit ? a[pos] : 9;//根据limit判断枚举的上界up
LL ans = 0;
//开始计数
for (int i = 0; i <= up; i++)//枚举,然后把不同情况的个数加到ans就可以了
{
if () ... //根据题目要求分不同情况继续进行dfs
else if ()...
ans += dfs(pos - 1,/*状态转移*/, lead && i == 0, limit && i == a[pos]) //最后两个变量传参都是这样写的
}
//计算完,记录状态
if (!limit && !lead) dp[pos][state] = ans;
/*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
return ans;
}
LL solve(LL x)
{
int pos = 1;
while (x)//把数位都分解出来
{
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos - 1/*从最高位开始枚举*/,/*初始状态*/, true, true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
LL l, r;
memset(dp, -1, sizeof(dp));
while (~scanf("%lld%lld", &l, &r))
{
//初始化dp数组为-1
printf("%lld\n",solve(r)-solve(l-1));
}
}
这个模板可以适用于许多数位DP的题目,但都要根据题目要求进行一些修改与补充,比如说这道题套用这个模板的话代码如下。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <vector>
#include <queue>
#include <map>
#include <algorithm>
#include <set>
#include <functional>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
const int INF = 1e9 + 5;
const int MAXN = 50007;
const int MOD = 1e8 + 7;
const double eps = 1e-7;
int a[50];
LL dp[50][50];
LL dfs(int pos, int state, bool lead, bool limit)
{
if (pos == 1)//当还剩最后一位的时候
if (lead)//如果有前导零,那就只有0这一个数
return 1;
else//如果没有前导零
if (limit)//如果最后一位数有上限,当最后一位数为0的时候有1+state个0,不为0的时候有state*a[pos]个0
return 1 + state + state*a[pos];
else//如果没有上限,那么0到9一共10个state,再加上为0的时候的1个。
return 1 + state * 10;
if (!limit && !lead&&dp[pos][state] != -1)
return dp[pos][state];
int up = limit ? a[pos] : 9;
LL ans = 0;
for (int i = 0; i <= up; i++)
{
if (i==0&&lead)//如果有前导0的话,当前的这个0不会加入state中
{
ans += dfs(pos - 1, state, lead && i == 0, limit && i == a[pos]);
continue;
}
if (i == 0)//如果没有前导0的话,当前的这个0就会加入state中
ans += dfs(pos - 1, state + 1, lead && i == 0, limit && i == a[pos]);
else
ans += dfs(pos - 1, state, lead && i == 0, limit && i == a[pos]);
}
if (!limit && !lead)
dp[pos][state] = ans;
return ans;
}
LL solve(LL x)
{
int pos = 1;
while (x)
{
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos - 1,0 , true, true);//初始的非前导0一个也没有,所以是0
}
int main()
{
LL l, r;
LL a, b;
memset(dp, -1, sizeof(dp));
while (scanf("%lld%lld",&l,&r)!=EOF)
{
if (l == -1 && r == -1)
break;
if (r == 0)//0需要特判一下
a = 1;
else
a = solve(r);
if (l == 0)
b = 0;
else if (l == 1)//因为l要减一,但题目里l可以是0和1,所以在这里0和1都特判一下
b = 1;
else
b = solve(l - 1);
printf("%lld\n", a-b);
}
}