动态规划
刷题也有好几个月了,但一直都是沉醉于数组和字符串这些入门的知识点,这假期准备好好啃一些知名的算法了
动态规划作为科技公司基本上必考的知识点,在刷题网站中占有很大的比重,很多的题目都可以用动态规划去做。今天就来捋一捋dynamic program。
什么是动态规划
先来个例题:
问总共有多少种方法可以走到右下角?
如果想用动态规划解题,首先要先明确什么样类型的题目适合用动态规划去做,毕竟面试时
候,面试官不可能说:来用动态规划给我解这道题。
动态规划题目特点
**1**:计数
-计算有多少种方法走到右下角
-有多少种方法选出K个数使得和等于sum
**2**:求最值
-从左上角到右下角路径的最大数值和‘
-最长上升子序列长度
**3**:求存在性
-取石子游戏,先手能否获胜
-能否选出K个数使和为sum
实战例题(LeetCode)
[LeetCode322](https://leetcode-cn.com/problems/coin-change/)
这就是一道典型的动态规划的题目,因为题目要求最少的硬币个数,但不是说所有的求最值都一定是动态规划。
题目要求用最少的硬币数,常规的思维也就是直觉一定是先尽量用较大的面额的硬币,剩下的再用第二大,以此类推这样一定是最少的。*但这样一定是对的吗?*来看个例子:
例如有2,5,7三种面额的硬币,请你凑出27元。
所以直觉不一定是对的。动态规划有固定的做题步奏。
一:确定状态
动态规划中确定状态是最重要的事情,类似于确定数组中a[i]代表着什么意义。
确定状态需要两个意识:
---- 最后一步
---- 子问题
**最后一步**:
就像LeetCode322题目中的一样假设要凑的是用2,5,7凑27,虽然最开始不知道最优策略是
啥,但是可以明确的是肯定是K枚硬币a1,a2............ak加一起和为27。
所以一定有一枚最后的硬币ak,除去这个硬币,其他的加一块就是27-ak。
由上图可以得知:我们不关心前K-1枚硬币怎么拼出27-ak,可能有1种方法,也可能有1000000种,不care,但是可以确定的是 前面的确是拼出了27-ak。
再者,因为是最优策略,所以前K-1枚硬币一定是最少的。
接下来的问题就是怎么用最少的硬币拼出***27-ak***。
原问题是用最少的硬币拼出 27.,将问题转化为更小的规模了。为了简化定义可以用f(x) = 最少用多少枚硬币拼出x。
且最后一枚硬币ak只可能是2,5,7中的一枚硬币,如果ak=2,则 f(27)= f(27-2) + 1
,最后加一是加上最后一枚2块钱的硬币。其他的情况以此类推。
故可得出 f(27) = min { f(27-2)+1, f(27-5)+1, f(27-7)+1}
上式可理解成拼出27元所需的硬币数目等于(拼出25所需的数目加一,拼出22所需的数目加一,拼出20所需的数目加一,三个中的最小值。)
二:转移方程
**f(27) = min { f(27-2)+1, f(27-5)+1, f(27-7)+1}**
这就是实际code时候 ,也包括面试时候最重要的东西,写对转移方程基本上这题就做对一大半了。
三:边界条件
刷过LeetCode或者牛客的人应该都有这种体验,一道题能不能通过往往都是取决于那些坑爹的边界条
件或者奇葩的测试用例。
例如用2 3 5拼27那个例子,如果x-2 x-5 x-7小于0怎么办,什么时候停下来?
所以f(1) = min { f(-1)+1, f(-4)+1, f(-6)+1} = 正无穷,表示拼不出来。
设置初始条件:f[0] = 0
四:计算顺序
现在已经知道了转移方程:f(X) = min { f(X-2)+1, f(X-5)+1, f(X-7)+1}
初始条件:f[0] = 0
计算顺序从小到大,先计算f[1],f[2].......f[27].
从图可以看出,从左往右计算,当计算拼出k的时候,需要分别用到前面的k减去前面三个面额硬币的数据。
小结*
动态规划的流程
1:确定状态
-最后一步
-子问题
2:转移方程
3:边界条件
-数组不可越界
-初始条件的设定‘
4:计算顺序
-从左往右还是相反
BB了那么多,那道LeetCode还是没做呢。322
talk is cheap,show me your code!
dp = [sys.maxsize-1] * (amount+1)#定义一个dp矩阵,分别存放着拼出dp[i]所需最少的硬币个数
dp[0] = 0 #初始化dp矩阵最开始的元素为0
for i in range(amount+1):#初始化之后开始最dp数组中每个元素进行赋值
if dp[i] == sys.maxsize - 1:#如果dp[i]当前元素为无穷,则说明此时无解,进行下轮循环
continue
for coin in coins:#若此时dp[i]不是无穷大,则分别添加硬币列表中的硬币进行选择
if coin + i <= amount:#如果加上此时的硬币还小于amount
dp[i+coin] = min(dp[coin+i],dp[i]+1)#比较原始的跟加上一枚之后哪个更小
if dp[-1] == sys.maxsize -1:return -1#如果循环赋值之后,dp[i]最后一位还是无穷,则无解
else:return dp[-1]#有解,则返回最后一个数
觉得代码注释的可能不太清楚,灵魂画手又手画了一幅图解释代码中的循环都在干啥,觉得起码这道题应该是解释清楚了
还可以用递归去解这道题,用字典去存储每一步运算的结果这样可以省掉多次重复运算,应该也是可以的!