声明:《剑指offer》系列几乎所有的分享都与leetcode里面代码分享者Krahets有关,他是主要贡献者,里面会有我的一些个人总结和理解,请大家多指教。
一、 连续子数组的最大和
leetcode 《剑指offer》:42
1、题干
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
2、示例
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
3、解题思路
(1)状态定义:设动态规划列表dp,dp[i]代表元素nums[i]为结尾的连续子数组最大和。(动态规划问题都需要状态定义,也可称之为动态规划列表定义,在这个列表里面存储的值与题干紧密相关)
(2)转移方程:若 dp[i-1]≤0 ,说明 dp[i - 1]对dp[i] 产生负贡献,即 dp[i-1] + nums[i] 还不如 nums[i] 本身大。所以知道状态转移方程为:
(3)初始状态: dp[0] = nums[0],即以 nums[0]结尾的连续子数组最大和为 nums[0]。
(4)返回值:返回 dpdp 列表中的最大值,代表全局最大值。
4、复杂度分析:
(1)时间复杂度 O(N): 线性遍历数组 numsnums 即可获得结果,使用 O(N)时间。
(2)空间复杂度 O(1):由于 dp[i]只与 dp[i-1]和 nums[i]有关系,因此可以将原数组 nums 用作 dp列表,即直接在 nums上修改即可。
5、Python代码:
def f(nums):
for i in range(1, len(nums)):
nums[i] += max(nums[i-1], 0)
return max(nums)
6、代码讲解:
(1)因为没有使用额外的存储空间,所以直接在nums上建立动态规划列表dp(即dp和nums共用同一块存储空间)
所以nums +=相当于dp[i] +=
(2)根据状态转移方程,dp[i-1]和0作比较, 即nums[i-1]和0作比较,选择较大的值与dp[i](即nums[i])相加
(3)返回dp中的最大值,即max(nums)
7、动态规划解题流程:
(1)判断是不是动态规划类型题目(核心词:“最”,可以对照后面的例题)
(2)状态定义——>转移方程——>初始值——>返回值(dp)
(3)编程实现(max)
二、买卖的最佳时机
leetcode 《剑指offer》:63
1、题干
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
2、示例
(1)输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
(2)输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
3、解题思路
又是“最”+数组的问题,动态规划。
(1)状态定义:状态转移列表dp[i]表示第i天卖出的最大受益
(2)转移矩阵:这里不需要重建状态转移列表,只需要保留最大值。即,如果第i天的卖出收益最大,则保留最大值;
公式为:dp[i] = max(dp[i-1], nums[i] - min(nums[:i]))
(3)初始状态:dp[0] = 0
(4)返回值:最后保留的值
值得注意的是:如果我们每次计算dp[i-1]会有大量的重复计算,我们可以用profit表示前i-1天最大受益, 那么第i天的收益只需要与profit比较就好了;同理,也没必要重复计算前i-1天的买入最小值,使用cost代替。这样会降低时间复杂度。
4、Python代码
def f(nums):
cost, profit = float('+inf'), 0
for price in prices:
cost = min(price, cost)
profit = max(profit, price-cost)
return profit
dp在变化过程中,不是以列表的形式存在的,是以最大值的数值形式存在的。动态规划类型问题常有两种返回值情况(1)最大值(2)列表求和或者max(alist),当元素之间有相关性的时候常采用后一种方式。元素间无相关性,如本题,只需保留最大值。
5、复杂度分析
(1)时间复杂度O(n):只需遍历一次列表
(2)空间复杂度O(1):状态转移结果采用常数保存。