DP 概论

对于一个题目,我们有暴力搜索算法。而 dp 就是尽可能将同一类的东西合并到一个状态上。

如何检查 dp 的正确性?

  1. 检查集合内部转移是否一致。
  2. 检查方案数是否正确。

1 基础 dp 方向

1.1 状压 dp

有好几种状态压缩的方式:

  1. 记录“选了哪些东西”这样的信息,可以用一个长度为 \(n\) 的 01 串,对应一个十进制数。
  2. 记录可重无序集合:如果不关心顺序,只关心它们分别出现了多少值,可以记录无序集合,如果集合内的数字之和保证小于等于某一个数 \(S\),那么状态的个数小于 \(S\) 的分拆数,大概 \(53\) 以下的分拆数能过。
  3. 网格图,考虑一行一行转移,然后要记录 \(1 \sim i\) 行的信息,比如黑色格子组成的连通块,使用最小表示法:考虑同一个连通块上所有点用一个数字来表示,但是从左到右数字第一个出现的位置符合升序。这样就可以一格一格转移。
  4. 插头 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。

一般用来解决这样的问题:

  1. 如果给定一个字符串,可以使用 dp 判断它是否合法。
  2. 需要求每个位置在许多字符中任取的时候有多少合法的串。

我们的方法是:直接对记录状态的那个 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,因为这样可以描述为一个矩阵的变换,比较好进行优化。

posted @ 2023-04-26 20:25  OIer某罗  阅读(62)  评论(0编辑  收藏  举报