前言
算法实验课的题目是一道关于动态规划(Dynamic Programming)的题目,正好借这个机会,学习一下动态规划(Dynamic Programming)。
动态规划简单介绍
动态规划(Dynamic Programming,简称DP)的基本想法就是将原问题转化为一系列相互联系的子问题,然后通过逐层递推来求得最后的解。
动态规划快速而高效解决问题的关键就是利用历史记录,来避免重复计算。
动态规划的题目特点
一般来说,我们面对各式各样的算法题,可能会有许多不同的想法,有些题目适合动态规划,有些题目可能不适合动态规划,那么哪些题目适合动态规划呢?
当我们看见如下三种类型的题目时,可以首先考虑一下,DP是否可以成为解决这道题的方法(当然,不一定是最优的算法)
- 计数型算法题
- 有多少种方式走到右下角
- 有多少种方式选出k个数,使其和为sum
- 求最大最小值型算法题
- 从左上角走到右下角路径的最大数字和
- 最长上升子序列长度
- 求存在性算法题
- 取石子游戏,先手是否必胜
- 能不能选出k个数使得和为sum
例题分析
对于以上三种题型,我们举三个简单的题目来总结出动态规划问题的解题思路。
例题一:Coin Change(LeetCode 332)
题目描述:
You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.
Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.
You may assume that you have an infinite number of each kind of coin.
您将获得一个整数数组,表示不同面额的硬币,以及一个整数金额,表示总金额。
返回您需要的最少数量的硬币,以弥补该金额。如果这些硬币的任何组合都无法弥补这一数额,则返回-1。
你可以假设每种硬币的数量是无限的。
举例分析一下
我们有三种硬币,面值分别为2元、5元和7元,且假设每种硬币有无限多。
买一本书需要27元。
如何用最少的硬币组合正好付清?
分析:
拿到问题的时候,我首先想到的是尽可能用大面值
即7+7+7=21
21+5=26
没有得到27
那我们改变一下策略,尽量使用大面值,最后可以使用一种硬币付清就行
所以我们得到了
21+2+2+2=27
所以得到答案为6,但是这个答案是错误的
正确答案是5,7+5+5+5+5=27
为什么会出现这种情况呢?换句话说,第一种算法有什么问题呢?
对于每一种正确的算法,我们应该可以通过数学证明出该方法是正确的,但实际上,我们并不能证明这种算法是正确的,贪心只是局部最优,但对于整体而言,它不一定是最优的。
那么利用动态规划该如何解决这个问题呢?
我们运用DP解决问题时,只需要把握以下四个部分就行:
- 确定状态
- 转移方程
- 初始条件和边界条件
- 计算顺序
确定状态
解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j] 代表什么,就像数学中,x,y,z表示什么。
确定状态需要两步
-
最后一步
对于这个问题,虽然我们还不知道最优策略是什么,但是我们知道最优策略一定是k枚硬币,a1,a2…ak相加得到27
所以一定有最后一枚硬币ak
除去ak,前面的硬币加起来就是27-ak
事实上,我们并不关心前面k-1枚硬币是怎么组成27-ak的,它可以有很多种方式,但肯定至少有一种,而且我们可以知道,前面k-1枚硬币组成27-ak的策略一定是最优的。也就是说,我们的最后一步就是(27-ak)+ak=27
-
子问题
原来的问题是最少用多少枚硬币组成27
现在的问题变成了最少用多少枚硬币组成27-ak我们把这种问题一样,规模变小的问题称为原问题的子问题。
为了简化表达,我们设状态f[X]=最少用多少枚硬币组成X
等等,此刻,我们似乎还不知道ak是多少
但是,毫无疑问,ak只能是2,5,7三个数的其中一个
如果ak=2,那么f[27]=f[27-2]+1(加上最后一枚硬币)
如果ak=5,那么f[27]=f[27-5]+1(加上最后一枚硬币)
如果ak=7,那么f[27]=f[27-7]+1(加上最后一枚硬币)为了满足硬币最少的需求,我们就得到了:
f[27]=min{f[27-2]+1,f[27-5]+1,f[27-7]+1}
写到这里可能就有很多小伙伴们会直呼这不就是递归吗?
那我们就用递归来看看,递归伪代码如下:
毫无疑问,当X=0时,拼成0的方法有0种
那么为什么定义初始值为正无穷呢?
关于这一点,我们会在DP的第三步:初始条件和边界条件中具体讲解。
这个方法有什么问题?
理论上,这个方法可以解决问题,但是递归有如下的问题: