题目
题目链接:leetcode-cn.com/leetbook/re…
题解
动态规划的难点在于划分出合适的子问题,确定状态转移方程,良好的状态转移方程会使得编程更加容易;
在本文的分析过程中,就体现了找到一个便于编程的状态转移方程的艰难过程;
1、动态规划常规分析
为什么要使用动态规划,使用动态规划可以减少计算,当前问题的解可以由记录下来的子问题的解求的;以下是本问题使用动态规划的分析过程;
1.1、划分子问题、确定子问题的边界
对于长度为 k 的整数数组 nums, 设具有最大和的连续子数组的和为 f(k) ;
需要一个数组 result 来记录具有最大和的子序列;
-
当长度为 1 时,具有最大和的连续数组的和为 f(1) = nums[0];
-
当长度为 2 时,具有最大和的连续数组的和为f(2) = max( f(1) , nums[1] , f(1) + nums[1] ) ;
-
当长度为 k 时
如果 result 是以 nums[k-2] 结尾,
- 且 nums[k-1] 大于0,则 f(k) = f(k-1) + nums[k-1] ;
- nums[k-1] 小于 0 时,f(k) = max(f(k-1) , nums[k-1] ) ;
如果 result 不是以 nums[k-2] 结尾,
- 如果 nums[k-1] 大于 0,则 f(k) = max( f(k-1) , 以nums[k-1] 结尾的子串中和最大的子串的和(设其为 g(k-1) );
- 如果 nums[k-1] 小于 0 ,则 f(k) = max(f(k-1) , nums[k-1] ) ;
1.2、定义优化函数(状态转移方程),列出递推方程
其中:
- f(k):nums 这个序列的最大子序和;
- nums为待求整数数组;
- nums[k-1] 为第 k 个整数;
- result:子序列中具有最大子序列和的序列
- g(k) :以nums[k-1] 为结尾的序列中具有最大序列和的序列的和;
结论: 由上面的状态转移方程我们知道,f(k) 的大小依赖于 f(k-1) 的大小,其满足优化原则,所以可以使用动态规划;
进一步分析:
由
- f(k) : nums 这个序列的最大子序列和
- g(k):以nums[k-1] 为结尾的子序列中具有最大序列和的子序列的和;
根据 g(k) 的定义和限制条件,递推方程可写为:
其实 f(k-1) = max( g(1) , ... , g(k-1) ),所以
根据这个递推方程,我们知道求出 g(1) ... g(nums.length) 和 result 就可以得到 f(nums.length) ;
求解 g(x) 的算法描述 (注意 k 从 1 开始,nums 下标从 0 开始)
- 当 k = 1,g(1) = nums[0];
- 当 k = 2,g(1) > 0,g(2) = g(1) + nums[1] ; 当 g(1) <= 0 , g(2) = nums[1] ;
- 当 k = i ,g(i-1) > 0, g(i) = g(i-1) + nums[i-1] ; 当 g(i-1) <= 0 , g(i) = nums[i-1] ;
在求解 g(x) 的过程中,可以将对应的子序列 result 求出;
1.3、代码实现
代码可以执行,但是面对大量的数据,因为 g 和 result 的缓存太大了,导致超出了 leetcode 限制的堆大小;实用代码要用下面的优化算法;
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
if(nums.length === 1) {
return nums[0];
}
// 求解 g(x) 和对应的 子序列result
function getG(nums) {
const g = [];
const len = nums.length;
for(let i = 1;i <= len;i++) {
// console.log(g,'----')
if(1 === i) {
g.push({
sum:nums[0],
result:[nums[0]]
})
}else {
const preSum = g[i-2].sum;
if( preSum > 0) {
g.push({
sum:preSum + nums[i-1],
result:[...g[i-2].result,nums[i-1]]
})
}else {
g.push({
sum:nums[i-1],
result:[nums[i-1]]
})
}
}
}
return g;
}
// 求 f(nums.length)
function ms(nums,g) {
const len = nums.length;
let preMaxSum = g[0].sum;
g.slice(0,len-2).forEach(item => {
if(preMaxSum < item.sum) {
preMaxSum = item.sum;
}
})
if(nums[len-1] < 0 ) {
return Math.max(preMaxSum,g[len-2].sum,nums[len-1]);
}else {
if(g[len-2].result[g[len-2].result.length] === nums[len-2]) {
return g[len-1].sum;
}else {
return Math.max(preMaxSum,g[len-1].sum);
}
}
}
const g = getG(nums); // g[k]={以 nums[k-1] 为结尾的具有最大子序和的子序列的和sum,这个子序列result}
return ms(nums,g);
};
2、动态规划分析的思路改进
2.1、分析
动态规划的难点在于如何划分子问题,确定状态转移方程;
本题中,求解 f(k) 时 ,将 以某个元素为结尾的和最大的子序列的和构成一个数组 g(k),进一步思考,我们会惊奇的发现 f(k) = max( g(1) ... g(k) );
于是有递推方程:
其中:
- k = nums.length ;(nums 为待求整数数组)
由递推方程可知其满足优化原则;
2.2、代码实现
借助一个辅助数组用来存储 g(k) , 1<= k <= nums.length ;
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
const g = []; // 存储 g(k) , 1<= k <= nums.length ;
g.push(nums[0])
const len = nums.length;
for(let i = 1;i < len;i++) {
const preSum = g[i-1];
if( preSum > 0) {
g.push(preSum + nums[i])
}else {
g.push(nums[i])
}
}
const gLen = g.length;
let MaxSum = g[0];
// 在 g[0] ... g[nums.length - 1] 中找最大值
for(let i = 1;i < gLen;i++) {
if(sum < g[i]) {
sum = g[i];
}
}
return MaxSum;
};
2.3、代码优化
因为 f(k) 的大小只由 f(k-1) 和 g(k) 决定,所以可以在计算 g(k) 的过程中将 f(k-1) = max(g(1) ,...,g(k-1)) 求出,这样就不需要使用一个数组来保存 g(1) ... g(k-1) 了,只需要使用一个变量保存 f(k-1) ;
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
const len = nums.length;
let g = nums[0];
let maxSum = g; // 只使用一个变量记录最大值
for(let i = 1;i<len;i++) {
if(g>0) {
g += nums[i];
}else {
g = nums[i];
}
// 更改最大值
if(g > maxSum) {
maxSum = g;
}
}
return maxSum;
};
3、分治法
根据 leetcode 的官方答案,还可以使用分治法,但是因为我没有去深入思考,这里就放出官方答案链接:
里面涉及了 线段树 的概念;
大家如果有更好的思路和解法,欢迎大家一起来讨论啊~
这是使用 JavaScript 对 LeetCode《初级算法》的每道题的总结和实现的其中一篇,汇总篇在这里: