[萌新向] 基础dp 笔记

dp基础

最基础的dp,侧重于讲一种思维方式,比较宏观

至于各种形式的dp,各种优化,甚至于如何写出来代码,不是本文的重点

具体的dp转移,可能不会给出

基本内容:本质与定义

dp:dynamic programming, 动态规划

dp的本质: 我们有一个要解决的问题,把问题分成若干步(dp的阶段), 每一步都有一个状态表示(dp的状态),然后考虑状态间的转移。

dp把问题通过某种顺序解决,使得我们可以 不记录 一些(通常来说,很多)东西,只记录我们 关心 的东西,从而比较高效的描述状态

所以,我们可以说,所有的dp都会有一个“状压”。只不过普通的dp压缩的比较浅显,而使用二进制的状压dp压的比较复杂,才叫它“状压dp”

就好比背包dp中,我们不暴力存下选择的物品的集合,而只记录选到第几个,占用多少空间,这也算一个压缩。如果不压缩,那dp数组可能是一个 map<set<int>,int>

起飞

确保基础知识理解之后,更多更难的dp题似乎就迎刃而解了

dp套dp

现在我们有一个问题,可以用dp解决。

那如果我们现在反手问一下,有多少个问题,跑一遍dp,能得到某个输出?

注意到这个dp(下面称为“内层dp”)已经帮我们划分好了阶段。我们直接以它划分的阶段为阶段,跑一遍dp。

dp的本质,特别适合被用来dp!

形式化一点讲,我们可以这样设计 \(dp\)

\(dp(u,S)\) 表示,在内层dp的第 \(u\) 个阶段,到达了 \(S\) 状态,方案数。然后依着内层dp的转移,进行外层dp的转移就行了。

详情见 这篇

选东西,带限制

这一类问题就是,你需要选择一些东西,但是有些限制,如xxx不能选,xxx不能同时选,...

解决这一类问题,需要观察限制的性质,然后设计一个处理问题的顺序,还有一个合适的状态,解决掉这个限制

如果限制长得不好看,那就转化一下,把它变好看

NOI2015 寿司晚宴

俩人选数,要求都得互质

互质 → 质因数集合没有交

注意到,\(\ge \sqrt{n}\) 的质数顶多被一个人选一次。而 \(\le \sqrt{n}\) 的质因数,最多才 \(8\) 个。称那个 \(\ge \sqrt{n}\) 的质因数为 “大质因数”

限制的性质

把所有数按大质因数分类。对于同一种大质因数里面的,分类讨论:给A,或者给B

处理的顺序

对于小质因数,暴力记两个bitmask状压即可。

合 适 的 状 态 (迫真)

详解

十二省联考2019 皮配

我们发现某些学校有特殊偏好。但是我们发现这个数量非常少,所以,对于没有偏好的学校,可以先跑一遍不带限制的dp,再考虑这些有偏好的学校

有偏好的学校,就分类讨论一下所在的城市进哪个阵营,然后看学校如何选择导师即可。

把受限制的和不受限制的分开解决

这仅限一部分问题

这题的思维过程是挺套路,挺典型的。只是代码细节很多

详解

删东西,记剩下

我们每次要删掉一些东西,然后要决策怎么删,或者是求删剩下的结果。

我们可以用dp,记录当前删完剩下了啥。因为剩下的东西会继续反应,而删掉的就没了,所以我们只需要去关注剩下了的信息就行

对于剩下的信息,也可能要适当压缩一下。毕竟不是剩下的所有信息我们都关心。

EllysTournament

这题意,这数据范围,仿佛把 “区间dp” 四个大字写在了脸上

那我们需要关心的是,区间 \([l,r]\) 里面比,每个人最终获胜的概率 ... 每个人吗?

可以是可以,完全没问题,但是开销太大了。

其实,我们只需要关注两端点(挺套路的转化)。对于 \([l,r]\) 中的第 \(k\) 个赢下,可以看成是在 \([l,k]\) 里面的最右边赢下来,然后在 \([k,r]\) 里面的最左边赢下来。

那就设 \(L(l,r),R(l,r)\) 表示 \([l,r]\) 里面 \(l/r\) 最终获胜的概率。枚举最后剩下的两个人,里面的另一个人,然后想一想 \(l/r\) 这个人怎么才能赢下,写出转移就行了

[THUSC2016]成绩单

