最近在Leetcode上面刷题,刷着刷着以前觉得最难的递归问题也开始有了一些思路,但是又遇到一些涉及动态规划的问题,基本没有思路,每次都是去看别人的Solution之后才恍然大悟,但是自己来写的时候一点也下不了手
看别人的标准解法的时候,总感觉非常抽象,动态规划当中的局部最优解以及全局最优解都是用非常抽象的数据类型来实现的,所以看起来很不容易搞懂,后面我又去反复看了几篇文章,其中有一句
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions.
这句话告诉了动态规划有三个最重要的部分
- 将复杂问题抽象为子问题(也就是很多文章中提到的找到局部最优解的规则)
- 解决子问题,但是每个子问题只运算一次
- 储存已解决的子问题
第一点将复杂问题抽象为子问题,这个感觉思路和一般的递归一致,但是后面两点是简单递归所不具备的,简单的递归会重复运算更深层次的子问题,并且也不会储存子问题的运算结果;
贴一个走格子的简单例子上来,假设有长为m,宽为n的矩形格子阵列,每一步只能往上或者往右边走,求从左下角走到右上角一共有多少种走法?
首先拿到这个问题,很容易找到复杂问题与子问题之间的关系:
因为只能往上或者往右走,假设左下角那一点的总走法为 f(m, n),可以将 f(m ,n)分解
f(m ,n) = f(m-1, n) + f(m , n-1)
然后发现这个问题可以瞬间实现为一段非常简单的递归代码:
def zougezi_recur(n, m):
if n == 1:
return 1
if m == 1:
return 1
return zougezi_recur(n-1, m) + zougezi_recur(n, m-1)
但是当格子的数目变大的时候,这个简单的递归就已经吃不消了
所以考虑采用动态规划的思路来解决这个问题,首先我需要找到方法来存储之前的运算结果,然后每一次针对子问题的运算结果都储存于我们设计的这个存储模型当中
从这里就可以看出,动态规划和简单递归还是有所区别的,递归是从最复杂的问题一点点往最简单的问题进行挖掘,比如斐波那契数列,从第n项挖掘到最简单的第1项或者第2项,但是动态规划会从最简单的元素着手,然后一步步生成最复杂的结果
这里我们先构造一个数据模型来存储运算结果,这里我选择用一个字典来存储,因为这个问题当中我们每一次运算结果有长m,宽n两个索引,所以考虑以(m,n)作为key,(m,n)对应的走法作为value
首先将问题的最简单部分列举出来,这里列举m+n不大于4的格子,那么可以将字典初始为
D = {(1, 1): 0, (1, 2): 1, (2, 1): 1, (2, 2): 2, (1, 3): 1, (3, 1): 1}
由此为基础,可以很快推出m+n=5时,所有的子问题为D[(2,3)],D[(3,2)],D[(1,4)],D[(4,1)],其中D[(1,4)]和D[(4,1)]可以直接等于1,那么重点就在D[(2,3)]与D[(3,2)]上,根据我们之前的规则f(m ,n) = f(m-1, n) + f(m , n-1),所以D[(2,3)] = D[(1,3)]+D[(2,2)], D[(3,2)] = D[(3,1)]+D[(2,2)], 而D[(1,3)]、D[(3,1)]、D[(2,2)]已经是我们字典中现成的东西了,可以直接拿来用,所以可以很快在字典中获得m+n=5时的运算结果
代码如下:
def zougezi_dp(n, m):
if n == 0 or m == 0:
return 0
D = {(1, 1): 0, (1, 2): 1, (2, 1): 1, (2, 2): 2, (1, 3): 1, (3, 1): 1}
s = 5
while s <= (m+n):
for i in range(1, s):
j = s-i
D[(i, j)] = 1 if i == 1 or j == 1 else D[(i-1, j)] + D[(i, j-1)]
s += 1
return D[(n, m)]
但是感觉我的解法应该还是和最标准的动态规划有所出入,有点像暴力破解的方法了,继续刷题,继续学习吧