DP 入门

这些天搞了搞 DP 专题,把一些动态规划的东西总结一下。
PS:在原本的博客站里我分成了两个文章,现在决定把它们合并。
状态设计
DP+DP
考虑一个问题,如果不能简单地用一个 DP 去解决,我们可以再来一个。
当然还有的 DP+DP 的目的是平衡复杂度,在后面会有的。
Luogu3188 梦幻岛宝珠
给定 个物品,每个物品有重量和价值。一个物品只能选一次,求限重 的情况下,能装载的物品价值和的最大值。
,每个重量能写成 的形式。
题解
考虑 的值很小。
对 相同的物品可以跑朴素的 01 背包,求 表示 ,容量为 的最大价值。
然后考虑合并答案。设 表示当前考虑到 ,且 的物品占用空间为 时, 的部分空间不超过容量第 位的最大价值。
转移考虑从 里面拆掉一个 给其他物品放:
1 | for(register int i=1;i<=n;++i){ |
容斥原理
用容斥进行的 DP 不少了,还是一句话,正难则反。
基本上有两大类:
- 直接转化为全集-补集,用 DP 求解补集问题。
- 以常规的容斥原理进行 DP,或用二项式定理、容斥原理等合并 DP 的答案。
通常我们建立的求的东西和答案所需的东西为二项式反演的关系,组合数学里有时是斯特林反演的关系,毒瘤出题人可能会把它放在计算几何里形成圆反演的关系。
上一个经典问题:
Luogu4859 已经没有什么好害怕的了
给定两个序列 ,两序列无重复元素,无交。求将两个序列两两配对,使得 的组数比 的组数恰好多 的配对方案数。
。
题解
令 ,则我们只需要让 的组数为 即可。先将两个数组排序。
若一个 匹配到了比它小的 ,则对于任意 ,它匹配比它小的 的方案数会 。
这种情况貌似还算好考虑:设计状态 表示考虑到第 位,已经产生了 个上述情况的方案数,设 表示 序列里比 小的数有多少个,有:
剩下的数字是不是任意匹配?那如何得到恰好 组的答案?这是不好算的,但是至少 组的答案呢?
设 表示将 个没有按上述方法匹配的数任意分配,得到至少 组合法匹配的方案数。不难有 。
然后容斥答案即可:
1 | for(register int i=1;i<=n;++i) |
DP of DP
这是一个神奇的思想。
设计动态规划的时候,从另外一个动态规划入手,考虑另外一个动态规划的转移,挖掘性质,然后建立一个动态规划来维护这个动态规划的转移。
有时候还需要我们会建一个自动机,然后在自动机上跑 DP。
BZOJ3864 Hero meet devil
给定一个由 AGCT 组成的字符串 ,对于每个 ,求有多少个长度为 的字符串 ,满足 的最长公共子序列为 。
。
题解
考虑如何求两个字符串的 LCS,这是一个非常经典的 DP:设 表示考虑到 ,最长公共子序列的长度:
考虑这个 DP 有什么特殊性质,我们发现 。
对 差分,用差分数组当状态。设 表示长度为 的字符串与 的 LCS 的 DP 匹配状态为 的方案数。
实际转移前需要预处理出每个状态添加一个字符能转移到哪些状态。
1 | for(register int i=0;i<S;++i){ |
状态数优化
顾名思义,就是挖掘题目的性质,考虑如何优化状态数。
状态数优化还有一种精妙的实现方式:如果原 DP 的一维值域很大,但是 DP 值很小,考虑交换 DP 维度和答案。也可以实现状态数的优化。
当然更多的状态数优化还是如下几种:
- 从根本上优化:去除一个或几个维度。
- 挖掘性质,去除冗余状态。
- 离散、压缩、合并状态。
CF643E Bear and Destroying Subtrees
一个树,初始只有根。支持添加叶子或查询如下问题的答案:
每条边若有 的概率断裂,则断边后点 的子树的期望高度。
误差不超过 ,。
题解
考虑朴素 DP 有: 表示 为根的子树内,断边后深度为 的概率。转移是 naive 的。每次加入一个叶子的时候,需要从这个叶子一直跳到根,更新这条链上所有点的状态。更新状态是 的。
转移复杂度非常优秀,考虑如何优化状态。注意到精度要求为 ,因此对于一个树,断边后高度的期望值其实不大。
例如如果断边后高度达到 ,即使有 个点,概率也只有 ,对答案几乎没有影响。这些状态几乎是冗余的。
因此状态的第二维可以开得比较小,每次加入叶子往上也不需要跳到根,跳到某一级祖先就够了。
1 |
|
HDU4623 Crime
求有多少大小为 的排列满足相邻两个数互质。
。
题解
暴力 DP 设 表示最后一个数填的是 ,已经填了集合 中的数,状态数为 。太大了。不能通过。
注意到我们并不需要知道这个数具体是什么,只需要让其和上一个数不互质即可。
对每个数按照其有的质因子分类,注意到 和任何数都互质,可以单独划分到一类。
然后你就有了 类数,预处理每一类数的互质情况,设 表示最后一个数是 类,每个类剩余数字情况为 的方案数,则状态数为 ,可以通过。
这题最好写变进制状压,不要学我暴力压然后写了个高达十几维的 DP。
1 | for(register int i=0;i<14;++i) |
转移优化
所谓转移优化,你先得要有一个暴力的转移。
然后根据观察方程/打表找性质等方法选择合适的优化。如果优化不了,反思一下是不是有更好的状态设计。
决策单调性
决策单调性,顾名思义,就是在转移过程中,使 dp 数组有最优转移的下标具有某些性质(一般是单调性),从而可以用此优化 DP。
通常证明一个决策单调性非常复杂,而且一般你也想不到这个题有决策单调性。因此考场上常用的找决策单调性的做法为打表观察。
设 为状态数组, 为最优转移的决策,常见的决策单调性有两种:
- 单调。
- 四边形不等式。
若 ,则 满足四边形不等式。
若 满足四边形不等式,则 。
【数据删除】
给定两个 的网格图,横向边都是向右的。已知第一个图左侧第 个点到右侧第 个点的最短路 和第二个图左侧第 个点到右侧第 个点的最短路 。
现在要把两个图拼成 的网格图,求拼接后左侧第 个点到右侧第 个点的最短路 。
。
题解
暴力 Floyd,,复杂度为三次方。题目说是网格图,但是图本身并没有给我们,所以边权之类的也找不到性质。
如果没什么思路的话,不妨先写出来,打个表观察决策吧?不难发现,设 表示使 最小的决策点,打表发现 。为什么?
- ,对答案无影响。
- , 的最优决策在 。
- , 的最优决策在 。
考虑每次转移的时候,假设要求出 的值,且已经知道了 ,则直接二分出 ,求出 ,求可以递归了。
复杂度 。
斜率优化
可以大致分为两种:
- 转移方程写成向量点积的形式:。
- 转移方程可以转化为求最优斜率:。
不同于决策单调性,判定一个 DP 是否可以斜率优化一般直接观察转移方程。
单调栈、单调队列、平衡树、分治……乱七八糟的东西都可以维护,但是因规约不同有些还不能用(比如点集的每一维都没有单调性时,不能用单调栈维护等等)。
【数据删除】
有 个盒子,分别有 个球,但球数和盒子无法一一对应。你可以每次选一个盒子拿一个球,如果这个盒子空了裁判会告诉你。求把 个盒子取空的最坏情况下至少要拿多少球。
。
题解
显然先排序。考虑最后空盒子的集合对应在 上的下标为 ,则最坏情况下球数为:
最优策略为:每个盒子模 个球到空,剩下的盒子每个摸 个球……
状态设计就简单了: 表示考虑到第 个盒子,空了 个的最小球数:
这个式子可以化为斜率优化通式一。单调队列优化即可。
1 | inline double slp(int k1,int k2,int j){ |
DS 优化
通常见到一个 DP,如果你看起来觉得它非常能 DS,那么它就可以 DS。
如果遇到一个题,它本身看起来像 DS,但它也可能是 DP+DS 优化。
有些东西看起来就不像是 DS 只像是有更优的状态/转移,它还是有可能是 DS 优化 DP。
CF1129D Isolation
给定序列 ,将其划分为若干段,使得每段恰好出现一次的元素个数 ,求方案数。
。
题解
这个暴力 DP 的思想就更简单了:
考虑怎么判 ok。记 为 这个区间内只出现一次的数的个数。所有可转移的 就是满足 的 。而 的维护:每次 加一时相当于对 进行区间加和区间减。
问题转化为:对 区间 ,维护集合 。
考虑分块。给每个块开一个桶,整块修改打一个整体加减标记,局域修改暴力重构;整块查询直接查,局域查询暴力差即可。时间复杂度 。
1 | inline void clear(int x){ |
wqs 二分
如果直接设计状态发现复杂度爆炸,考虑一维是否有单调性,用 wqs 二分配合起来实现 DP 的优化。
具体而言,如果你要最优化的数和一些别的数存在凸函数关系,就可以考虑用 wqs 二分优化。这个思想可以运用在简单的二分一个维度,另一个维度用最优化类型的 DP 解决,还可以把题目转化为凸包问题求解一些奇怪的凸优化问题上。
【数据删除】
一个 个点的图,每条边有两个权值 。求一个生成树,最大化 。
。
题解
貌似不好用朴素的 wqs + DP 的方法去解决它?
考虑第二个模型,如何把这个题变成一个凸包?
有凸包就要有二维平面,先整一个二维平面出来。将一棵树的 看作横纵坐标。用 wqs 二分求出这些点的凸包。显然答案一定在凸包上。
复杂度?凸包大小不超过最大权的 次幂,因此复杂度为 。
矩阵、多项式
矩阵和经典多项式的优化都见怪不怪了,一般是用于递推式子的。
如果你看到这个题的模数非常 NTT,而且转移式子非常 NTT,或者转移式非常线性递推或非常卷积,数据范围非常奇怪,可以尝试往这个方面想一想,
当然也并不是所有的多项式优化 DP 的题都这么明显。还可能用生成函数、卷积过程中的性质辅助算法的瓶颈优化。
Luogu3746 组合数问题
有 个不同的物品,求选出的物品数模 为 的方案数。
。
题解
暴力转移设 表示选到第 个物品,选了的物品数量模 为 的方案数:
显然可以用矩阵优化,观察转移方程不难得到:
复杂度 。
1 | struct matrix{ |
【数据删除】
对于一个序列,令 。若 不存在,默认 ;若 不存在,默认 。
多组询问,每次给定 ,查询有多少大小为 的排列满足:
。
题解
暴力设 表示长度为 的排列,权值为 的方案数。枚举排列最大数的位置,将其拆成两半:
状态两维转移平方,复杂度不得爆炸。貌似没有其他方法做了,考虑生成函数优化。
设 为 的生成函数。有:
考虑 次数的范围,显然当 时答案才不是 。因此 次数不超过 ,就可以卷积了。
一开始就把 转成点值,每次查询时用拉插还原多项式。复杂度为 。
其他 Trick
随机化
对于数据规模小的时候,可以用一些诸如高幂级 DP、状压 DP 等做法解决的问题,数据规模较大时,可能可以通过随机化规约成小问题解决。
对于答案函数不具备单调性的时候,不能用朴素的二分/三分等做法,用 DP 来判定,其可能可以用模拟退火等做法模拟二分/wqs 二分,从而配合 DP 得到答案。
BZOJ5232 好题
给定一个节点带颜色的树,求最小的包含 种颜色的连通块。
。
题解
如果颜色数很少怎么做?
对于颜色数很少的情况, 表示考虑到 号点,包含这个点且颜色状态为 的最小连通块大小。转移显然。
考虑颜色很多的情况,把所有颜色随机 Hash 成 的颜色,跑上面的做法。重复若干次取最优答案。
正确性?
显然不会求出比正解更小的解出来。而对于正解连通块,其一定恰好包含 种颜色。一次 Hash 跑一遍每个颜色都可以变成 中任意一个数,若这些数两两不同则可以找到正解。因此一次 Hash+DP 正确的概率为 。在 的情况下,这个数很小。
1 | inline void dfs(int u,int fa){ |
复杂度平衡
这个词多用于 DS,但是 DP 也有这样的思想。具体和 DS 类似:考虑一种方法实现的转移在某方面很快,某方面很慢;另一种方式恰恰相反,则可以考虑如何将这两种方法进行结合,从而达到平衡复杂度的目的。
复杂度平衡的方式也因题而异。相对常见的比如直接将两个 DP 结合在一起。
Luogu3773 吉夫特
给定元素两两不同的序列 ,求有多少长度不小于 的子序列满足:
。
题解
记 。
考虑 Lucas 定理:
不难发现这是二进制拆位的过程。这个条件可以转化为 的二进制是 的子集。于是有暴力 DP: 表示考虑到第 个数,找出的包含 的合法子序列个数:
复杂度 。
考虑如下两种可行的优化方法:
- 记 。每次增加一个数时 修改,转移 枚举子集。
- 记 。加数 枚举超集修改,转移 查询。
这两种单独使用复杂度都是 ,考虑优化:
- 转移时,固定 二进制后一半,前一半枚举子集。
- 查询时,固定 二进制前一半,后一半枚举超集。
然后 一起用。复杂度 。
1 | for(register int i=1;i<=n;++i){ |
转移建图
AGC005D ~K perm counting
有多少大小为 的排列满足 ?
。
题解
考虑容斥,设 为至少有 个位置不符合条件的方案数。答案为:
考虑到对于一个数 ,使其不满足条件的位置不超过 个。将 向让 不合法的位置连边,就可以得到 条链:
对每条链分别 DP: 表示前 个点有 个不符合条件, 有没有选, 有没有选的方案数。转移是分类讨论。
1 | int tot=0; |
转化特殊情况
hiho1315 Reaching Permutations
给定一个大小为 的排列。一次操作可以选出一个逆序对并交换。求经过若干次操作后可以得到多少不同的排列。
。
题解
不妨考虑一下 01 序列的情况吧。
正常情况是不好考虑的,考虑 01 序列。排列 能经过若干次操作到达 当且仅当 , 中第 个 都不在 中第 个 的左边。
将 转化成 个 01 序列 。其中 。显然排列和 01 序列双射。而 能到 当且仅当 能到 。必要性显然,充分性考虑: 枚举 ,若要把 从 移到 ,每次找到 中最大数所在的位置 ,交换 。
于是做一个 01 串上的 DP:从全 串开始,每次转移将一个 改成 ,到达全 串。复杂度 。
1 | for(register int s=0;s<S;++s) if(f[s]){ |
- 标题: DP 入门
- 作者: Arahc
- 创建于 : 2022-06-21 08:00:00
- 更新于 : 2023-03-16 13:55:34
- 链接: https://arahc.github.io/2022/06/21/【笔记】DP-入门/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。