看了前文 动态规划解题套路框架,我知道了如何一步步优化动态规划问题;
看了前文前提概要,我知道了利用数学归纳法写出暴力解(状态转移方程)。
但就算我写出了暴力解,我很难判断这个解法是否存在重叠子问题,从而无法确定是否可以运用备忘录等方法去优化算法效率。
对于这个问题,其实我在动态规划系列的文章中写过几次,在这里再统一总结一下吧。
首先,最简单粗暴的方式就是画图,把递归树画出来,看看有没有重复的节点。
比如最简单的例子,动态规划解题套路框架 中斐波那契数列的递归树:
这棵递归树很明显存在重复的节点,所以我们可以通过备忘录避免冗余计算。
但毕竟斐波那契数列问题太简单了,实际的动态规划问题比较复杂,比如二维甚至三维的动态规划,当然也可以画递归树,但不免有些复杂。
比如在 最小路径和问题 中,我们写出了这样一个暴力解法:
你不需要读过前文,光看这个函数代码就能看出来,该函数递归过程中参数 i, j
在不断变化,即「状态」是 (i, j)
的值,你是否可以判断这个解法是否存在重叠子问题呢?
假设输入的 i = 8, j = 7
,二维状态的递归树如下图,显然出现了重叠子问题:
但稍加思考就可以知道,其实根本没必要画图,可以通过递归框架直接判断是否存在重叠子问题。
具体操作就是直接删掉代码细节,抽象出该解法的递归框架:
可以看到 i, j
的值在不断减小,那么我问你一个问题:如果我想从状态 (i, j)
转移到 (i-1, j-1)
,有几种路径?
显然有两种路径,可以是 (i, j) -> #1 -> #2
或者 (i, j) -> #2 -> #1
,不止一种,说明 (i-1, j-1)
会被多次计算,所以一定存在重叠子问题。
再举个稍微复杂的例子,后文 正则表达式问题 的暴力解代码:
代码有些复杂对吧,如果画图的话有些麻烦,但我们不画图,直接忽略所有细节代码和条件分支,只抽象出递归框架:
和上一题一样,这个解法的「状态」也是 (i, j)
的值,那么我继续问你问题:如果我想从状态 (i, j)
转移到 (i+2, j+2)
,有几种路径?
显然,至少有两条路径:(i, j) -> #1 -> #2 -> #2
和 (i, j) -> #3 -> #3
,这就说明这个解法存在巨量重叠子问题。
所以,不用画图就知道这个解法也存在重叠子问题,需要用备忘录技巧去优化。