Leetcode学习计划之动态规划入门day5(53,918)

目录

53. 最大子数组和

问题描述

思路与算法1

思路与算法2:动态规划

918. 环形子数组的最大和

问题描述

思路与算法

 代码


53. 最大子数组和

问题描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

思路与算法1

        可以肯定的是,满足这样条件的子数组(称为最大和区间)肯定是夹在两个负数或0(不包含)之间的。

        进一步,从该区间的左端从右向左看,以一个0或者负数为起点到下一个0或者负数(不包含)为界的区间作为一个单位区间,区间和的累计和不会出现大于0的情况。否则的话将该最大和区间向左延申可以找到更大和的区间,这样就矛盾了。

        同理,从该区间的右端从左向右看,以一个0或者负数为起点到下一个0或者负数(不包含)为界的区间作为一个单位区间,区间和的累计和不会出现大于0的情况。

        因此,可以分别从两端出发扫描,确定最大和区间的两端边界。

        首先,从左向右扫描。每碰到一个负数或0,计算到该数之前的区间的总和\text{left\_sum},如果该总和比之前的更大则更新最大和\text{max\_leftsum},并将右边界\text{right\_boundary}更新为该数的索引值。最终得到的\text{right\_boundary}即为最终的最大和区间的右边界(不包含)。

        同样, 从右向左扫描。每碰到一个负数或0,计算到该数之前的区间的总和\text{right\_sum},如果该总和比之前的更大则更新最大和\text{max\_rightsum},并将 左边界\text{left\_boundary} 更新为该数的索引值。最终得到的 即为最终的最大和区间的左边界(不包含)。        

         以下以示例1的数据[-2,1,-3,4,-1,2,1,-5,4]为例进行说明。搜索过程如下图所示:

         由此可以得到最大和区间在\text{(2,7)} 之间(不包含两端边界)。

        以上思路看起来可行,然而,实际coding完一测试,才发现所作的前提是错误的。当数组中很多负数的情况下事情就不对了。。。不过毕竟是一种思路,留作纪念。说不定这一思路以后解决别的问题能够用得上。

  

思路与算法2:动态规划

        假设 \textit{nums} 数组的长度是 n,下标从 0 到 n-1。

        我们用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是:

\max\limits_{0\leq i < n}{f(i)}

        因此我们只需要求出每个位置的 f(i),然后返回 所得f 数组中的最大值即可。那么我们如何求 f(i) 呢?我们可以考虑 \textit{nums}[i] 单独成为一段还是加入 f(i-1)对应的那一段,这取决于 \textit{nums}[i] 和f(i-1) + \textit{nums}[i] 的大小,我们希望获得一个比较大的,于是可以写出这样的动态规划转移方程:

        f(i) = \max \{ f(i-1) + \textit{nums}[i], \textit{nums}[i] \}

        由此不难给出一个时间复杂度 O(n)、空间复杂度 O(n) 的实现,即用一个 f 数组来保存f(i) 的值,用一个循环求出所有f(i)。考虑到 f(i) 只和 f(i-1) 相关,于是我们可以只用一个变量 \textit{pre} 来维护对于当前 f(i)f(i-1) 的值是多少,从而让空间复杂度降低到 O(1),这有点类似「滚动数组」的思想。

        以上思路来自官解,参考:https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/

from typing import List

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        maxsum = nums[0]
        prev   = nums[0]
        
        for i in range(1,len(nums)):
            cur = max(prev+nums[i], nums[i])
            maxsum = max(cur,maxsum)
            prev   = cur
        return maxsum

if __name__ == "__main__":
    
    sln = Solution()    
    
    nums = [-2,1,-3,4,-1,2,1,-5,4]
    print(sln.maxSubArray(nums))
    
    nums = [1]
    print(sln.maxSubArray(nums))
    
    nums = [5,4,-1,7,8]
    print(sln.maxSubArray(nums))

        执行用时:200 ms, 在所有 Python3 提交中击败了34.59%的用户

        内存消耗:25.5 MB, 在所有 Python3 提交中击败了85.07%的用户

         官解还给出了基于分治的解题方法,不过瞄了一眼觉得好复杂,暂且免了。

