数位 dp 基础 - 学习笔记

数位 dp 基础 - 学习笔记

代码集合

P.S. 带 * 的例题是我认为有一定技巧性或有一定难度的题

1. 应用范围

数位 dp 适用于求解有以下特征的问题:

  1. 统计满足一定要求的数的数量
  2. 可以用数位的思想理解
  3. 有范围限制 (或上界限制),且上界一般很大

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]\)

统计答案时,使用计数问题常用技巧,将区间询问变为前缀询问即可

实现时可以用一些小技巧:

  1. 将数转为字符串再进行处理
  2. 由于 \(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}\) 的倍数这三条性质的数的平方和

对于平方,可以类似的按照数位的思想对其拆解:

\[(\overline{a_1a_2 \cdots a_n})^2 = (a_1 \times 10^{n-1} + a_2 \times 10^{n-2} + \cdots + a_n \times 10^{0})^2\\ = (a_1 \times 10^{n-1})^2 + (a_2 \times 10^{n-2})^2 + \cdots + (a_n \times 10^{0})^2\\ + 2(a_1 \times 10^{n-1})(a_2 \times 10^{n-2}) + \cdots + 2(a_{n-1} \times 10^{1})(a_{n} \times 10^{0}) \]

由于我们知道每一项填入的数,即 \(a_1, a_2, \cdots, a_n\) 是什么,前半段的平方和是好求的;而求个数还有和 (CF1073E) 是好做的,于是对于交叉项,我们尝试凑出和的形式,以 \(a_1\) 为主元:

\[2(a_1 \times 10^{n-1})(a_2 \times 10^{n-2}) + \cdots + 2(a_{n-1} \times 10^{1})(a_{n} \times 10^{0})\\ = 2(a_1 \times 10^{n-1})(a_2 \times 10^{n-2} + \cdots + a_{n} \times 10^{0})\\ + 2(a_2 \times 10^{n-2})(a_3 \times 10^{n-3}) + \cdots + 2(a_{n-1} \times 10^{1})(a_{n} \times 10^{0}) \]

这里 \(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

posted @ 2025-04-06 15:11  lzlqwq  阅读(62)  评论(0)    收藏  举报