动态规划的本质是递归;所以做题之前一定要会递归;递归式就是状态转移方程;这里将会介绍使用动态规划做题的思维方式。
统一的做题步骤:
1、用递归的方式写出代码;(此方法写的代码在leetcode中一定会超时)
2、找冗余,去冗余;
3、找边界;
1、爬楼梯
假设你正在爬楼梯。需要 n 步你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.1 步 + 1 步
2.2 步
写递归
通常情况下,我们会将问题从大到小处理,也就是这里如果有n个台阶,先处理第n个台阶,然后处理n-1,n-2;所以在这里,总体思路是:第n个台阶可以走1步,也可以走两步,当走第一步时,n-1个台阶又可以分为n-1和n-2,不断递归,所以可写为recursionClimbStairs(n - 1),当n台阶走2步时,recursionClimbStairs(n - 2),然后将两种情况相加;示意图如下图所示:
递归代码如下:
def recursionClimbStairs(self, n):
if n <= 2:
return n
return self.recursionClimbStairs(n - 1) + self.recursionClimbStairs(n - 2)
去冗余:
从上图可知,左下角的n-2和右边的n-2重复了,都进行了计算,所以严重拉长的时间;所以,我们可以把计算了的存起来;首先定义数组nums,当n为1时,方法只有一种,当n为2时,方法有两种,所以nums的前两个值分别为1、2;然后从2到n开始循环,当前值为把上面的递归抄下来即可,就是把方法名改成nums,因为存起来了;所以,数组最后的那个值就是最后的答案;如果不理解,仔细把下面几道题按照这种思虑去写,多写几次就理解了。
找边界
因为循环中有n-2,所以n<=2的情况都需要在循环前写好;
def climbStairs(self, n):
nums = [1, 2]
if n <= 2:
return n
for i in range(2, n):
nums.append(nums[i - 1] + nums[i - 2])
return nums[len(nums) - 1]
2、买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
写递归
从最后第n个数开始,第n个数减去前n-1个数的最小值为当前递归层的最大利益,然后将每层的最大利益再去较大值;代码如下
递归代码如下:
def recursionMaxProfix(self, prices):
if len(prices) < 2:
return 0
return max(prices[- 1] - min(prices[:- 1]),
self.recursionMaxProfix(prices[:- 1]))
去冗余:
前1个数利益为0,这里result用来存结果,转换方法和上面那个例子一样,但是其实如果直接使用上面的递归转换的代码为max(prices[i] - min(prices[:i]), result[i - 1]),但是由于min(prices[:i])又是一层循环所以会导致时间又超时,所以这里可以优化为从第一个数开始就获取最小的值存起来,如下代码所示;
找边界
当列表的数值小于2个时,结果只可能为0;
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
result = [0]
if len(prices) < 2:
return 0
minPrice = prices[0]
for i in range(1, len(prices)):
minPrice = min(minPrice, prices[i - 1])
result.append(max(prices[i] - minPrice, result[i - 1]))
return result[-1]
3、最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
写递归
思想:从最后第n-1个数开始,所以列表从右到左开始加,反之也可以,但里面的代码要改,思想不变;
递归代码如下:
def recursionMaxSubArray(self, idx, nums, maxSum):
if idx < 0:
return maxSum
nums[idx] = max(nums[idx], nums[idx + 1] + nums[idx])
maxSum = max(nums[idx], maxSum)
return self.recursionMaxSubArray(idx - 1, nums, maxSum)
去冗余:
递归是从第n个数开始,循环可以从左到右,递归中使用了maxSum变量存储最大值,而在这里循环中可直接使用数组,和上面的例子相似,但是由于这里的nums变量循环的时候没有用到前面的值,所以直接存入nums中,代码如下所示;
找边界
因为循环中有i-1,且第0个数也不需要计算,所以直接从1开始循环即可;
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
for i in range(1, len(nums)):
nums[i] = max(nums[i] + nums[i - 1], nums[i])
return max(nums)
4、打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
写递归
因为必须相隔一个数,且每个数都是非负的,所以情况分为两种,一种从第一个数开始,一种从第二数开始;两种情况分别递归取较大值;从最后第n个数开始,第n个数加上n-2位置的递归和n-1的递归相比取较大值;代码如下
递归代码如下:
def recursionRob(self, idx, nums):
if idx < 0:
return 0
return max(nums[idx] + self.recursionRob(idx - 2, nums), self.recursionRob(idx - 1, nums))
去冗余:
直接将上述的方法名改成数值即可,如下代码所示;
找边界
因为循环中有i-2,所以列表长度为2之前的需要处理;
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
nums[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
nums[i] = max(nums[i] + nums[i - 2], nums[i - 1])
return nums[len(nums) - 1]