DP 概论

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

如何检查 dp 的正确性?

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

1 基础 dp 方向

1.1 状压 dp

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

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

给我的一个教训是:如果某一个状态,要么要加入进状态里面,要么要 dp 它,都是躲不开的,那么考虑加入进状态可能会比 dp 它少判断很多东西,并且更优。

HDU2484

这题是一个树形期望 dp,对每一个 minsum 相同的元素,其 fa 的 dp 值可能不一样,但是其 a,b 值是一样的,我们就可以记忆化搜索,只需要 n2 种状态而不是 2n 种。

为了优化 dp 的复杂度,需要找等价的 dp 状态。为了找等价的 dp 状态,考虑 dp 的转移具体和什么有关,有没有办法从状态里去掉不需要的东西。例如此题,虽然往回的状态需要整个 mask,但转移的系数只和后面的和有关。

2 基础 dp 优化

2.1 单调队列优化

考虑这样的 dp 式子:
fi 是最大的 ji 满足 si>tj,其中 tj=sj1×2sfj11s 单调递增。
转移的时候,首先注意到如果一个数比你大而且 t 还比你小,那么你就没了。其次由于 s 单调递增,可以不断从前面 pop 元素,直到如果该元素 pop 了下一个元素就大于 si 了。
这样就从 O(nlogn) 转移变成了 O(n) 转移。

2.2 整体转移

QOJ 1586. Ternary String Counting

一个只包含 0,1,2 的长度为 n 的序列,有 m 个限制形如第 liri 位中有 xi 种数。求符合限制这样的序列的个数。

1n5000,0m106


dpi,j,k 表示填到第 i 个数,前面第一个跟它不一样的位置在 j,第二个不一样的位置在 k 的方案数。

O(n3) 状态,O(n3) 做完整个 dp 的算法:

j=i1 的时候考虑什么时候第一次出现了 1。可以从 dpj,t,k 转移,t[k+1,j1],或者从 dpj,k,t 转移,t[1,k1]

ji1 的时候只能从 dpi1,j,k 转移。

但是这个 pull 形式很难做整体转移优化。考虑做 push。

(i,j,k)(i+1,j,k),(i+1,i,k),(i+1,i,j)

发现这个形式挺好的,自然考虑令 DPi 表示 dpi,, 矩阵。

考虑从 DPi 转移到 DPi+1。发现第一种转移是不改变,第二种转移是一个行求和,第三种转移是一个列求和,并且每一个位置最多被填一次。

考虑挂在 i 上的限制的作用。就是将 DPi 只保留一个矩形区域。

那么你可以维护每一行每一列的和,并且加点的时候给其所在行和列都标记上这个点有值;删点的时候从两边往中间删,直到删到矩形区域内。

每一个点最多被加一次删一次,所以时间复杂度 O(n2)

关于一些零碎,例如初值怎么设置,都可以尽量朝着兼容方向想,并不是思考的关键。

其实对于这类整体转移问题一般都是用 push,因为这样可以描述为一个矩阵的变换,比较好进行优化。

posted @   OIer某罗  阅读(70)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示