一类动态规划问题状态的简化
DP学习心得—状态的设计与简化
其实是带着一些奇怪的想法找了点题目。
动态规划由两个重要的部分组成:转移方程与状态。
好的状态是动态规划能够发挥威力的重要前提,而好的转移方程则是动态规划发挥作用的途径与方式。
因此,通过一段时间的学习,本人试着总结出一些动态规划问题的状态上的设计思路与巧妙化简,并写下这篇总结供日后学习所需。
Part 1 状态的设计
好的状态是动态规划的开始,一个动态规划问题中的状态应该首先满足:
- 能够准确地描述对转移有影响的状态维度,不存在不同转移情况的混淆(即转移过程不遗漏,不混淆)。
- 不具备后效性,即不会存在互相转移的情形。
- 能够满足转移时最优性的传递(即最优子结构性)。
在满足了以上几点后,我们基本可以得到一个保证正确性的状态。
但是有时候,仅仅这样是不够的,我们的状态可能会显得过于冗杂,此时则需要我们用一定的技巧来转变思考角度,减少冗杂的成分,来让转移更加的轻简。
Part 2 状态的简化
状态的优化大致上分为两种类型:修正型和简化型。
接下来将从两个方面,通过一些例题来总结一些思路与方式。
首先是两道较为简单的基础优化。(注意是优化较为基础)
Sharing Chocolates
题意简述:给定两个正整数 \(n,m\) ,问是否能够将一个长,宽分别为 \(n,m\) 的矩形通过每次沿长或宽的方向分成两个子矩形,使得最后恰好得到 \(k\) 个子矩形,且面积分别为 \(a_1,a_2,a_3,...,a_k\) ?
\(tips:n,m\le100,k\le15\)
首先,我们考虑一个较显然的 DP 思路。既然 \(k\) 只有小小的 \(15\) ,我们考虑把当前矩形最终可以得到的 \(a_1,..,a_k\) 中的哪些面积当成一维写入我们的状态,这样不难得出一个状态:
表示矩形的长为 \(i\) ,宽为 \(j\) 时,是否可以通过划分得到 \(sta\) 中的面积。
这样一来,我们就可以得到一个总数为 \(O(mn2^k)\) 的状态。
但是这个状态在空间上就已经难以接受,因此,我们需要考虑优化。
注意到一点:如果一个状态 \(sta\) 中所有元素之和不为 \(i\cdot j\),可以直接为 \(false\)。
这是不难理解的,因为我们的要求是恰好得到,如果面积都不等就不可能恰好得到了。
这样一来,我们便有了一个优化思路:我们可以通过 \(sta\) 与 \(i\) 的值来直接算出 \(j\) !也就是说状态中的 \(j\) 是可以被其他维度表示的多余维度。发现这个后我们就不难修正出新的状态了:
表示当前矩形较长的一条边长度为 \(i\) (这样规定方便处理),是否可以通过划分得到 \(sta\) 中的面积。
这样,我们的状态总数就降到了 \(O(\max(n,m)\cdot2^k)\) ,可以满足空间的要求了。
Blinker 的仰慕者
题意简述:给定两个正整数 \(L,R\) ,求区间 \([L,R]\) 中满足各位数字之积等于 \(k\) 的数的和。
\(tips:L,R \le 10^{18},k \le 9^{18}\)
这道题的状态是不难考虑的,也是很套路的数位 DP
设 \(dp[i][j]\) 表示考虑了前 \(i\) 位,当前数字各位乘积为 \(j\) 的数字之和;
\(f[i][j]\) 表示考虑了前 \(i\) 位,当前数字各位乘积为 \(j\) 的数字个数。
则我们可以很方便地写出转移方程...慢着!
\(k\) 不是最大可以有 \(9^{18}\) 吗,这怎么直接弄进状态了?!
是的,这样大的数据范围不允许我们把乘积写入状态,但是又不可以直接把这一维给省略掉,这该如何是好呢?
幸运的是,我们发现乘积只会由 \([1,9]\) 中的数字相乘而来,这就意味着每一个乘积都可以被分解为 \(2^a\cdot3^b\cdot5^c\cdot7^d\) 的形式!
而通过计算得知,\([2,10^{18}]\) 内满足此条件的数不超过 \(10^5\) !
于是,我们可以预处理所有的 \(2^a\cdot3^b\cdot5^c\cdot7^d\) ,然后就可以把大量的无用状态通过离散的方式舍去,这样就可以通过这道题了。
完成了这两道试手题,我们来看些有难度的。
义已失吾亦死
题意简述:给定 \(n,a,b,c,p\) ,求用 \(a\) 个 \(1\) ,\(b\) 个 \(4\) ,\(c\) 个 \(5\) 能够组成的最大的能被 \(p\) 整除的 \(n\) 位自然数。
\(tips:a,b,c,n\le233,p\le64\)
这道题乍一看不蛮好想,但我们大概确定了方向:数位 DP
接下来考虑状态的设置。
首先我们可以排除掉最优性 DP,因为数位在这个问题中并不具备最优子结构(后面尽可能大不一定最后也是最大)。
那么我们考虑设状态 \(dp_{i,a,b,c,q}\) 表示考虑了前 \(i\) 个数位,还剩余 \(a,b,c\) 个 \(1,4,5\) ,当前组成的数模 \(p\) 的值是否可以为 \(q\) 。
那么我们就可以考虑 DP 计算了,最后统计答案可以贪心地在每一位尽可能往高了选。
但是有一个问题:状态总数是 \(O(n^4p)\) 的,过不了怎么办?
方法一:简化状态
发现一个细节:我们可以直接通过 \(a,b,c\) 来算出 \(i\) 的值来。
这就帮助我们消去了许多无用状态,总数为 \(O(n^3p)\)。
方法二:压缩状态
我们每一个状态表示的都是是否可以的形式,意味着它的取值只有 \(0/1\) 。
那么不难发现大量空间被浪费了,又注意到 \(p\le64\) ,所以我们考虑把状态变为 \(dp_{a,b,c}\) 表示已经用了 \(a,b,c\) 个 \(1,4,5\) 后组成的数模 \(p\) 的余数的集合,即一个状态压缩后的整数(\(64\) 位,每位为 \(0/1\) 表示是否存在模 \(p\) 后为这个数的数)。
这样的话状态总数成为 \(O(n^3)\) ,总的复杂度就会降为 \(O(n^3)\) ,可以通过本题。
Easy Climb
题意简述:给定一个长度为 \(n\) 的序列 \(a\) ,每次可以选择一个数 \(a_i(i \in [2,n-1])\) 花费 \(1\) 的代价把它变成 \(a_i+1\) 或 \(a_i-1\) ,求使得对于任意 \(i\in[1,n)\) 满足 \(|a_i-a_{i+1}|\) 不超过 \(d\) 的最小代价。
\(tips:2\le n\le100,0\le d,a_i\le10^9\)
这个题首先有一个很显然的暴力 dp 的思路。
设 \(dp[i][j]\) 表示考虑了前 \(i\) 个数,使得当前数的大小为 \(j\) 的最小代价。
那么我们就可以快乐的 \(O(nd^2)\) 计算啦!(离谱)
那我们就应该考虑怎么优化。
首先我们并不可以离散化,因为它的修改是任意的,无法直接离散化。
考虑把状态的冗杂部分去除?但是这两个状态都是不可抹去的,不能互相推导。
或许考虑换一下状态的定义?在本题中这种方法并不适用,无法起到优化复杂度的作用。
这个时候,我们就应该进行对问题的分析,运用贪心思想来优化状态。
假设现在有三个数字 \(a,b,c\) ,考虑 \(b\) 的变化范围应该是: \([\max(a,c)-d,\min(a,c)+d]\) 。
那么 \(b\) 的取值就只有三种情况:
- 不变。
- \(\max(a,c)-d\) 。
- \(\min(a,c)+d\) 。
由此我们可以发现一个结论: \(b\) 的最后结果一定可以表示为 \(p+k\cdot d\) ,其中 \(p\) 为 \(a,b,c\) 中的一个数。
那么这个想法是否适用于 \(a_1,a_2,...,a_n\) 呢?答案是肯定的。
即:所有 \(a_i\) 最后都可以表示为 \(a_p+k\cdot d\) ,其中 \(p\in[1,n],k\in[-2n,2n]\) 。
这个想法的正确性证明参考这篇博客(不想画图)。
那么就可以通过贪心思想来帮助我们实现离散化的操作。
设 \(dp[i][j]\) 表示考虑了前 \(i\) 个数,使得当前数的大小离散化后为 \(j\) 的最小代价。
那么状态总数就是 \(O(n^3)\) 的,已经优化了不少,下一步是很经典的单调队列优化,不再赘述。
Summary
综合来看,dp 的状态简化一般有如下方法:
- 消除冗杂量。
- 状态离散化。
- 位运算压缩状态。
- 贪心等其他算法辅助。
一般的针对状态的优化大概就是这四种类型,其中除最后一点需要深入思考题目性质外,其他三点都是比较容易发现的,当转移方面已经无路可走时,不妨想一想能否从状态本身下手来进行优化。