一类动态规划问题状态的简化

DP学习心得—状态的设计与简化

其实是带着一些奇怪的想法找了点题目。

动态规划由两个重要的部分组成:转移方程状态
好的状态是动态规划能够发挥威力的重要前提,而好的转移方程则是动态规划发挥作用的途径与方式
因此,通过一段时间的学习,本人试着总结出一些动态规划问题的状态上的设计思路与巧妙化简,并写下这篇总结供日后学习所需。


Part 1 状态的设计

好的状态是动态规划的开始,一个动态规划问题中的状态应该首先满足:

  1. 能够准确地描述对转移有影响的状态维度,不存在不同转移情况的混淆(即转移过程不遗漏,不混淆)。
  2. 不具备后效性,即不会存在互相转移的情形。
  3. 能够满足转移时最优性的传递(即最优子结构性)。

在满足了以上几点后,我们基本可以得到一个保证正确性的状态。
但是有时候,仅仅这样是不够的,我们的状态可能会显得过于冗杂,此时则需要我们用一定的技巧来转变思考角度,减少冗杂的成分,来让转移更加的轻简。

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\) 中的哪些面积当成一维写入我们的状态,这样不难得出一个状态:

\[dp[i][j][sta] \]

表示矩形的长为 \(i\) ,宽为 \(j\) 时,是否可以通过划分得到 \(sta\) 中的面积。
这样一来,我们就可以得到一个总数为 \(O(mn2^k)\) 的状态。
但是这个状态在空间上就已经难以接受,因此,我们需要考虑优化。

注意到一点:如果一个状态 \(sta\) 中所有元素之和不为 \(i\cdot j\),可以直接为 \(false\)
这是不难理解的,因为我们的要求是恰好得到,如果面积都不等就不可能恰好得到了。
这样一来,我们便有了一个优化思路:我们可以通过 \(sta\)\(i\) 的值来直接算出 \(j\) !也就是说状态中的 \(j\)可以被其他维度表示的多余维度。发现这个后我们就不难修正出新的状态了:

\[dp[i][sta] \]

表示当前矩形较长的一条边长度为 \(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\) 的取值就只有三种情况:

  1. 不变。
  2. \(\max(a,c)-d\)
  3. \(\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 的状态简化一般有如下方法:

  1. 消除冗杂量。
  2. 状态离散化。
  3. 位运算压缩状态。
  4. 贪心等其他算法辅助。

一般的针对状态的优化大概就是这四种类型,其中除最后一点需要深入思考题目性质外,其他三点都是比较容易发现的,当转移方面已经无路可走时,不妨想一想能否从状态本身下手来进行优化。

posted @ 2021-01-03 17:18  Tuifei_oier  阅读(184)  评论(0编辑  收藏  举报