动态规划从入门到放弃【2】
本文将利用“最大子序列和”问题来对比动态规划和其它算法之间的在实现上的区别。
最大子序列和问题
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
1.1 贪心算法
最通俗易懂的解释:
假设你是一个选择性遗忘的赌徒,数组表示你这几天来赢钱或者输钱,
你用sum来表示这几天来的输赢,
用ans来存储你手里赢到的最多的钱,
如果昨天你手上还是输钱(sum < 0),你忘记它,希望什么都没发生,明天继续赌钱;
如果你手上是赢钱(sum > 0), 你记得,你继续赌钱;
你记得你手气最好的时候
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
#当前元素及之前的子数组的和
sum_pre=0
#最大子序列的和
maxSubSum=nums[0]
#遍历
for i in range(len(nums)):
#判断当前元素及之前的子数组的和
if sum_pre<0:
sum_pre=nums[i]
else:
sum_pre+=nums[i]
#选择当前最大的子序列和
maxSubSum = sum_pre if sum_pre > maxSubSum else maxSubSum
return maxSubSum
1.2 动态规划
“动态规划”的两个步骤是思考“状态”以及“状态转移方程”。
有的资料又将“动态规划”分为 3 步:
base case:思考问题规模最小的时候,是什么情况;
update function:自下而上思考这个问题,即上面的“状态转移方程”;
gola:重点强调了输出是什么,很多时候输出并不一定是最后一个状态。
(1) 状态
题目中的关键字是“连续”,首先我们尝试就将状态定义成题目要求的结果。
dp[i]
表示 nums
在区间 [0, i]
中连续子数组的最大和。
在思考“状态转移方程”的时候,
dp[i]
之前的,例如dp[i - 1]
就有可能是是更前面的连续子数组的最大和,不利于我们分类讨论。
根据「力扣」第 300 题:“最长上升子序列”的经验,既然一个连续子数组一定要以一个数作为结尾,那么我们就将状态定义成:
dp[i]
:表示以 nums[i]
结尾的连续子数组的最大和。
输出应该是把所有的 dp[0]
、dp[1]
、……、dp[n - 1]
都看一遍,取最大值。 同样的情况也适用于「力扣」第 300 题:“最长上升子序列”。
(2)状态转移方程
接下来分类讨论就变得容易多了,dp[i]
的值要看 dp[i - 1]
,因为 nums[i]
一定被选取,那么 dp[i - 1]
的正负就作为分类的标准。
- 如果
dp[i - 1] >= 0
,那么可以把nums[i]
直接接在dp[i - 1]
表示的那个数组的后面。 - 如果
dp[i - 1] < 0
,那么加上前面的数反而我越来越小了,干脆“另起炉灶”吧,单独的一个nums[i]
,就是dp[i]
。
以上两种情况的最大值就是 dp[i]
的值,写出“状态转移方程 1”:
或者你还可以这样写,反正求的是最大值,我也不用分类讨论了,就这两种情况,直接取最大即可,因此还可以写出“状态转移方程 2”:
参考代码 1:根据“状态转移方程 1”
def dp01(nums):
length=len(nums)
if length==0:
return 0
dp=[0 for _ in range(length)]
for i in range(1,length):
if dp[i-1]<0:
dp[i]=nums[i]
else:
dp[i]=dp[i-1]+nums[i]
return max(dp)
参考代码 2:根据“状态转移方程 2”
def dp02(nums):
length=len(nums)
if length==0:
return 0
dp=[0 for _ in range(length)]
dp[0] = nums[0]
for i in range(1,length):
dp[i]=max(dp[i-1]+nums[i],nums[i])
return max(dp)
复杂度分析:
- 时间复杂度:O(N)。
- 空间复杂度:O(N)。
既然当前状态只与上一个状态有关,我们可以将空间复杂度压缩到 O(1)。
参考代码 3:将空间复杂度压缩到 O(1)。
def dp03(nums):
length=len(nums)
if length==0:
return 0
# 起名叫 pre 表示的意思是“上一个状态”的值
pre = nums[0]
res = pre
for i in range(1, length):
pre = max(nums[i], pre + nums[i])
res = max(res, pre)
return res
复杂度分析:
- 时间复杂度:O(N)。
- 空间复杂度:O(1)。
1. 3 分治法
分治法的思想其实就是认为某一序列的最大子序和要么在左半边,要么在右半边,要么是穿过中间,对于左右边的序列,情况也是一样,因此可以用递归处理。中间部分的则可以直接计算出来,时间复杂度应该是 O(nlogn)。
所以共分为3个子问题:
- Between [low, mid]:位于low和mid之间。
- Between [mid+1, high]:位于min和high之间。
- 穿过中间元素mid,包含了2部分,分别是Between [i, mid]和Between [mid+1, j]。
注意: mid 和 mid+1 所对应的元素必须被包含。
代码如下:
def dc(nums):
low=0
high=len(nums)-1
res=maxSub(nums,low,high)
return res
def maxSub(nums,low,high):
if low==high:
return nums[low]
else:
mid=int((low+high)/2)
maxleftSub = maxSub(nums, low, mid)
maxrightSub = maxSub(nums, mid + 1, high)
maxCrossSub = maxCrossingSubArray(nums, low, high)
return max(maxleftSub, maxrightSub, maxCrossSub)
def maxCrossingSubArray(nums, low, high):
# Calculate max_left in [i,mid]
mid = int((low + high) / 2)
left_sum = float('-Inf')# Make sure sums[mid] is included
sum = 0
for i in range(mid, low - 1, -1):
sum = sum + nums[i]
if sum > left_sum:
left_sum = sum
# max_left = i , i is the index
# Calculate max_right in [mid+1,j]
sum = 0
right_sum = float("-Inf")# Make sure sums[mid+1] is included
for j in range(mid + 1, high + 1):
sum = sum + nums[j]
if sum > right_sum:
right_sum = sum
# max_right = j, j is the index
return left_sum + right_sum
print(dc(lists))
小结
本文章中对比了动态规划算法、贪心算法和分治法的之间在代码实现上的不同,在动态规划中注重“状态”和“状态转移”在解决问题中的应用,为了保存产生的中间状态,可能需要使用数组来开辟空间保存,但转化为迭代的过程后,空间复杂度也是可以优化为O(1)。
参考链接:
https://leetcode-cn.com/problems/maximum-subarray/solution/jia-she-ni-shi-yi-ge-du-tu-by-acnesu/
https://leetcode-cn.com/problems/maximum-subarray/solution/dong-tai-gui-hua-fen-zhi-fa-python-dai-ma-java-dai/
https://leetcode-cn.com/problems/maximum-subarray/solution/bao-li-qiu-jie-by-pandawakaka/
转载:https://blog.csdn.net/qq_33414271/article/details/100905362