动态规划从入门到放弃【2】

动态规划从入门到放弃【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个子问题:

  1. Between [low, mid]:位于low和mid之间。
  2. Between [mid+1, high]:位于min和high之间。
  3. 穿过中间元素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/

发布了205 篇原创文章 · 获赞 655 · 访问量 53万+

猜你喜欢

转载自blog.csdn.net/qq_33414271/article/details/100905362
今日推荐