918. 环形子数组的最大和

问题描述

给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。

示例 1:

输入:nums = [1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3

示例 2:

输入:nums = [5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10

示例 3:

输入:nums = [3,-2,2,-3]
输出:3
解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3

提示:

  • n == nums.length
  • 1 <= n <= 3 * 10^4
  • -3 * 10^4 <= nums[i] <= 3 * 10^4

思路与算法

        考虑最大和子区间的位置可以分为两种情况。

        第一种是不跨越首尾边界,这种情况的求解与上一题相同。记这种条件下求得的最大和为maxsum1。

        第二种是跨越了首尾边界。跨越了首尾边界的某个区间的和是最大的话,那就意味着互补区间(这个一定不跨越首尾边界)的和一定是最小子区间和。所以,以非环形的方式求出最小区间和,用总和减掉这个最小区间和即得跨越首尾边界的最大区间和maxsum2。

        取maxsum1和maxsum2中的较大者即得最终答案。

        记以索引\textit{i}结尾的子区间最小和为\textit{f(i)},我们所要求的就是\min\limits_{0 \leq i \leq n-1}{f(i)}。考虑从\textit{f(i-1)}\textit{f(i)}的状态转移方程,可以得到(思路与上一题求最大区间和相同):

                f(i) = min(f(i-1)+nums[i], nums[i])

        但是有一种例外情况需要考虑,就是输入数组全为非正数时,此时最小区间和就是全区间,总和减去最小区间和就是0了,此时对应的最大和区间是空区间,而空区间是不满足条件的。这种情况下直接返回第一步求得的最大区间和即可。 

 代码

from typing import List
class Solution:
    def maxSubarraySumCircular(self, nums: List[int]) -> int:
        # Find the maximum subarray sum
        maxsum = nums[0]
        prev   = nums[0]
        
        for i in range(1,len(nums)):
            cur = max(prev+nums[i], nums[i])
            maxsum = max(cur,maxsum)
            prev   = cur
            
        # Find the minimum subarray sum
        minsum = nums[0]
        prev   = nums[0]
        
        for i in range(1,len(nums)):
            cur = min(prev+nums[i], nums[i])
            minsum = min(cur,minsum)
            prev   = cur
        
        print(maxsum, minsum)
        if minsum == sum(nums):
            return maxsum
        else:
            return max(maxsum, sum(nums)-minsum)

if __name__ == "__main__":
    
    sln = Solution()    
    
    nums = [1,-2,3,-2]
    print(sln.maxSubarraySumCircular(nums))
    
    nums = [5,-3,5]
    print(sln.maxSubarraySumCircular(nums))
    
    nums = [3,-2,2,-3]
    print(sln.maxSubarraySumCircular(nums))
    
    nums = [1,-2,1-2]
    print(sln.maxSubarraySumCircular(nums))
    
    nums = [-3,-2,-3]
    print(sln.maxSubarraySumCircular(nums))

        执行用时:288 ms, 在所有 Python3 提交中击败了13.38%的用户

        内存消耗:18.4 MB, 在所有 Python3 提交中击败了36.49%的用户

         以上两个for-loop可以合并为一个。

class Solution:
    def maxSubarraySumCircular(self, nums: List[int]) -> int:
        # Find the maximum subarray sum
        maxsum = nums[0]
        prevMax   = nums[0]
        minsum = nums[0]
        prevMin   = nums[0]
        
        for i in range(1,len(nums)):
            curMax = max(prevMax+nums[i], nums[i])
            maxsum = max(curMax,maxsum)
            prevMax   = curMax
            curMin = min(prevMin+nums[i], nums[i])
            minsum = min(curMin,minsum)
            prevMin   = curMin
        
        maxsum2 = sum(nums)-minsum
        if maxsum2 == 0:
            return maxsum
        else:
            return max(maxsum, maxsum2)

        官解关于本题给出了四种解法(看不懂,不搬运了^-^):

        方法 1:邻接数组

        方法 2:前缀和 + 单调队列

        方法 3:Kadane 算法(符号变种)

        方法 4:Kadane 算法(最小值变种)

        回到总目录:笨牛慢耕的Leetcode每日一题总目录(动态更新。。。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值