author: Hope666666
引入
本文将介绍 DP 套 DP 的思想,并通过两道例题展示它如何应用到具体问题中.
思想
所谓「DP 套 DP」,实际上指的是在动态规划的过程中,把一个子问题的求解过程(通常是一个 DP)抽象成一个自动机(DFA),然后在这个自动机的基础上再设计一层新的 DP 的方法.
这种技巧主要应用于一类 序列计数、概率 或 期望 问题.一个典型的问题具有如下结构:
- 给定字符集 和它上面一个长度为 的「合法序列」的集合 .根据字符集不同,序列可以是二进制串、数字串、状态序列等.
- 对于每一个具体的序列 ,可以通过动态规划来判断它是否合法(即 ),计算它的权值或求出相关值.
- 最终,我们希望统计集合 中所有序列的数目、总权值、期望值等.
这时,枚举所有序列是不可行的.于是我们考虑将「判断一个序列是否合法」的过程(即内层 DP)抽象为一个 确定有限状态自动机(DFA).一般地,对于固定的序列 ,内层 DP 的状态函数可以表示为 ,即已经处理完序列 的长度为 的前缀,且其他状态分量为 时某个量的取值.相应地,内层 DP 的状态转移方程为
也就是说,函数 由之前的函数 和当前的字符 唯一确定.如果将函数 看作是自动机的一个状态,那么,内层 DP 的状态转移方程就给出了自动机的一个转移.因此,内层 DP 对应的自动机 结构如下:
- 状态集合 就是所有可能的 和 对应的函数 的集合;
- 转移函数 就是内层 DP 的状态转移方程中的 ;
- 起始状态 通常是显然的,即内层 DP 的初始状态;
- 接受状态集合 就对应着所有的合法序列 .
函数 本身可能相当复杂,因此,在处理具体问题时,通常需要进行 状态压缩 或结合 DFA 最小化的技巧来压缩状态空间.这也是 DP 套 DP 相较于暴力 DP 能够显著降低时空复杂度的主要原因.
将内层 DP 抽象为 DFA 后,就可以在这个 DFA 上设计一个新的 DP 用于求解原问题,即外层 DP.为方便表述,以简单的计数问题为例.外层 DP 的状态函数定义为 ,即处理到长度为 的前缀时,到达 DFA 中状态 的前缀的数目.它的状态转移方程为
起始状态当然是 ,而最终要求的答案通常可以根据 简单计算得到.外层 DP 实际上是 DAG 上 DP 的特殊情形.
例题
接下来的两个例题会详细说明 DP 套 DP 的一般做法.
例一
???+ example “Hero meet devil”
给定一个字符集为 ACGT 的字符串 ,且 .对于每个 ,求有多少个长度为 ,字符集 ACGT 的字符串 ,满足它与 的最长公共子序列长度为 .
??? note “题解”
我们首先会想到一个 DP:设 表示长度为 的 中,和 的最长公共子序列的长度为 的方案数.但是这样无法转移,我们发现主要的问题是,我们不知道这个最长公共子序列对应的是哪些字符.
考虑朴素求最长公共子序列的过程.设 表示 的前 位和 的前 位,它们的最长公共子序列的长度,就有
我们发现,对于一个 ,只需要记录 这个一维数组每一位的值,就能准确维护当前 与 前 位最长公共子序列的状态.因为 长度只有 ,我们发现这个思想是可行的.
于是重新设状态 表示对于长度为 的 ,与 的 DP 数组(就是 )状态为 的方案数.这个 DP 看起来状态数很多,然而我们发现 ,就可以维护 的差分数组,状态数是 的.
现在思考怎么转移.容易发现,如果我们知道了 这个数组,也知道了 ,就能通过朴素 LCS 转移(即前文的 DP 方程)求出 .于是朴素的 LCS 就成为了帮助 转移的内层 DP.
因此,我们枚举 ,计算出 转移后的状态 ,再将 加上 就可以完成外层 DP 的状态转移.最后,我们记录 为 LCS 长度为 的答案,枚举每个状态 , 加上 即可.
??? note “参考代码”
--8<-- "docs/dp/code/dp-of-dp/dp-of-dp_1.cpp"例二
???+ example “[ZJOI2019] 麻将”
假设麻将牌有 种大小的牌,每种大小有 张牌.定义面子为三张相邻大小的麻将牌 (顺子)或三种相同大小的麻将牌 (刻子),对子为两张相同大小的麻将牌 .定义一个麻将牌的序列是胡的,当且仅当它(看作多重集合)可以拆成四个面子和一个对子,或者七个不同的对子.给定 张麻将牌,问期望再摸多少张牌可以满足存在一个胡牌的子序列的条件.
??? note “题解”
首先,对于一副牌,我们只需考虑每种牌的数量,而不必关心它们的顺序.因此,对于任何一副牌的任意前缀,我们都可以将它转化为一个长度为 ,每个位置上为 的序列.初始时,第 张牌的数量为 ,就相当于限制了序列中第 个数字 的取值为 内的整数.注意,转化后的序列没有考虑摸牌的顺序,但是题目中的麻将牌的序列是考虑顺序的.
设 表示可以胡牌的最小摸牌次数.直接计算期望 较为困难,可以考虑进行如下转化.设 表示摸了 张牌后 没有胡 的序列数.因为它对应的麻将牌序列中,这 张牌必然排在剩下的 张牌前方,但是这 张牌和剩下的 张牌的顺序是任意的,所以,只摸前 张牌无法胡牌的麻将牌序列的数目就是
因为麻将牌序列的总数为 ,所以,只摸前 张牌无法胡牌的概率为
利用尾求和公式,就可以得到所求期望
至此,问题转化为如何计算 .我们采用 DP 套 DP 的方法来解决这一问题.
首先,考虑内层 DP,也就是要用动态规划判断一个(转化后的)序列是否对应一副能胡的牌.七对子的情形较为容易,重点讨论第一种胡牌的形式.为此,设 表示处理完前 种牌,还剩 组 以及 张 ,且存在/不存在对子(即 )时最多的面子数.如果对于一个序列进行 DP,最后得到的 中包括一个大于等于 的数字,那么这个序列就是能胡的.
这个 DP 的状态转移较为复杂.我们分两步讨论.第一步,考虑 的转移.这相当于说,如果要向现在的牌型中,添加 张大小为 的牌,但是不组成新的对子时,面子数如何转移.显然,如果希望在添加 张大小为 的牌后,要得到 个顺子, 个 和 个单独的 ,那么,就应该从 中转移过来(这里的选择最大程度避免了浪费),并将剩余的牌 用于组成尽可能多的刻子.穷举所有的可能性,就得到如下转移方程:
第二步,再考虑需要组成对子的情形.加入 张大小为 的牌时,有如下三种转移:
- 将 加 张牌转移到 ;
- 将 加 张牌转移到 ;
- 若 ,将 加 张牌转移到 .
由此,就得到了从 添加 张牌得到 的全部转移.
解决了内层 DP 的状态转移,就可以构建 胡牌自动机.自动机的转移就是上述内层 DP 的转移,还需要考虑如何表示自动机的每个状态.每个状态都对应 的一种可能的取值.它有三个维度 .因为 和 对应的维度中保留的 和 都是用于将来组成顺子的,而三个相同顺子总是可以重组为三个刻子,所以,只需要考虑组成不超过 个相同顺子的需求就行,每种牌型也就只要保留不超过 个,即 .因此, 可以表示为一个 的数组.另外,为了维护七对子的胡牌牌型,还需要为每个状态添加一个计数器,用于表示当前最多可组成的对子数目.
数组 中每个元素的取值范围可能是 ,但是,因为面子数目大于等于 都是胡牌,所以,可以限制每个元素取值不超过 .由于胡牌序列再添加任何牌都是胡牌序列,所以,可以利用 DFA 最小化的思想,将全体胡牌状态压缩为一个状态.因此,对于非胡牌状态,实际上每个位置的取值只要考虑 就可以了.实现时, 用 表示.
尽管如此,所有可能的状态依然相当地多,共有 种.穷举它们并不现实.实际上,绝大多数这些可能性都不会真的出现在一个胡牌自动机中.为了避免考虑实际不存在的状态,可以利用 BFS 的思想,从初始状态开始,一步一步扩展状态,直到胡牌状态处停止.这样得到的自动机中有 个状态.
最后,考虑如何在胡牌自动机上 DP(即外层 DP).设 表示处理到第 张牌,共摸了 张牌,走到了胡牌自动机上的 号状态的序列数.转移时,枚举摸牌数 ,其中 为初始 张牌中用掉的 的张数,将之前的序列数乘以 张牌中选 张牌的方案数 ,再累加到一起.形式化地,有:
其中,,表示向自动机的状态 中加入 张牌后的状态.外层 DP 结束后,就可以计算出摸了 张牌仍然没有胡牌的序列数目,即
代入前文所述表达式,就可以得到所要求的期望.
??? note “参考代码”
--8<-- "docs/dp/code/dp-of-dp/dp-of-dp_2.cpp"习题
多平台练习
| 洛谷 | 本题单 | 竞赛基础 |
| POJ (北大) | PKU JudgeOnline | 经典题目,适合巩固 |
| HDU (杭电) | HDU OJ | 暑期多校训练 |
| Codeforces | Codeforces | 国际竞赛,适合提升 |