同样考虑区间dp

我们关心什么信息呢?

注意到,我们甚至不关心当前剩下了几个,只需要知道min/max就行

\(f(l,r,L,R)\) 表示,区间 \([l,r]\) 删完之后,数的值域在 \([L,R]\) 里面,最小的代价。

这玩意看起来很好转移,然后就瞎几把做一波就没了

交换坐标轴

有时我们需要维护 \(f(x)=y\) 这样的一个函数

如果这个函数具有特殊性,并且 \(y\) 的范围小,\(x\) 的范围大,那我们可以转而考虑 \(y\)

大重量小价值的背包

这是我胡的一个题。以01为例吧

每个物品有重量 \(w_i\),价值 \(v_i\),背包是01的,总容量是 \(C\),表示重量的和不能超过 \(C\)

如果 \(w\) 的范围小,\(v\) 就算是 \(1e9\) 也能做

\(f(w)\) 表示,如果装的重量不超过 \(w\),最大收获的价值和

那现在反过来,如果 \(v\) 的范围是 \([0,50]\),而 \(w\) 的范围是 \([0,1e9]\),咋搞?

反过来,设 \(f'(v)\) 表示,要达到价值 \(v\),所需的最小的重量

我们发现,这个 \(f\)\(f'\) 正好反了一下,这就是我们所说的 “交换坐标轴”

发现 \(f'\) 同样很容易转移。最后就找一下最大的 \(v\) 使得 \(f(v)\le C\) 就行了。

某zr题

给两个串 \(S,T\),每次给 \([l,r]\),求 \(S[l:r]\)\(T\) 的最小编辑距离。\(|S|\le 10^5,T\le 20\)

参考 dyls的博客

观察他的博客,我们看到这一句话

继续优化,前面说过,\(F(x)\) 有一个很好的性质,就是值域很小。所以考虑将 DP 的下标与值域互换。

这就是我们讲的交换坐标轴的技巧。

这里也有另一种方法:我们自己来 构造 一个函数,使得它满足换轴的条件,我们再换轴维护。

至于这个 \(F\) 是如何想到的,可能得问问 dyls。这里给我个人的看法:

注意到两个串 \(A,B\),长度为 \(n,m\),它们的编辑距离在 \(|n-m|,|n+m|\) 范围内。

我们发现它的区间跨度是 \(2\times \min(n,m)\)

这个很好证,发现这个东西关于 \(n,m\) 对称,不妨令 \(n<m\),拆开得到跨度是 \(2n\)

放在本题里,它的跨度只有 \(2|T|\) 这么多,但是区间范围很大,是因为加了一个 |S| 上去。我们把这个 |S| 减掉,那它的值域就是 \(-|T|,|T|\) 了。

所以我们才考虑 \(F(i)=i-h(i)\) 这样一个函数。同时,考虑 \(h(i)-i\) 也是可以的。前者增,后者减,只有一些微小的区别

小结

对于一个dp题,我们有时会觉得它复杂度太高。

此时,我们可以通过 观察,看看 哪个东西比较小,然后联想一下,能不能把复杂度往这里倾斜。这里用到的一个调复杂度的方法就是换轴,我们把下标,值两个东西反过来看,减小复杂度

naive-dp的变形

naive-dp指我们学dp的前几节课就学到的那些经典题

上面提到的 “大重量小价值的背包”,算是背包的变形。背包的变形我们见的多,这里不讲

编辑距离相关

上面提到了一个题,这里再讲一个题,BJOI2015隐身术

考虑最naive的编辑距离dp:\(f(i,j)=\min(f(i-1,j),f(i,j-1))\)。而当 \(s_i=t_j\) 时,\(f(i,j)=f(i-1,j-1)+1\)

我们可以把它看成是两个指针 \(p,q\)。当失配的时候,其中一个跳一步;否则,两个同时跳一步

我们注意到 \(k\le 5\),所以只会有 \(5\) 次失配。不失配的时候,用后缀数组直接匹配,否则就暴力搜,就行了。

LIS相关

LIS最naive的dp是两次方的

这里考虑一个不那么naive的dp,设 \(f_i\) 表示,LIS要到 \(i\),末尾的数最少是几

这是LIS的nlogn做法之一。每次我们贪心的lowerbound,更新 \(f\)

但是这个更新未免太简单了,让我们不禁想到:能不能更带劲点啊?

于是就有了下面的题:CF809D

相当于我们每次要把一个区间的数加到这个 \(f\) 里面去。

首先,对于 \(f_i<l\)\(f'_{i+1}\) 可以用 \(l\) 去更新。即,若 \(f_i<l,f_{i+1}\ge l\),令 \(f_{i+1}=l\)

对于 \(f_i\in [l,r)\),我们可以往新序列里面加 \([l,r]\) 中的任意整数。我们注意到 \(f_i+1\) 肯定在这里面,所以可以用 \(f_{i}+1\) 更新 \(f'_{i+1}\)。注意到 \(f\) 是增的,所以完全可以直接令 \(f'_{i+1}=f_i+1\),啥事没有

那现在我们要干的事情就是,找到分界点 \(i\) 使得 \(f_i<l,f_{i+1}\ge l\),把 \(f_{i+1}\) 变成 \(l\);再找一个 \(j\) 使得 \(f_j<r,f_{j+1}\ge r\),把区间 \((i,j]\) 里的数 \(+1\) 并向后挪一位

向后挪一位,非常的熟悉啊,就是插入操作。由于前面正好有一个变成 \(l\) 的操作,我们可以直接看成是插入了一个 \(l\) 进去。然而这样长度不对,手玩一下,\(j\) 后面的那一个位置(如果有)要删一下。

然后就平衡树维护就行了

小结

对于这种经典题变形类的题,可以直接考虑原问题的dp,观察它的形式和性质,结合实际问题用一些东西优化两下子

(这里用到的“一些东西”,分别是:后缀数组;平衡树)

不要的不看

上面提到,我们的dp只关注需要关心的信息。对于不关心的信息,就不必记录在状态里

hdu6652 Getting Your Money Back

看到区间,很容易想到区间dp:设 \(f(l,r)\) 表示我们要取出的钱在 \([l,r]\) 范围内,最坏情况多少额外花费。

它的复杂度爆炸,\(n^3\)(设 \(n=r-l\))。别急,看我给它砍到 \(O(n)\)

首先发现,假设我们取了 \(k\) 块钱 \(l\le k\le r\),接下来的转移只和 \(k\)相对位置 有关,也就是 \(k-l+1\) 的值。为啥?因为我们取一次钱的额外花费只和相对大小有关,这是显然的。里面存 \(100\) 块我取 \(105\) 块,和里面存 \(10\) 块我取 \(15\) 块,都会得到一个 fail 然后花费 \(b\) 块钱,这和 \(100\) 还是 \(10\) 无关

所以,我们不需要管它 \(l,r\) 具体多少,只要有个长度就行。那我们的区间dp只需要记长度就行了。此时,砍到了 \(O(n^2)\) 。注意一个细节是,左端点是否为 \(0\) 也有影响,要记一个 \(f(i)\) 一个 \(g(i)\) 表示左端点不是/是 \(0\),长度为 \(i\) ,最优策略最坏情况额外花费。

Hint:这个“是不是0” 的本质是在考虑“左边那些要不要取”。想明白这个,下面的转移就能明白了

推一下 \(f,g\) 的转移式:

\[f(i)=\min\{\max(b+f(j-1),a+g(i-j))\} \\ g(i)=\min\{\max(b+g(j-1),a+g(i-j))\} \\ \]

内层 \(\max\) 表示取最优情况,外层 \(\min\) 表示取最优策略,里面 \(b+...\) 表示取失败,\(a+...\) 表示取成功。这是非常自然的一个转移

一增一减取 \(\max\),显然凸,来一个三分。尽管不太能过(网上说法,我自己没写),但理论上已经到 \(\log\) 了,也很优秀

但是这东西还有一个性质就是它满足决策单调性。很容易想到,随着 \(i\) 的增加,这个凸壳凸起的那个点肯定是往右动的,所以就单调的增加 \(j\) 就行,复杂度严格线性。

CF1103D Professional layer

先把 \(\gcd\) 求一遍。接下来,只有 \(\gcd\) 的那些因子是有用的,其余质因子去掉即可

对于每个质因子,我们肯定只会选一个数来除。毕竟一个数除就能达到目的,选两个也没必要,还白花钱

假设一共有 \(m\) 个质因子。容易发现 \(m\le 11\)。那么我们最多肯定只选 \(m\) 个数。

对于一个质因子,如果没别的限制,我们肯定选花费最小的那个除。但它可能已经被占掉了,再除就会超过 \(k\) 的限制。

但是我们发现它最多被占掉 \(m-1\) 次。此时我们取第 \(m\) 小的就行。

所以,对于每个质因子,我们保留前 \(m\) 小的就行。

再进一步想,对于选择的一个质因子集合,要想除掉这一整个集合里的质因子,也只需要选能除这些质因子的数中,前 \(m\) 小的那些。

所以我们实际上只关心 \(m\times 2^m\) 个数。

我们可以先按花费排序,然后跑一个状压dp,设 \(f(i,k,S)\) 表示考虑前 \(i\) 个,选了其中 \(k\) 个数除,搞掉 \(S\) 集合里的质因数,最小花费。

看起来状态数很多,但是我们可以用短短几个if排除掉大量状态。我们实际保留的状态只有 \(m\times m\times 2^m\) 种。

接下来可以枚举子集暴力转移。复杂度 \(O(m^23^m)\)。由于 \(m\) 小的离谱,直接过了

小结

如果你发现你会一个dp,但是时间复杂度有亿点大,可以想想哪些东西不要,就丢掉,从而优化复杂度

与贪心结合

dp是一个非常精确的东西,它无敌对,太对了。但是和贪心一比,又显得非常的慢

于是有这样的一个处理技巧,当数据非常大的时候,用贪心解决问题,把数据降下来;数据小的时候,再用dp解决问题

经典题:马跳棋盘

象棋的马,大家都比较熟悉。形式化的描述它的运动,就是每次可以在 \(x,y\) 坐标里,选一个 \(\pm 1\),另一个 \(\pm 2\)。每次有 \(8\) 种跳法。

现在它从 \(0,0\) 出发要跳到 \(x,y\)。问最小跳多少步。\(0\le x,y\le 10^{18}\)

如果 \(x,y\le 1000\),那是傻逼题,在棋盘上写个dp就行

如果 \(x,y>1000\) 咋办呢?我们注意到此时 \(x,y\) 非常的大,宏观上,有如下贪心策略:把 \(x,y\) 里面绝对值大的那个 \(\pm 2\),小的那个 \(\pm 1\)。容易发现,当 \(|x|,|y|\)\(1000\) 往上的时候,这样的策略肯定是对的。我们可以用一些除之类的计算,来优化这个过程到 \(O(\log)\)

\(|x|,|y|\) 都在 \(1000\) 以内的时候,直接调用dp数组的值。

至此,我们做到了 \(O(\log \max(x,y))\) 的复杂度。

arc096d Sweet Alchemy

就是上面提到的那个“大重量小价值的背包”,多重背包版本。不但是个多重背包,选择的上限还是 \(1e9\)

我们发现,在大范围内,我们选性价比高的那个贪心是正确的。可以自己先想一下,这个范围是多少,以及为什么对,再看下去

这个范围是,当选的数 \(>n\) 个时,选性价比高的一定优

如果有两个物品 \((w_1,v_1)\)\((w_2,v_2)\),满足:

\(\dfrac{v_1}{w_1}\ge \dfrac{v_2}{w_2}\)

此时假设我们选了至少 \(w_1\)\(2\) 物品。我们把这 \(w_1\)\(2\) 物品,换成 \(w_2\)\(1\) 物品,显然总重量不变,而价值变大了。

此时我们得到,当选的数 \(>max(w)\) 个时,确实性价比高的优。但是 \(max(w)\) 太大了,换一个角度考虑

假设我们选了至少 \(v_1\)\(2\) 物品。把它们换成 \(v_2\)\(1\) 物品,显然总价值不变,而重量变小了,同样也是更优的

但此时我们得到了更爽的一个界:只要选的数 \(>max(v)\) 个,就一定性价比高的优

\(v\) 就是子树 \(size\),所以 \(max(v)\) 就是 \(n\)。证毕

在小范围内,再跑一遍上面提到的那个换轴dp,这题就没了

总结

dp的技巧非常多,就算不涉及到插头dp,斜率优化,四边形不等式优化...等等这些高级技巧,就单纯考思维能力,也有着许许多多的变数

思维一定要灵活,找到问题的本质并想办法优化求解的方法

所以,不要停下来啊

posted @ 2021-08-02 23:47  Flandre-Zhu  阅读(261)  评论(0编辑  收藏  举报