vlambda博客
学习文章列表

如何解决动态规划问题

解决动态规划问题是一个很有意思的过程,某个看似复杂并需要大量计算才能解决的问题,如果它满足动态规划的条件,往往可以被轻而易举地化解,这其中往往涉及一个由简入深的推演过程。  
解决动态规划的基本步骤:
  • 判断这个问题能不能用动态规划解决

  • 尽量使用最少的参数,确定一个状态表达式

  • 制定状态转移方程

  • 画表格(写代码)




第一步  如何把一个问题划分为动态规划(DP)问题?
  • 通常,所有涉及求解最大或最小值的问题,或者是在特定条件或概率下计算数量的问题,都可以用DP求解。

  • 通常,所有的DP问题都满足重叠子问题和最优子结构特性。一旦我们确定了某个问题满足这两个条件,我们就可以用DP来解决。


第二步 定义状态

DP问题总是离不开状态和状态的过渡。那如何定义一个问题的状态呢?我个人粗浅的理解是,某个状态是可以定义为一组参数,它们可以唯一地标志给定问题中的某个位置或立场(或者说某个子问题这组定义状态的参数的数量应该尽可能地少,以减小状态的空间或者说维度,简化求解复杂度。

我们来看一个经典的DP问题,0-1背包问题。先简单回顾一下这个问题,假设我们有个一个背包,容量是为 W ,同时有N个货物,每个货物有不同的价值,求解在背包中放入哪些货物能使背包中货物的价值总和最大(假设每个货物要么放入要么不放入,不能拆分),即找出一个货物的最佳组合放包里使得整体价值最大。在背包问题中,我们可以通过index和weight这两个参数定义一个状态DP[index][weight],其中index表示考虑前index个可选的货物(假设货物从1开始编号),weight表示背包的某个容积,例如DP[3][10]表示在背包容量为10的情况下,考虑前3个物品的最佳组合。

假设W>10,N>3,我们可以看到考虑背包容量是10的时候从前3个货物中选择最佳组合,是最终要求解的原始问题的一个子问题,DP[3][10]就可以唯一标志这个子问题的状态。


第三步 状态转移方程

要知道状态与状态之间的关系,就需要推演出状态转移方程。这也是解决DP问题时最难的一部分,需要耐心观察和练习。我们还是以上文提到的背包问题来讲,我们取背包问题中的一个子问题来看,假设此时背包容量为10,我们已经考虑了只有前两个物品时的状态,现在考虑要不要把第3个物品放进去,我们首先要判断目前背包的剩余容量是否还放得下第3个物品。

  • 如果当前背包的剩余容积小于第3个物品的体积,那物品肯定放不下,此时DP[3][10]就等于DP[2][10],也就是说背包容量为10时考虑前三个物品的最佳组合与考虑前两个物品的最佳组合是一样的,因为第3个物品太大了压根放不进去。

    DP[3][10] = DP[2][10]

  • 如果当前背包的剩余容积大于第3个物品的体积,那这个物品是可以放入背包的,此时我们只有两个选择,放或不放,我们只需要判断一下这两个选择的收益,选收益大的就好了。

    • 不放入第3个物品。此时的DP[3][10]就等于DP[2][10],即此时的最佳组合就是只考虑前两个物品的最佳组合。

    • 放入第3个物品,那我们需要在背包中先预留出第3个物品空间,我们已知第3个物品的体积是w_3,价值是v_3,那么此时的最佳组合应该是:DP[2][10 - w_3] + v_3,这里的[10 - w_3]就是预留出第3个物品的空间后,背包的剩余容积。

    • 最后我们需要在放和不放这两个抉择中,选出一个最佳组合,那其实很简单,只需要比较一下这两个状态的收益(或者说结果)就可以。所以,此时的状态转移方程就是:

    • DP[3][10] = Max(DP[2][10 - w_3] + v_3, DP[2][10])


到这里我们的状态转移方程就基本确定了。


第四步 画表格

如果把整个DP问题的求解看作一条很长路径的话,上一步中我们只是取了路径中间某一个段做了推演,如果我们需要重头开始推演的话,画表格无疑是一个帮助我们理解的好方式。从DP[3][10]这样的状态定义方式也能看出,二维数组是一个比较合适的数据结构来来存储这些状态,正好一个二维数组也可以用一张二维表格来形象地表示。下面就是一个针对0-1背包问题的表格,具体如何画出来的,其实是一个动态的过程,文字不太好描述,大家可以脑补一下我们从表格的左上角的开始,利用第三步中的状态转移方程,从左到右,从上到下地一步步将表格填满,表格右下角的值,就是我们的最优解。


至此,我们大致梳理了如何解决一个DP问题,但到目前为止我们还没涉及具体代码,后续我们会把一些经典的DP挨个手把手写一遍,到时我们再看具体的代码实现。