解题报告——论对“数位 dp”的新理解
解题报告——论对“数位 \(dp\)”的新理解
以前我总是对这个东西心存畏惧,但是今天打了一道题之后发现它好像不是那么难以理解。我们一步一步剖析它。
首先,数位 \(dp\) 是什么?给定一个区间 \([L,R]\),求区间内满足某些条件的数的数量。这些条件通常跟数位有关,我们务必要把这个数拆开。
题目提问说让我们求 \([L,R]\) 之间满足条件的数的数量,我们何不把它看成 \([0,R]\) 之间满足条件的数的数量减去 \([0,L-1]\) 之间满足条件的数的数量?这就是数位 \(dp\) 的第一步。
第二步,我们把数一个一个往上数,比如从 \(2000\sim 2999,3000\sim 3999,4000\sim 4999\),这样 \(000\sim 999\) 就数了三遍,这很明显是可以优化的。我们已经深入到了数位 \(dp\) 的核心。
第三步,数位 \(dp\) 的状态设置。最朴素的数位 \(dp\) 一般只会设置一个量,就是 \(f_{dep}\),其中 \(dep\) 表示第几位。但是根据题目情况的不同,为了满足条件,我们一般设置多个量,其中一个是 \(dep\),另外一大堆就视题目而定了。
第四步,数位 \(dp\) 的实现方式。由于是数位 \(dp\),要一位一位地 \(dp\),我们一般采用记忆化搜索的方式一位一位往下搜。那么搜索的时候又应该设置什么量呢?除了前面状态表示中的一堆量,我们还要额外设置两个特殊的 \(\texttt{bool}\) 量:一个 \(lim\) 用来记录是否顶到上界,一个 \(lead\) 用来记录当前位是否是前导零。这两个量的实际用处视题目不同而不同,但是一般都要设置。此处额外提醒,如果 \(lim\) 或者 \(lead\) 是 \(\texttt{true}\),那么当前状态得到的 \(dp\) 结果不能记录入 \(f\) 数组中。
第五步,规范一下代码吧。数位 \(dp\) 一般分几块来写,条理清晰:
- 边界条件。比如,\(dep=0\),搜到最后一位外面去了。
- 记忆化。如果当前状态的 \(lim\) 和 \(lead\) 都是 \(\texttt{false}\) 并且当前位置对应的 \(f\) 已经有值了,就直接返回。
- 向下搜索。这是最重要的一步,由当前状态,往下递推,所有状态都会因为这个增加的数而发生变化。
- 存储答案。特别的,如果 \(lim\) 或者 \(lead\) 是 \(\texttt{true}\),不能存储答案。
最后说一个遇到的易错点。如果题目问话只给上界,一定要看下界是 \(1\) 还是 \(0\)。大多数数位 \(dp\) 过程中都会把 \(i=0\) 的情况算入答案,这个时候要考虑一下是否要去掉。
这种数位 \(dp\) 入门题,曾经能够卡我 \(3\) 个小时,今天往后不会了。但是有一说一,也正是这道题让我悟出了数位 \(dp\) 的核心。