动态规划算法其实并不复杂,动态规划算法的实际操作核心是“做笔记”,即将前期运算的结果保留下来成为后续运算的基础信息,说白了其实就是用空间换取时间。
其实在我们平时编程时已经初步接触到了许多使用动态规划算法的问题:菲波那切数列、生兔子问题、跳台阶问题,如果选择使用数组(列表)来解决这些问题,那么就是使用了动态规划的思想(才意识到?)接下来的博文将从几个经典的动态规划题目来大致展示这个算法的思想。
找零钱问题
给你n种面额的纸币和目标金额quota,因为要保护环境,请问如何使用最少的纸币数达到目标金额?最少使用几张纸币?
刚看到这个题目,不少朋友会第一时间想到贪心法。没错贪心法是一种解决方案,在后续的博文中会详细地展示贪心法,但这篇博文讲的是动态规划算法贪心法只能模糊求解,在找零钱的边界问题上会面临比较复杂的问题,所以我们老实的用动态规划实现。
Talk is cheap, show me the code.
python
money = [2,3,7] # 设置纸币的面额
quota = int(input("please tell me the quota")) # 设置目标金额
dp = [100]*(quota + 1) # 建立dp列表,100的意思是取一个大数,理论上是正无穷,这里100就可以了。列表下标i代表当前的金额,dp[i]代表使用的纸币数
for j in money:
dp[j] = 1 # 初始化dp列表,将拥有的纸币面额所对应的金额最小纸币数设为1
i = money[0] # 从拥有纸币面额开始,比设置的最小纸币面额小的金额是达不到的
while i <= quota:
for j in money: # 对于所有纸币
if i > j and dp[i] > dp[i - j] + 1: # 如果此时的金额满足减去纸币j>0的条件(此金额的前一项金额i-j已求出最小纸币数)并且当前的纸币数dp[i]并非最小纸币数
dp[i] = dp[i - j] + 1 # 更新dp[i]为z[i-j]+1,即上一项的值加面额j一张纸币
i = i + 1 # 下一个金额
print(dp[-1]) # 输出目标金额的最小纸币数
上面代码的注释写得很详细,我们就不再多讲这个问题的代码了,我们来看更内在的问题。找零钱问题的递推式是A[n]=min(A[n-k(i)] 1<=i<=m),这个递推式主要体现在上述代码的更新条件上,而这个更新就是找零钱问题最重要的一个成分,各位朋友不难看出这个递推式的重要性。这引出了一个对于动态规划算法的理解:动态规划算法就是数列的递推(斐波那契数列的递推式为A[n]=A[n-1]+A[n-2])。
根据找零钱这个问题我们可以初步总结出使用动态规划算法的一般步骤
- 通过分析问题找到递推式
- 通过递推式创建dp表(一般为列表或二维表)
- 根据已有信息初始化dp表
- 根据递推式更新dp表,求出最终的结果
通过找零钱问题,我们还可以注意到动态规划算法适用于要求局部最优的问题,即如果要计算第i步的最优解,第i步前期的所有步骤都必须是最优解。接下来我们用另一个经典例题加深一下对动态规划算法的理解。
最大递增子序列
有一个数列[x1,x2,x3,x4,x5……,xn],这个数列有一个子序列[xk1,xk2,xk3……xkm],子序列满足k1<k2<k3……<km,xk1<xk2<xk3……<xkm,这个数列称为递增子序列。现给定一个数列,求最大递增子序列的长度。
我们一步一步地解决这个问题。
- 首先想着创建一个列表用于储存前期计算的结果,那么列表的下标代表什么?列表内的数值又代表什么呢?我们知道动态规划算法是一步一步地接近结果,所以想到下标i代表数列的第i-1个值的位置,dp[i]当然就是当前的最优解。直白的说,z[i]就是以数列的第i+1个数为结尾时的最大递增子序列的长度。有一个道理我不说大家也明白:dp列表的值肯定不会变小。
- 然后寻找递推式,在dp列表中,如果数列的第i个比数列i前面的第j个大,此时的dp[i]是不是应该变为dp[j]+1呢?不要着急,我们还要比较此时dp[i]与dp[j]+1的大小,大家都明白该怎么做。
- 编程解决问题
python
z=[5,3,4,8,6,7]
dp=[1]*len(z)
i=0
while i < len(z):
j=i-1
while j >=0:
if z[i] > z[j] and dp[i] < dp[j]+1:
dp[i]=dp[j]+1
j = j - 1
i = i + 1
print(dp)
看到这里,大家想必对动态规划有了初步的认识,但目前为止这些用动态规划解决的问题似乎都不是必须用动态规划算法来解决的。接下来我们来看一些更难的经典例子。
Path sum: two ways
给定N^N的矩阵,每个格子中都有有正整数,每到一个格子就加上这个格子对应的正整数,计算从矩阵左上方走到矩阵右下方的和最小是多少。(你只能向右和向下移动)。
这个问题如果不用动态分析直接穷举也可以做出来,建议小伙伴抽出时间去试一试
这个问题使用动态规划其实很简单,对于任意一个格子,因为规定只能向右和向下移动,所以对于任意一个格子,只需比较左格和上格就可以了,哪个少就从哪个走。因为此时左格和上格都是最优解,所以这样求出的当前格子也是最优解。
递推式A(i,j)=min(A(i-1,j),A(i,j-1))+v(i,j)
python
def small(x,y):
if x < y:
return x
else:
return y # 定义一个函数,返回两数的较小值
dp=[
[131,456,23,658,257],
[154,709,465,15,76],
[346,437,653,724,45],
[9,20,546,240,547],
[14,8,360,57,367]