【算法-LeetCode】53. 最大子序和(动态规划初体验)

LeetCode53. 最大子序和

发布:2021年7月24日23:23:15

问题描述及示例

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

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

示例 2:
输入:nums = [1]
输出:1

示例 3:
输入:nums = [0]
输出:0

示例 4:
输入:nums = [-1]
输出:-1

示例 5:
输入:nums = [-100000]
输出:-100000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-subarray
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105

我的题解

【更新】:2021年7月29日12:04:57
下面这道题目我感觉更适合作为动态规划入门级的理解,可以先看看本篇博客的相关总结,再开始尝试由下面这道题开始动态规划的实际应用。

参考:【算法-LeetCode】70. 爬楼梯_赖念安的博客-CSDN博客

【更新结束】

更新:2021年9月12日10:30:46

这段时间也陆续碰到了其他的动态规划的题目,相互之间的总体思路都差不多,可以到下方【有关参考】的【我做的其他动态规划类题目】查看相关的题解。

【更新结束】

这算是我在LeetCode上碰到的第一个有关于动态规划的题目了。当然一开始压根没有往动态规划上面想,以为总还是能用暴力解法先解一遍的,但是,万万没有想到,经过一番复杂思考后,我用我想到的暴力解法提交代码,发现居然出现了运行超时的问题……这还是我第一次碰上因为运行超时而不通过的题。看来我的暴力解法是没办法发挥作用了。

我的题解1(暴力解法)

这个暴力解法的思路很简单。就是穷举输入的数组的所有连续子序列,同时对每个连续子序列求和,并用变量max来动态保存和的最大值。最后返回max作为结果。同时运用了辅助函数getSubSum用于计算一个数组中下标由start到end的连续子序列的和。注意这里也用到了for循环,所以加上穷举所有连续子序列的那两层for循环,这个算法总共有三层for循环,所以时间复杂度为O(n^3),这也是导致算法最后会因为运行超时而无法通过的原因吧。

/**
 * @param {number[]} nums
 * @return {number}
 */

// getSubSum函数用于计算一个数组中下标由start到end的连续子序列的和
const getSubSum = function (arr, start, end) {
    let sum = 0;
    for(let i = start; i <= end; i++) {
        sum += arr[i];
    }
    return sum;
}

var maxSubArray = function(nums) {
	// max用于动态存储目前nums连续子序列和的最大值
    let max = nums[0];
    // 使用双层for循环穷举nums数组的所有连续子序列
    for(let i = 0, length = nums.length; i < length; i++) {
        for(let j = i; j < length; j++) {
        	// 计算当前连续子序列的和,并与max作比较以更新max
            max = Math.max(max, getSubSum(nums, i, j));
        }
    }
    return max;
};


提交记录
201 / 203 个通过测试用例
状态:超出时间限制
提交时间:2 天前(2021722日)

最后执行的输入:(大概有上千个数字吧,反正很长……)
最后执行的输入:
很可惜,我以为上面的暴力解法虽然可能会执行效率很差,但起码还是能通过的,但是没想到LeetCode对于居然对运行时间还有相关的要求。但是可以看出,算法本身的思路还是符合逻辑的,只是性能太差了。

我的题解2(运用动态规划)

总体思路是运用动态规划(Dynamic Programming) ,其中动态规划数组dp[i]用于存储nums数组中下标i之前且包括下标i的连续子序列的和的最大值。变量max用于存储目前dp[i]的最大值。其中状态转移方程为dp[i] = Math.max(dp[i-1] + nums[i], nums[i])。每次更新dp数组后,都要同时更新max的值(也就是确保max是下面那张图中dp[i]数组中的最大值),以确保最后结果返回正确。

注意上面我说的dp[i]的含义:dp[i]用于存储nums数组中下标i之前且包括下标i的连续子序列的和的最大值。其实当我把测试用例nums = [-2,1,-3,4,-1,2,1,-5,4]代入动态规划中的状态转换数组中时,我发现dp数组的输出为:
在这里插入图片描述
上面的dp[0]dp[1]都好理解,但是到了dp[2]的时候我就有点懵了,因为我觉得dp[2]应该是1。为什么这么说呢?因为根据dp[i]的定义,下标i=2时,所求得的满足和为最大的连续子序列不应该是[1]吗(即nums[1],这里刚好是单元素数组)?后来经过一番思考,我觉得应该要将上面dp[i]的定义加上:必须包含dp[i]。这样才更说得通。

/**
 * @param {number[]} nums
 * @return {number}
 */

var maxSubArray = function(nums) {
	// dp数组是用于存储相关状态的数组,在动态规划的题目中经常会有这样一个数组
    let dp = [];
    // 初始化dp数组,根据dp数组的定义,当 i 为 0 时,有最大和的连续子序列就是nums[0]
    dp[0] = nums[0];
    // max应该初始化为dp[0],根据上一条,max初始值为nums[0]
    let max = nums[0];
    // 从前往后遍历nums数组,注意遍历时,i(即nums下标)的初始值应该为1,
    // 否则dp[i-1]会有数组下标溢出的问题
    for(let i = 1, length = nums.length; i < length; i++) {
    	// 动态规划中的状态转移方程
        dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
        // 每经过一次状态转移,应当更新max的值,确保max存储的是dp[i]中的最大值
        max = Math.max(max, dp[i]);
    }
    return max;
};


