数位 dp 基础 - 学习笔记
数位 dp 基础 - 学习笔记
P.S. 带 * 的例题是我认为有一定技巧性或有一定难度的题
1. 应用范围
数位 dp 适用于求解有以下特征的问题:
- 统计满足一定要求的数的数量
- 可以用数位的思想理解
- 有范围限制 (或上界限制),且上界一般很大
2. 例题
LOJ #10163
这道题很容易用数位的思想理解;考虑把十进制数转为 \(B\) 进制数,题意即求 \(\bm{[X, Y]}\) 内满足 \(\bm{B}\) 进制下恰有 \(\bm{K}\) 个 \(\bm{1}\),且其他位均为 \(\bm{0}\) 的数的个数
考虑使用记搜,记录四个状态 \(step, now, f, k\),其含义分别为:
- \(step\) 表示 当前搜到了第几位 (我们从高位向低位搜,因为这样方便进行可行性剪枝)
- \(now\) 表示 上一位填了什么
- \(f\) 表示 是否在 "贴边搜索" (即前面的每一位都恰好等于上界的相应位)
- \(k\) 表示当前 填入了几个 \(\bm{1}\)
设上界的当前位为 \(up\),我们需要剪枝的情况有:
- 当前填的数 \(>1\) (由题意,显然)
- \(f\) 为真且当前填的数 \(>up\) (不能超过上界)
- \(step\) 大于上界总位数 (此时直接统计是否有 \(k = K\) 即可)
- \(k\) 大于 \(K\) (填的 \(1\) 过多)
除去需要剪枝的情况,显然只能进行两种转移,即当前填 \(0\) 或者填 \(1\),转移为
- \(dp[step][0/1][f][k] \rightarrow dp[step+1][1][f \land (up=1)][k+1]\)
- \(dp[step][0/1][f][k] \rightarrow dp[step+1][0][f \land (up=0)][k]\)
统计答案时,使用计数问题常用技巧,将区间询问变为前缀询问即可
实现时可以用一些小技巧:
- 将数转为字符串再进行处理
- 由于 \(X\) 特别大时处理 \(X-1\) 较为麻烦,可以变为 \(Y\) 的前缀和减去 \(X\) 的前缀和,最后单独 \(check\) 一下 \(X\) 是否满足要求
warning:在求 \(\bm{X-1}\) 前缀和时一定注意 \(\bm{1-1}\) 变字符串可能会出现空串引发一系列问题,最好特判这种情况并返回 "0"
LOJ #10164
题意为求 \(\bm{[X, Y]}\) 内十进制数从左到右各位不下降的数的个数
记搜,仍然记录 \(step, now, f\),含义同上
记上界当前位为 \(up\),本题与上一题的区别在于转移方程稍有不同:
- 当 \(f\) 为真时,下一位能填 \([now, up]\),当且仅当填 \(up\) 的时候转移后的 \(f\) 为真
- 当 \(f\) 为假时,下一位能填 \([now, 9]\),转移后的 \(f\) 总为假
P2657
题意为求 \(\bm{[A, B]}\) 内不含前导零,且十进制数从左到右每相邻两位绝对值 \(\bm{\ge 2}\) 的数的个数
这题除了记录 \(step, now, f\) 外,还需要多记录一维 \(zero\),表示当前是否在处理前导零
记上界当前位为 \(up\),不妨设 \(up'\) 表示当前能填入的最大数,容易得出当 \(f\) 为真时,\(up' = up\),反之为 \(9\);转移方程为:
- \(zero\) 为真时,可以继续填前导零,转移为 \(dp[step][now][f][zero] \rightarrow dp[step+1][0][f \land (up = 0)][true]\)
- \(zero\) 为真时,可以填任意 \(i \in [\bm{1}, up']\),转移为 \(dp[step][f][zero] \rightarrow dp[step+1][i][f \land (up = i)][false]\)
- \(zero\) 为假时,可以填任意 \(i \in [\bm{0}, up']\),转移为 \(dp[step][f][zero] \rightarrow dp[step+1][i][f \land (up = i)][false]\)
LOJ #10167
题意为求 \(\bm{[N, M]}\) 内不含 \(\bm{4}\) 且没有相邻两位为 \(\bm{62}\) (\(\bm{26}\) 是满足要求的) 的数的个数
仍然直接上记搜,除了 \(step, now, f\) 多记录一维 \(invalid\),表示当前是否不合法;如果在结束时 \(invalid\) 为真则返回 \(0\),反之返回 \(1\) 即可
\(step, now, f\) 正常转移即可,对于 \(invalid\) 的转移:
- \(now = 6\) 且当前填的数为 \(2\),转移后的 \(invalid\) 为真
- 当前填的数为 \(4\),转移后的 \(invalid\) 为真
- 其他情况下,转移后的 \(invalid\) 即为原来的 \(invalid\)
P2602
题意为求 \(\bm{[A, B]}\) 内每个数码的出现个数
算是前面几道题的综合
枚举 \(dig \in [0, 9]\) 分别统计;上记搜,记录 \(step, now, f\) 以及 \(zero\) 表示是否在处理前导零,\(sum\) 表示当前数码 \(dig\) 出现了多少次;结束时返回 \(sum\) 即可
\(step, now, f\) 正常转移,\(zero\) 同 P2657 的方法转移,\(sum\) 则在当前填的数码为 \(dig\) 时 \(+1\),反之不变即可;总体来说还是大同小异的
LOJ #10166
题意为求 \(\bm{[A, B]}\) 内各位数字加起来为 \(\bm{N}\) 的倍数的数的个数
除了 \(step, now, f\),多记录一维 \(mod\) 表示之前填入的所有数字之和模 \(N\) 的倍数;当 \(step\) 大于上界的长度时若 \(mod = 0\) 则返回 \(1\),反之返回 \(0\) 即可
P4999
题意为求 \(\bm{[L, R]}\) 内所有数字各位数码的和
除了 \(step, now, f\),多记录一维 \(sum\) 表示当前的数字和;注意有取模,边搜边模即可
P6218
题意为求 \(\bm{[L, R]}\) 内二进制表示下为 \(0\) 的位数大于等于为 \(1\) 的位数的数的个数
记录 \(step, now, f, zero\),同时多开两维记录 \(cnt0, cnt1\),分别表示为 \(0\) 的位数与为 \(1\) 的位数;递归超出边界时进行判断,如果 \(cnt0 \ge cnt1\) 则返回 \(1\),反之返回 \(0\) 即可
dp 数组直接照着 dfs 状态大胆开 \(dp[35][2][2][2][35][35]\) 即可,反正爆不了
P4317
题意为求 \(\bm{[1, N]}\) 中所有数二进制表示下 \(\bm{1}\) 的个数的乘积
记录 \(step, now, f\),同时开一维记录 \(cnt\) 表示当前 \(1\) 的位数;注意题目要求的是乘法,因此答案应该累乘;有个细节是当递归超出边界时不能直接返回 \(cnt\),因为如果一直填 \(0\) 就会返回 \(cnt = 0\),进而使整个乘积都变成 \(0\)。因此我们返回 \(max(cnt, 1)\)
P4124
题意为求 \(\bm{[L, R]}\) 内不同时含有 \(\bm{4, 8}\) 且有至少连续三位数字相同的数的个数
既然空间足够,我们可以多开几维简化问题
在记录 \(step, now, f, zero\) 的基础上,记录 \(vis4, vis8\),表示是否出现 \(4\) 以及是否出现 \(8\);另外再记录 \(cnt, tot\),其中 \(cnt\) 表示 当前数末尾 的最长连续相同位数,\(tot\) 表示 整个数字中 的最长连续相同位数
- 对于 \(vis4\) 与 \(vis8\),我们只需要在转移时判断是否选择了 \(4\) 或 \(8\) 即可
- 对于 \(cnt\),我们需要判断当前选择的数字与 \(now\) 是否相同 (填前导 \(0\) 除外),显然相同则 \(cnt' = cnt+1\),不相同则 \(cnt' = 1\)
- 对于 \(tot\),每次转移时与 \(\bm{cnt'}\) 取 max 即可
在递归超出边界时,若 \(\lnot (vis4 \land vis8) = true\) 且 \(tot \ge 3\) 就返回 \(1\)
CF1036C
题意为求 \(\bm{[L, R]}\) 中包含 \(\bm{1 \sim 9}\) 的数位不超过 \(\bm{3}\) 个的数的个数
记录 \(step, now, f, zero\) (其实可以不记录 \(now\)),另开一维 \(cnt\) 记录包含 \(1 \sim 9\) 的数位有多少个即可
*CF1073E
题意为求 \(\bm{[L, R]}\) 内各数位包含的不同数字个数 \(\bm{\le K}\) 的数之和
首先考虑如何求出满足要求的数的个数;记录 \(step, now, f, zero\),由于 \(K \le 10\),可以另外开一维 \(tp\) 来 状压 当前选择的数位的情况,\(tp\) 二进制下第 \(i\) 位为 \(1\) 则表示当前的数含有数位 \(i\)
\(step, now, f, zero\) 正常转移;设选择填 \(i\),对于 \(tp\),若选择填前导零则转移后的 \(tp\) 不变,否则 按位或 \(\bm{2^i}\) 即可,递归超出边界时若 \(tp\) 中 \(1\) 的个数 \(\le K\) 则返回 \(1\)
但是题目要求的是满足要求的数之和;显然边递归边记录当前填入的所有数位形成的数、最后再相加是不可取的,因为记忆化过程中可能会直接 return,到不了最后
其实这里仍然可以用数位的思想;将每一数位 \(i\) 分开考虑,其贡献即为 \(\bm{i \times 10^x \times dp_{cnt}}\),\(i \times 10^x\) 为当前数位的具体值,\(dp_{cnt}\) 则是当前状态后满足要求的后续填法个数;
具体地,我们设
- \(dp[step][now][f][zero][tp]\) 表示 上一位填 \(\bm{now}\),当前状态后有多少种满足要求的填法 (当前位及以后位的填法)
- \(ans[step][now][f][zero][tp]\) 表示 上一位填 \(\bm{now}\),当前状态后满足要求的数中数位贡献之和 (当前位及以后位的贡献)
P.S. 下面用 \(step', now', f', zero', tp'\) 描述转移后的各个参数,\(len\) 代表上界总长度 (这里从高位向低位搜)
对于 \(ans\) 的转移,枚举每个当前位能填的 \(now'\),累加当前数位的贡献并向后递归即可
- \(ans[step][now][f][zero][tp] = \sum_{now'} (ans[step'][now'][f'][zero'][tp'] + now' \times 10^{len-step} \times dp[step'][now'][f'][zero'][tp'])\)
- 递归超出边界时,对应状态的 \(ans\) 必然为 \(0\)
*P4127
题意为求 \(\bm{[A, B]}\) 内各位数字之和能整除原数的数的个数
首先考虑记录 \(step, now, f, sum, num\),\(sum\) 表示当前填过的所有数位之和,\(num\) 表示当前填过的所有数位组成的数字;递归超出边界时判断是否有 \(num\bmod sum = 0\) 即可
此时就出现了问题;如果我们不把 \(num\) 加入到记忆化数组中,仅有 \(step, now, f, sum\) 的限制过于宽松,会影响正确性;但是 \(num\) 的范围可以到 \(1e18\),如果加入记忆化数组空间开销太大
因此,我们考虑记录 \(num\) 取模后的值;此时 \(sum\) 就是一个天然的模数;但是 \(sum\) 会随着递归改变啊?其实没关系,\(sum\) 的范围只有 \(9 \times 18 + 1\),我们可以提前枚举,设为 \(sum'\);因此,最终递归超出边界时若 \((sum = sum') \land (num = 0)\) 为真则返回 \(1\) 即可
*CF855E
题意为求 \(\bm{[L_i, R_i]}\) 中 \(\bm{B_i}\) 进制下为 \(\bm{0 \sim B_i-1}\) 的位的个数均为偶数的数的个数
记录 \(step, now, f, zero\),同时记录 \(tp\) 状压 当前填 \(0 \sim B-1\) 的位的个数的奇偶性;设当前选择填 \(i\),除去填前导零的情况,转移后的 \(tp'\) 即为 \(tp \oplus (2^i)\);设初始时 \(tp = 0\),递归超出边界时若 \(tp = 0\) 则返回 \(1\)
这时我们一交,哎,直接 TLE!一看数据范围,\(q \le 1e5\)。每次都直接 memset 清空 dp 数组,不 TLE 才怪
那么,我们显然需要减少清空的次数;注意到同进制下 dp 数组似乎可以共用,但一试发现也不太对;问题出在哪?考虑不同上界对搜索的影响:
- \(\bm{f = true}\) 时,会有问题;不同的上界,当前位的最大值限制可能不同,这会进一步导致利用 dp 数组记忆化 (也就是若 dp 数组非空则直接返回这一步) 出现问题,可能多搜或少搜
- 正搜和倒搜有区别!(即 \(\bm{dp}\) 数组记录 \(\bm{step}\) 不应按照 \(\bm{0 \rightarrow len}\) 而应 \(\bm{len \rightarrow 0}\)) 如果我们正搜,当之前的串比当前串短时,利用 dp 数组记忆化可能会 导致少搜,因为可能本来还能往后填但我们认为不能填了;倒着搜则能够避免这个问题。这是因为当长串和短串的剩余长度相同时,接下来的填法本质上是一样的 (我们已经记录了 \(tp\) 保证奇偶性相同),可以直接记忆化
综上,我们改为倒搜,且在 \(f = true\) 时不进行记忆化即可;实现时可以多开一维记录进制,即将定义改为 \(dp[B][step][f][zero][tp]\),其中 \(B\) 是进制维;这样我们只需要在一开始 memset 一次即可
P3413
题意为求 \(\bm{[L, R]}\) 内存在长度至少为 \(\bm{2}\) 的回文子串的数的个数
考虑分析回文子串如何形成;注意到一个长回文串必然是由短回文串两侧拼上相同字符形成的,因此我们 只需要判断是否存在最短的回文串即可,显然最短的回文串只能形如 \(\bm{xyx}\) 或 \(\bm{xx}\)
因此,除了记录 \(step, now, f, zero\),我们额外记录 \(noww\) 表示上上位填了什么与 \(flag\) 表示当前是否已经存在回文串
设当前选择填 \(i\),转移即为
- \(noww'\) = \(now\)
- \(now'\) = \(i\)
- \(flag'\) = \(flag \lor (i = now) \lor (i = noww)\)
实现时,注意在填前导零时直接令 \(now = 0\) 会对之后判断回文串产生影响;一种想法是让 \(now = -1\),但这样会使数组下标变成负数,因此我们令 \(now = 10\) 即可
*LOJ #10168
题意为求 \(\bm{[L, R]}\) 内满足每个数位都不为 \(\bm{7}\),每一位加起来不为 \(\bm{7}\) 的倍数,自身不是 \(\bm{7}\) 的倍数这三条性质的数的平方和
对于平方,可以类似的按照数位的思想对其拆解:
由于我们知道每一项填入的数,即 \(a_1, a_2, \cdots, a_n\) 是什么,前半段的平方和是好求的;而求个数还有和 (CF1073E) 是好做的,于是对于交叉项,我们尝试凑出和的形式,以 \(a_1\) 为主元:
这里 \(a_2 \times 10^{n-2} + \cdots + a_{n} \times 10^{0}\) 已经是和的形式了;而后半段可以继续递归求解,那么做完了
转移时,记录当前状态后个数的贡献 \(cnt\)、数位和的贡献 \(ans\)、数位和的平方的贡献 \(sqans\) 即可;设我们当前选择填入 \(i\),转移即为
- \(cnt = \sum cnt'\)
- \(ans = i \times 10^x \times cnt' + \sum ans'\)
- \(sqans = (i \times 10^x)^2 \times cnt' + 2 \times (i \times 10^x) \times ans' + \sum sqans'\)
实现时,dfs 除了记录 \(step, now, f\) 另开 \(vis\) 维护是否出现 \(7\),开 \(dig\) 维护数位之和模 \(7\) 的余数,开 \(tot\) 维护自身模 \(7\) 的余数;递归超出边界时,\(ans\) 与 \(sqans\) 显然为 \(0\),若满足题目要求则 \(cnt = 1\)
*CF908G
题意为求 \(\bm{[1, N]}\) 中每个数在各数位从小到大排序后形成的数之和
经过思考,容易发现通过一次 dfs 搜出答案是较难的,因为当前填的数可能影响很多前面的数;
考虑将排序后的数拆成没有大小关系的一堆数相加,即拆成一堆 \(11 \cdots 1\) 相加;我们分析这些 \(11 \cdots 1\) 的产生,例如 \(345 = 111+111+111+11+1\),写成竖着的形式:
345
111
111
111
11
1
容易发现,数位 \(3\) 贡献的 \(3\) 个 \(1\) 在第 \(1,2,3\) 行,数位 \(4\) 贡献的 \(4\) 个 \(1\) 在第 \(1,2,3,4\) 行,数位 \(5\) 贡献的 \(5\) 个 \(1\) 在第 \(1,2,3,4,5\) 行;
因此,我们有结论:数位 \(i\) 贡献的 \(i\) 个 \(1\) 在 \(1,2,\cdots,i\) 行;反过来,第 \(i\) 行的 \(1\) 的个数实际上就等于原数中 \(\ge i\) 的数位的个数 (因为它们能在第 \(i\) 行贡献 \(1\))
于是直接数位 dp 即可;我们枚举行数 \(d\),统计填的数位中有 \(\ge d\) 的数位个数 \(cnt\),预处理下 \(s[i]\) 表示 \(\underbrace{11 \cdots 1}_{\text i}\),最终贡献即为 \(s[cnt]\)
CF628D
题意为求 \(\bm{[L, R]}\) 内从高位向低位数,偶数位是 \(\bm{d}\),奇数位不是 \(\bm{d}\),且能被 \(\bm{m}\) 整除的数的个数
除记录 \(step, f\) 之外,另记录 \(rem\) 表示模 \(m\) 的余数,\(vis\) 表示当前填过的所有数位是否满足要求 (是/不是 \(d\) ) 即可
对于 \(rem\) 的转移,可以考虑按照数位的思想,将填过的每一位变化为 \(i \times 10^x\) 的形式并累加余数
实现时,注意如果 \(step\) 从 \(0\) 开始枚举,奇偶数位与 \(step\) 的奇偶性是相反的;另外函数传参时不要把字符串传进去,本题字符串长度最大能到 \(2000\),容易 TLE
*CF401D
题意为求 将 \(\bm{\overline{a_1a_2 \cdots a_n}}\) 各数位重排后能生成的 \(\bm{m}\) 的倍数有多少个
显然一种很暴力的想法是直接数位 dp,开 \(10\) 个变量记录值为 \(0,1,2,\cdots,9\) 的数位各出现了多少次,最终暴力比较与原数中对应数位出现次数是否相同,于是我就交了两发 MLE
实际上,直接数位 dp 的话空间至少得开到 \(18 \times 3^9 \times 100\),再加上恐怕需要用 map 存,很容易被卡到 MLE
注意到实际上能填的数很少,数位 dp 会有很多状态实际上是没用的;于是我们考虑每次确定要填的数在原数中的 哪一位,本质上就是 状压;设状态 \(dp[S][md]\) 表示当前填的位置集合为 \(S\) (用二进制数状压) 且模 \(m\) 的余数为 \(md\)
对于转移,我们枚举每一位 \(i\),若 \(S\) 中没有则将第 \(i\) 位补到当前填的所有位的最后;转移即为 \(dp[S][md] \rightarrow dp[S \lor 2^i][(md \times 10 + i) \bmod m]\);注意题目中还有不能填前导 \(0\) 的限制,我们在开始时特判一下即可
不过这样连样例都过不了;这是因为对于相同的数位,填的先后顺序本质相同,导致我们多算了贡献;此时显然的想法是,设各数位出现次数为 \(cnt_0, cnt_1, \cdots, cnt_9\),我们将答案除掉 \(cnt_0!cnt_1! \cdots cnt_9!\) 即可;于是我又交了一发 TLE
这么算答案是对的,但效率不高 (最坏 \(2^{18} \times 100 \times 18\) );对于这个问题,我们还有一种解决方式,就是 钦定填入顺序为从前到后;具体的,我们钦定填值为 \(i\) 的数位时,必须填所有值为 \(i\) 的数位中还未填的最靠前的一个;这样就剪掉了一些状态,同时也处理了重复,足够通过此题
实际上是个状压 dp 套数位 dp

浙公网安备 33010602011771号