DP专题-学习笔记:数位 DP
一些 update
update 2021/2/23:最近作者发现数位 DP 的 \(f\) 数组初始化有问题,导致代码出现了根本性错误(原理不变),现在已经纠正,对各位读者造成的困扰深表歉意。
1. 概述
数位 DP,是一种 DP (废话),专门用于数字统计类问题。
这种问题首次接触可能会有些难理解,但是练过几道题之后就会掌握套路了。
2. 例题
这是数位 DP 的基础题,我个人认为比别的题目更好入门。
2.1 表达形式
数位 DP 的题目通常都是这样表达的:
问:在 \([l,r]\) 范围内,满足条件 \(P(x)\) 的数 \(x\) 有几个?
比如例题:
问:在 \([a,b]\) 范围内,满足条件 \(相邻数之差大于等于2\) 的数 \(x\) 有几个?
2.2 修改问题
数位 DP 的问题一般都满足 区间可减性。
比如说例题,无论 \(a,b\) 怎么变,Windy 数始终是 Windy 数,非 Windy 数始终是非 Windy 数。
于是乎,我们就可以使用差分将询问变为 \(f(b)-f(a-1)\)。
其中 \(f(x)\) 表示在 \([1,x]\) 内有几个满足条件的数。
代码里面需要特别对 \(a=0\) 特判!
2.3 设计 DP
既然是数位 DP,那么肯定跟数位有关。
那么首先我们要拆分数字。假设拆分在 \(a\) 数字里面,\(a_i\) 表示 从低到高第 \(i\) 位(这是为了与代码匹配)。\(cnt\) 为位数。
数位 DP 有两种写法:记忆化搜索与直接递推。
记忆化搜索便于理解,直接递推代码简洁。
本文章采用记忆化搜索讲解。
首先设计 DFS 函数:(LL
即为 long long
)
LL dfs(int pos, int las, ..., int zero, int limit)
pos
表示当前搜到第几位,las
表示上一位数字是什么(Windy 数需要上一位数字),zero
表示是否为前导 0,limit
表示这一位有没有最高位限制。
...
是表示有的题目可能需要一些别的。
先撇下 zero
和 limit
不管,我们继续设计 DFS 函数。
那么搜索思路显然:我们只需要枚举第 pos
位上的数字,满足题目要求就继续往下搜,而采用 \(f[pos][las]\) 来记忆化。
那么这个 zero
和 limit
呢?
zero
是前导零,limit
表示限制。
因为前导零是不计入数字限制,所以特别要注意前导零不能对数字统计造成干扰,于是我们需要注意前导零的限制,在记忆化搜索的时候对于有前导零限制的数我们不能记忆化。
limit
表示限制,这个限制是干什么用的呢?
考虑以下数据:
0-47382
,现在已经填了 473??
那么第四位我们要怎么枚举呢?
如果我们直接枚举 0-9
,那么在枚举到 9
的时候就会发现:4739?
这个数字不在范围内。于是我们需要引入 limit
做出限制。
怎么处理?
2.4 特别处理
首先看 zero
的处理。
zero
的判定条件:当上一位 zero
为真且当前位为 0 时,接下来的 zero
为真。
解释:
上一位 zero
为真表示前面所有位都是 0,而这一位也是 0,于是乎数字为 00..0??...?
,那么 zero
接下来为真,告诉后面的数字这一位的 0 是前导零。
再看 limit
的处理。
limit
的判定条件:当上一位 limit
为真且当前位为最高位时,接下来的 limit
为真。
解释:
上一位 limit
为真表示前面都是最高位,这一位也是最高位,于是乎从最前面到这一位都是最高位,那么 limit
接下来为真,告诉后面的数字这一位是最高位。
需要特别注意的是,当 zero=0
或 limit=0
的时候不能记忆化!(当然你可以加两个状态表示 zero
和 limit
,这样可以)
2.5 书写代码
在搜索时我们按照一般的搜索套路,特别注意对 zero
和 limit
的处理。
这里先简单画一下流程图。以例题为例。
这是本人第一次使用 Markdown 画流程图,如果画的不好请各位大佬海涵,有更好的建议请在评论区提出或者私信提出,本人将会采纳,表示感谢!
那么有了这个流程图,代码应该很好写了。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int l, r, f[20][10], a[20], cnt;
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
int Abs(int x) {return (x < 0) ? -x : x;}
int dfs(int pos, int las, int zero, int limit)
{
if (pos == 0) return 1;
if (!zero && !limit && f[pos][las] != -1) return f[pos][las];
int t = limit ? a[pos] : 9, ans = 0;
for (int i = 0; i <= t; ++i)
{
if (zero && i == 0) ans += dfs(pos - 1, -2, zero, i == a[pos] && limit);
else if (Abs(i - las) >= 2) ans += dfs(pos - 1, i, 0, i == a[pos] && limit);
}
if (!zero && !limit) f[pos][las] = ans;
return ans;
}
int Get(int k)
{
cnt = 0; memset(a, 0, sizeof(a));
memset(f, -1, sizeof(f));
while (k) {a[++cnt] = k % 10; k /= 10;}
return dfs(cnt, -2, 1, 1);
}
int main()
{
l = read(), r = read();
printf("%d\n", Get(r) - Get(l - 1));
return 0;
}
这里再放一个万能板子,供大家参考:
#include <bits/stdc++.h>
using namespace std;
//sth.
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
int dfs(int pos, ..., int zero, int limit)
{
if (pos == 0) return 1;
if (!zero && !limit && f[...] != -1) return f[...];
int t = limit ? a[pos] : 9, ans = 0;
for (int i = 0; i <= t; ++i)
{
if (zero && i == 0) ans += dfs(pos - 1, ..., zero, i == a[pos] && limit);
else if (由题目条件决定) ans += dfs(pos - 1, ..., 0, i == a[pos] && limit);
//else ...
}
if (!zero && !limit) f[...] = ans;
return ans;
}
int Get(int k)
{
cnt = 0; memset(a, 0, sizeof(a));
memset(f, -1, sizeof(f));
while (k) {a[++cnt] = k % 10; k /= 10;}
return dfs(cnt, ..., 1, 1);
}
int main()
{
l = read(), r = read();
if (l != 0) printf("%d\n", Get(r) - Get(l - 1));
else printf("%d\n", Get(r));
return 0;
}
这里对代码做一个统一说明与解释:
- 例题代码的
-2
是为什么?
处理方便,不需要特判。 - 注意板子里面要特判一下
l!=0
。
这里再给一个板子,这个板子是将 zero
和 limit
也加入记忆化的状态里面的。
int f[...][2][2];
int dfs(int pos, ..., int zero, int limit)
{
if (pos == 0) return 1;
if (f[...][zero][limit] != -1) return f[...][zero][limit];
int t = limit ? a[pos] : 9, ans = 0;
for (int i = 0; i <= t; ++i)
{
if (zero && i == 0) ans += dfs(pos - 1, ..., zero, i == a[pos] && limit);
else if (由题目条件决定) ans += dfs(pos - 1, ..., 0, i == a[pos] && limit);
//else ...
}
f[...][zero][limit] = ans;
return ans;
}
3. 练习题
练习题传送门:DP专题-专项训练:概率/期望 DP + 数位 DP