提交记录
203 / 203 个通过测试用例
状态:通过
执行用时: 76 ms,在所有 JavaScript 提交中击败了89.71%的用户
内存消耗: 39.4 MB,在所有 JavaScript 提交中击败了34.53%的用户
时间:202172423:26:33

更新:2021年7月25日14:07:05

动态规划思路初步总结

  1. 先确定dp[i]数组的含义,也就是说搞清楚dp[i]数组保存的是什么,其中的下标i又代表什么。
  2. 再推导出状态转移方程。这里要采用分治思想,即把一个问题分解为一个规模更小的问题,重复这个操作直到不可再分。其实我觉得有点像分析递归时的思路,把一个大问题按照同一个套路分解为更小的问题,然后一直重复直到分解到初始状态。
  3. 然后确定动态规划数组dp的初始值。上面说到我们推导状态转移方程时是一步步分解直到不可再分,其实这里的不可再分我觉得就是指到达了初始状态,(也就是可以此时问题的规模已经小到通过我们人工分析就可以很容易地得到答案的地步),在这题中就是分解到了dp[0]的时候,我们已经很容易的可以通过定义准确地推断出dp[0]的值为nums[0]。所以确定初始值还是比较关键的一步的,因为根据动态规划的特点,后续的每一步状态推导结果都是基于前一步的结果,刚开始的初始值就出了问题的话,后面的结果也必然会出问题。当然,有时确定初始值不仅仅需要确定dp[0],可能还要确定dp[1]或是dp[2]之类的,这个就要根据题目的意思来具体分析了。
  4. 再就是确定我们遍历输入数组的起始位置和遍历顺序(从前往后还是从后往前)。其中起始位置就是我们该从输入数组的哪个下标开始对数组运用状态转移方程来获取相关的状态。就像我上面相关代码部分写的代码注释所说的。遍历顺序的话,我目前还没有自己接触过相关的问题,只是在别处了解到有这个注意点,所以暂且按下不表。
  5. 最后根据题目意思,我们可能要对dp数组做一些操作才能获得题目要求的结果,这就与具体题目要求相关了。比如这题中我们就应该想办法用一个变量保存dp数组中的最大值来作为题目的返回结果。

目前我接触的有关动态规划的题目还不多,LeetCode上的话,目前就做过这一道,上面的总结也是在结合我做该题的过程再综合参考了许多博主的经验所写的,实际体验还需要再进一步丰富才能有更好的总结。

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年7月29日19:55:15

参考:最大子序和 - 最大子序和 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年7月24日23:30:09
参考:手把手带你入门动态规划 | 对应力扣(leetcode)题号:509.斐波那契数_哔哩哔哩_bilibili
参考:从此再也不怕动态规划了,动态规划解题方法论大曝光 !| 理论基础 |力扣刷题总结| 动态规划入门_哔哩哔哩_bilibili
更新:2021年7月24日23:37:06
参考:如何理解动态规划? - 知乎
参考:什么是动态规划? - CTHON - 博客园
参考:【算法】详解动态规划_如风逝去-CSDN博客_动态规划
更新:2021年9月12日10:27:05

【我做的其他动态规划类题目】
参考:【算法-LeetCode】70. 爬楼梯(动态规划入门)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】300. 最长递增子序列(动态规划)_赖念安的博客-CSDN博客
参考:【算法-剑指 Offer】10- I. 斐波那契数列(递归;动态规划)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】322. 零钱兑换(动态规划;博采众长的完全背包问题详解;二维数组;滚动数组)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】718. 最长重复子数组(动态规划)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】64. 最小路径和(动态规划;二维数组)_赖念安的博客-CSDN博客
参考:【算法-剑指 Offer】62. 圆圈中最后剩下的数字(环形链表;约瑟夫环;动态规划)_赖念安的博客-CSDN博客
更新:2021年9月17日17:35:23
参考:【算法-LeetCode】121. 买卖股票的最佳时机(动态规划;贪心)_赖念安的博客-CSDN博客
更新:2021年9月19日22:45:01
参考:【算法-剑指 Offer】10- II. 青蛙跳台阶问题(动态规划)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】198. 打家劫舍(动态规划)_赖念安的博客-CSDN博客
更新:2021年9月23日23:37:15
参考:【算法-LeetCode】5. 最长回文子串(动态规划)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】62. 不同路径(动态规划;滚动数组)_赖念安的博客-CSDN博客
更新:2021年9月24日17:44:46
参考:【算法-LeetCode】509. 斐波那契数(递归;动态规划)_赖念安的博客-CSDN博客
更新:2021年10月15日17:26:39
参考:【算法-LeetCode】122. 买卖股票的最佳时机 II(动态规划;贪心)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】1143. 最长公共子序列(动态规划;滚动数组;通用的空间优化)_赖念安的博客-CSDN博客
更新:2021年10月23日24:13:39
参考:【算法-LeetCode】392. 判断子序列(indexOf;动态规划;双指针)_赖念安的博客-CSDN博客
更新:2021年10月23日15:05:53
参考:【算法-LeetCode】221. 最大正方形(动态规划;空间优化)_赖念安的博客-CSDN博客
更新:2021年11月14日18:09:04
参考:【算法-LeetCode】63. 不同路径 II(动态规划;滚动数组)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】674. 最长连续递增序列(动态规划;贪心;双指针)_赖念安的博客-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值