LeetCode《初级算法》动态规划之最大子序和 -- JavaScript

401 阅读3分钟

题目

题目链接:leetcode-cn.com/leetbook/re…

image.png


题解

动态规划的难点在于划分出合适的子问题,确定状态转移方程,良好的状态转移方程会使得编程更加容易;

在本文的分析过程中,就体现了找到一个便于编程的状态转移方程的艰难过程;


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、定义优化函数(状态转移方程),列出递推方程


image.png

其中:

  • 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) 的定义和限制条件,递推方程可写为:

image.png


其实 f(k-1) = max( g(1) , ... , g(k-1) ),所以

image.png


根据这个递推方程,我们知道求出 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) );

于是有递推方程:

f(k)={nums[0],k=1max(f(k1),g(k)),k>1f(k) = \begin{cases} nums[0] , & k = 1 \\ max( f(k-1) , g(k) ) , & k > 1 \end{cases}

其中:

  • 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 的官方答案,还可以使用分治法,但是因为我没有去深入思考,这里就放出官方答案链接:

leetcode-cn.com/problems/ma…

里面涉及了 线段树 的概念;



大家如果有更好的思路和解法,欢迎大家一起来讨论啊~


这是使用 JavaScript 对 LeetCode《初级算法》的每道题的总结和实现的其中一篇,汇总篇在这里:

juejin.cn/post/700669…