DP 概论
对于一个题目,我们有暴力搜索算法。而 dp 就是尽可能将同一类的东西合并到一个状态上。
如何检查 dp 的正确性?
- 检查集合内部转移是否一致。
- 检查方案数是否正确。
1 基础 dp 方向
1.1 状压 dp
有好几种状态压缩的方式:
- 记录“选了哪些东西”这样的信息,可以用一个长度为 \(n\) 的 01 串,对应一个十进制数。
- 记录可重无序集合:如果不关心顺序,只关心它们分别出现了多少值,可以记录无序集合,如果集合内的数字之和保证小于等于某一个数 \(S\),那么状态的个数小于 \(S\) 的分拆数,大概 \(53\) 以下的分拆数能过。
- 网格图,考虑一行一行转移,然后要记录 \(1 \sim i\) 行的信息,比如黑色格子组成的连通块,使用最小表示法:考虑同一个连通块上所有点用一个数字来表示,但是从左到右数字第一个出现的位置符合升序。这样就可以一格一格转移。
- 插头 dp:高进制状压。可以自己定义的东西。
其实相当于哈希了。状态给他搞成一个方便处理的数字或者数组。
1.2 树形 dp
换根 dp:先求出子树内的所有点到自己的影响,然后求出子树外的所有点到自己的影响,先从下往上做,然后从上往下做。
树形背包:背包大小为 \(m\) 的树形背包,复杂度为 \(O(nm)\)。
拓扑序相关:
如果带修:结合树剖线段树等算法一起做,用矩阵。
1.3 区间 dp
什么时候会想到区间 dp:所有操作区间在原序列中要么无交要么包含的情况。例如石子合并问题,操作的区间(相对于原序列)要么无交要么包含。
再例如从一些区间内找出最多的区间使其无交或包含,也可以区间 dp。
1.4 数位 dp
如果考虑进位的,从低到高来;考虑比较大小的,从高到低来(比如某一个区间,那么考虑是否和原数前面几个数都一样),需要记录的记录一下就好了。
1.5 自动机上 dp
AC 自动机上可以 dp。但是我们现在说的是自己建立自动机的 dp:三步骤:考虑我们的状态,将每个可行状态当成一个节点,然后搜索出转移,最后直接跑 dp。算出的是精确状态数,可能可以缩小一些状态数。
这个做法是建立了一个自动机,因为自动机其实是基于 dp 建立的所以我们叫它 dp 套 dp。
一般用来解决这样的问题:
- 如果给定一个字符串,可以使用 dp 判断它是否合法。
- 需要求每个位置在许多字符中任取的时候有多少合法的串。
我们的方法是:直接对记录状态的那个 dp 数组进行压缩,然后根据内层 dp 的转移,建立一个自动机,然后在这个自动机上跑 dp。
QOJ1251. Even Rain
这题目,我设计状态的时候,考虑钦定目前 dp 的位置是前缀 max,然后状态 \(O(nk)\),转移 \(O(k^2)\) 并且很复杂;可以将前缀 max 的位置放进状态,然后 \(O(1)\) 转移。
给我的一个教训是:如果某一个状态,要么要加入进状态里面,要么要 dp 它,都是躲不开的,那么考虑加入进状态可能会比 dp 它少判断很多东西,并且更优。
HDU2484
这题是一个树形期望 dp,对每一个 \(\min\) 和 \(sum\) 相同的元素,其 \(fa\) 的 dp 值可能不一样,但是其 \(a, b\) 值是一样的,我们就可以记忆化搜索,只需要 \(n^2\) 种状态而不是 \(2^n\) 种。
为了优化 dp 的复杂度,需要找等价的 dp 状态。为了找等价的 dp 状态,考虑 dp 的转移具体和什么有关,有没有办法从状态里去掉不需要的东西。例如此题,虽然往回的状态需要整个 mask,但转移的系数只和后面的和有关。
2 基础 dp 优化
2.1 单调队列优化
考虑这样的 dp 式子:
\(f_i\) 是最大的 \(j\le i\) 满足 \(s_i > t_j\),其中 \(t_j = s_{j-1} \times 2 - s_{f_{j-1}-1}\),\(s\) 单调递增。
转移的时候,首先注意到如果一个数比你大而且 \(t\) 还比你小,那么你就没了。其次由于 \(s\) 单调递增,可以不断从前面 pop 元素,直到如果该元素 pop 了下一个元素就大于 \(s_i\) 了。
这样就从 \(O(n \log n)\) 转移变成了 \(O(n)\) 转移。
2.2 整体转移
QOJ 1586. Ternary String Counting
一个只包含 \(\mathrm{0,1,2}\) 的长度为 \(n\) 的序列,有 \(m\) 个限制形如第 \(l_i\) 到 \(r_i\) 位中有 \(x_i\) 种数。求符合限制这样的序列的个数。
\(1 \le \sum n \le 5000, 0 \le \sum m \le 10^6\)
\(dp_{i, j, k}\) 表示填到第 \(i\) 个数,前面第一个跟它不一样的位置在 \(j\),第二个不一样的位置在 \(k\) 的方案数。
有 \(O(n^3)\) 状态,\(O(n^3)\) 做完整个 dp 的算法:
\(j=i-1\) 的时候考虑什么时候第一次出现了 \(1\)。可以从 \(dp_{j,t,k}\) 转移,\(t \in [k+1,j-1]\),或者从 \(dp_{j,k,t}\) 转移,\(t\in [1,k-1]\)。
\(j \neq i-1\) 的时候只能从 \(dp_{i-1,j,k}\) 转移。
但是这个 pull 形式很难做整体转移优化。考虑做 push。
\((i,j,k) \rightarrow (i+1,j,k),(i+1,i,k),(i+1,i,j)\)
发现这个形式挺好的,自然考虑令 \(DP_i\) 表示 \(dp_{i,*,*}\) 矩阵。
考虑从 \(DP_i\) 转移到 \(DP_{i+1}\)。发现第一种转移是不改变,第二种转移是一个行求和,第三种转移是一个列求和,并且每一个位置最多被填一次。
考虑挂在 \(i\) 上的限制的作用。就是将 \(DP_i\) 只保留一个矩形区域。
那么你可以维护每一行每一列的和,并且加点的时候给其所在行和列都标记上这个点有值;删点的时候从两边往中间删,直到删到矩形区域内。
每一个点最多被加一次删一次,所以时间复杂度 \(O(n^2)\)。
关于一些零碎,例如初值怎么设置,都可以尽量朝着兼容方向想,并不是思考的关键。
其实对于这类整体转移问题一般都是用 push,因为这样可以描述为一个矩阵的变换,比较好进行优化。