数位DP是一种针对于数的每一位进行转移的动态规划。一般题目的特点更偏向于范围大( O ( n ) O(n) O(n)会超时)、数位计数。
通常会求 [ l , r ] [l,r] [l,r]内的所有数字中符合数位特点的数有多少个。此时我们将问题拆解成 [ 1 , l − 1 ] [1,l-1] [1,l−1]和 [ 1 , r ] [1,r] [1,r],针对两个区间进行答案的记忆化搜索,然后通过二者答案差值计算符合要求的个数。
HDU2089 - 不要62
给定一个区间 [ n , m ] [n,m] [n,m],问区间内不包含 4 4 4、 62 62 62(连在一起)的数的个数。
将问题拆解成 [ 1 , n − 1 ] [1,n-1] [1,n−1]和 [ 1 , m ] [1,m] [1,m],令 a n s ( x ) ans(x) ans(x)为区间 [ 1 , x ] [1,x] [1,x]中不包含 4 4 4和 62 62 62的数字的个数,那么最后的结果是 a n s ( m ) − a n s ( n − 1 ) ans(m)-ans(n-1) ans(m)−ans(n−1)。
现在问题转化成了如何求解 a n s ( 1 , x ) ans(1,x) ans(1,x)。
我们首先定义dp数组。我们在转移的过程中要注意的事情有三个:当前是第几位、上一位是什么(用于判断62,上一位如果是6,那么这一位不可以是2),当前转移的前几位是否与x的前几位相同(如果相同,那么这一位的上限是x在这一位里的数值,否则上限是9)。
初始化时将所有元素初始化成0。
于是:dp[i][j][0/1]表示当前转移状态是第 i i i位,上一位是 j j j,这一位之前与x是否相同的状态(是则为1,否则为0)。
我们先记录x的每一位的数是多少。接下来进入记忆化搜索。
- 如果我们已经判断完了最后一位,也就是当前位已经超过了我们存储数位的范围,那么说明我们已经确定了一个数字,那么我们直接返回1。
- 如果当前位置 p o s pos pos、上一个位置数是 j j j、且这一位之前与x全部相同的状态 l i m lim lim的情况之前已经搜索过了,根据记忆化搜索的原则,直接返回我们之前搜过的结果。
- 当前两种情况都不符合时,此时就需要计算当前情况下的dp值。我们枚举这一位所有可能的、合法的数值,然后递归枚举下一位。最后将每一位的答案求和,作为当前所求状态的dp值,并将这个值返回。
所谓可能的、合法的数值,要从以下情况考虑:
-
如果前面的位和x的位完全相同,那么这一位的上限取决于x的这一位是多少。如果前面存在一位与x不同,那么这一位可以0~9任意取。
-
题目中的一些特征的限定。比如说这道题目,对于任意一位,在任意一种情况下都不能选择 4 4 4,同时如果上一位时 6 6 6的话,那么这一位不能选择 2 2 2。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL n, m;
LL a[22];
LL dp[22][11][2];
LL DP(LL pos, bool las, bool lim) {
if (!pos) return 1;
if (dp[pos][las][lim]) return dp[pos][las][lim];
LL ans = 0;
for (LL i = 0; i <= (lim ? a[pos] : 9); ++i) {
if (i == 4) continue;
if (las && (i == 2)) continue;
ans += DP(pos - 1, (i == 6), (lim and (i == a[pos])));
}
return dp[pos][las][lim] = ans;
}
LL sol(LL x) {
LL dig = 0;
memset(a, 0, sizeof(a));
while (x) {
a[++dig] = x % 10;
x /= 10;
}
memset(dp, 0, sizeof(dp));
return DP(dig, 0, 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
while (1) {
cin >> n >> m;
if (!n and !m) break;
cout << sol(m) - sol(n - 1) << "\n";
}
return 0;
}
LuoguP2602 - [ZJOI2010] 数字计数
题目大意:给定区间 [ l , r ] [l,r] [l,r],求 0 0 0~ 9 9 9的每一个数在区间中的数的数位上出现的次数。
令 a n s ( i , x ) ans(i,x) ans(i,x)表示区间 [ 1 , x ] [1,x] [1,x]中每一个数中的每一个数位里 i i i出现的次数。那么本题的答案为 a n s ( r ) − a n s ( l − 1 ) ans(r)-ans(l-1) ans(r)−ans(l−1)。
现在求 a n s ( i , x ) ans(i,x) ans(i,x)。
我们还按照刚刚拿到题目的思路来思考这道题目。我们使用记忆化搜索来求解。
考虑如何设计dp。我们令dp[i][0/1][j][0/1]表示在第 i i i个数位,这一位之前是否和x的每一位都相同(0/1),到这一位之前所求的数出现的次数 j j j,当前是否是前导0。
dp数组初始化全部为0。
先将x按位拆分成数组形式,然后进行记忆化搜索。对于当前状态,设定四个参数:当前是第pos位;这一位之前是否和x的每一位都相同,用bool变量lim表示,是则lim=1,否则lim=0;所求的数出现的次数sum;当前是否是前导0,用bool变量zero表示,若是则zero=1,否则zero=0。步骤如下:
- 如果所有位都已经走过,即pos=0,则返回当前所求的数出现的次数sum。
- 如果当前状态已经在之前搜索过,即dp[pos][lim][sum][zero]>0,则直接返回这个dp值。
- 根据lim的状态设立当前位置的数的遍历上限,如果lim是1,则选取a[pos](a是x按位拆成的数组),否则取9。对于遍历的每一个数,如果当前状态是1而且这个数=a[pos],那么下一个状态也一定lim=1,否则lim=0;如果当前属于前导零范围,即zero=1,且当前遍历是0,则下一个状态一定zero=1,否则zero=0。如果当前遍历的数是我这一轮正在查询的数,那么下一个状态所求数出现的次数为sum+1。
- 将所有遍历得到的结果求和,作为当前状态的dp值,并将这个值返回。
code:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL A, B, dig;
LL dp[13][2][13][2];
LL a[22];
LL DP(LL x, LL pos, LL lim, LL sum, LL zero) {
if (pos == 0) return sum;
if (dp[pos][lim][sum][zero]) return dp[pos][lim][sum][zero];
LL ans = 0;
for (int i = 0; i <= (lim ? a[pos] : 9); ++i) {
ans += DP(x, pos - 1, (lim and a[pos] == i), sum + ((!zero or i > 0) and (i == x)), (zero and !i));
}
return dp[pos][lim][sum][zero] = ans;
}
LL solve(LL d, LL x) {
memset(dp, 0, sizeof(dp));
dig = 0;
LL xx = x;
while (xx) {
a[++dig] = xx % 10; xx /= 10;
}
return DP(d, dig, true, 0, true);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> A >> B;
for (LL i = 0; i <= 9; ++i) {
cout << solve(i, B) - solve(i, A - 1) << " ";
}
return 0;
}