数位统计 dp
简介
数位dp启蒙老师:数字组成的奥妙——数位dp - Mathison 的博客 - 洛谷博客 (luogu.com.cn)
数位统计dp,简称数位dp,指的是一种与每个数字的数位上有关的动态规划问题。其中,数位既可以是基于十进制,也可以基于二进制等等。
解决的问题
数位dp能解决什么问题?
数位dp通常能解决的问题有这样大致的题意:给定一个区间 [ L , R ] [L,R] [L,R],问在这个范围内能够使得给定条件 f ( i ) f(i) f(i)满足的个数。不同的问题可能会有不同,不过如果满足上述问题时则可以尝试使用数位dp解决问题。
另外一方面,因为在数位dp中,是按照数的每一个数位进行dp,因此在数位dp相关问题中给定的数据范围一般会很大,暴力的方法会TLE,而使用动态规划的方式则会使得过程的重复子问题被精简掉。
写法
递推
数位dp的递推写法,也就是考虑对当前枚举的这一位数字,要考虑到它对这三个状态的影响:
- 第 1 1 1位至第 i − 1 i-1 i−1位(可能没有)
- 第 i i i位
- 第 i + 1 i+1 i+1位至最后一位(可能没有)
考虑这三个大的子问题后,还要考虑其中的子问题:
- 可以有多少个数字的“放法”
- 是否受到前导零的影响
- …
但是由于记忆化搜索在数位dp上的应用反而会使得过程清晰化,所以其实推荐的还是记忆化搜索。
记忆化搜索
众所周知,记忆化搜索通过将某个状态记录下来,从而避免重复对状态的搜索。对于数位统计dp问题中,显然会存在非常多的重复子问题。因此,记忆化搜索很适合于数位dp中。并且,记忆化搜索还可以有“模板”供于使用。
using ll = long long;
ll f[35][35];
int digit[35], len;
ll dfs(ll pos, ll st, ..., bool lead, bool limit) {
/* 搜索结束,该返回点东西 */
if (pos > len) {
return ?;
}
/* 符合的状态搜过,结束搜索 */
if (f[pos][st]...[] != -1 && !lead && !limit) {
return f[pos][st]...[];
}
/* 继续搜索,同时结果先暂存 */
int LIM = limit ? digit[pos] : 9;
ll ret = 0;
for (int i = 0; i <= LIM; ++ i) {
/* 各种条件判断,比如前导0 */
if () {
} else {
ret += dfs(pos + 1, st + (?), ..., lead && i == 0, limit && i == LIM);
}
}
/* dp数组存的是没有前导零和达到边界的状态 */
if (!lead && !limit) {
f[pos][st]...[] = ret;
}
return ret;
}
ll solve(ll n) {
memset(f, 0, sizeof f);
m = 0;
while (n > 0) {
digit[++ m] = n % 10;
n /= 10;
}
reverse(digit + 1, digit + 1 + m);
return dfs(1, 0, ..., true, true);
}
其中,对于limit
,也就是上一位是否达到最大数字的表示,可以用limit && i == LIM
进行状态转移;同时,对于lead
,也就是上一位是否是0,可以用lead && i == 0
进行状态转移。
例题
例题 统计问题 The Counting Problem
给定两个整数 a a a 和 b b b,求 a a a 和 b b b 之间的所有数字中 0 0 0 ~ 9 9 9 出现次数。
例如, a a a = 1024 1024