[LeetCode]53. Maximum Subarray(暴力穷举+Dynamic Programming+Kadane)

[LeetCode]53. Maximum Subarray

  • 一、题目描述
  • 二、暴力穷举
  • 三、Dynamic Programming
    • 3.1 DP关键步骤
      • 3.1.1 定义子问题
      • 3.1.2 设置边界和初始条件/递推基
      • 3.1.3 方程/递推关系
    • 3.2 DP实现
  • 四、Kadane
    • 4.1 算法描述
    • 4.2 Kadane实现

一、题目描述

Problem Description:
Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

Example:
Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Follow up:
If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.

二、暴力穷举

暴力破解的解题思路比较好理解,其思想就是遍历穷尽给定数组nums的所有连续子数组,比较各个子数组的元素之和,最后求得一个最大连续子数组之和。算法的时间复杂度为O(n3)。

/*
 *暴力穷举
 *遍历nums中所有的子数组,比较所有子数组求和的大小,通过滑动窗口来实现
 *算法时间复杂度O(n^3)(子数组数量O(n^2),子数组求和O(n))
 */
class Solution {
    public int maxSubArray(int[] nums) {
		int maxSum = nums[0];
		for(int i = 0; i < nums.length; i++){
			for(int j = i+1; j < nums.length+1; j++){
                int subSum = 0;
				//子数组求和
				for(int k = i; k < j; k++){
					subSum = subSum + nums[k];
				}
				if(subSum > maxSum){
					maxSum = subSum;
				}
			}
		}
		return maxSum;
    }
}

针对上述代码O(n3)的时间复杂度,还可以进行简单的优化,减少不必要的循环,在第二层循环直接进行求和并比较。这样优化后代码的时间复杂度为O(n2)。

/*
 *暴力穷举优化
 *第二层循环中直接进行求和并比较,减少最内层循环,减少重复运算
 *算法时间复杂度O(n^2)
 */
class Solution {
    public int maxSubArray(int[] nums) {
		int maxSum = nums[0];
		for(int i = 0; i < nums.length; i++){
			int subSum = 0;
			for(int j = i; j < nums.length; j++){
                subSum = subSum + nums[j];
				if(subSum > maxSum){
					maxSum = subSum;
				}
			}
		}
		return maxSum;
    }
}

虽然优化后的代码时间复杂度由原来的O(n3)降到了O(n2),但仍过于复杂,所以我们需要用更加高效的方法去解决此题。
下一章节采用了动态规划来解决此题,时间复杂度降至O(n)。

三、Dynamic Programming

Dynamic Programming关键三步骤:
·定义子问题
·设置边界和初始条件/递推基
·方程/递推关系

接下来,结合本例题简单介绍一下Dynamic Programming关键三步,然后给出通过Dynamic Programming解决此题的完整代码。

3.1 DP关键步骤

3.1.1 定义子问题

动态规划问题求解需要先开一个数组dp[nums.length],并确定数组的每个元素dp[i]代表什么,就是确定这个问题的状态。

常见的定义子问题的方式有两种:
1、定义目标问题为子问题
2、定义非目标问题为子问题,目标问题的解可以通过所保存的所有子问题的解来获得

针对本例题分别使用这两种方式来来定义子问题:
1、定义目标问题为子问题
dp[i] = 数列长度为(i+1)的最大子列和,dp[0]=-2,dp[1]=1,dp[2]=1,dp[3]=4……我们发现很难找到它们之间的递推关系。
2、定义非目标问题为子问题
将子问题定义为:以第 (i+1) 个数结尾/以 i 为终止下标的子数列和的最大值,即**以 nums[i] 作为最大子数列的末尾元素时, 能找到的最大子数列和**。
nums[i] 作为末尾元素的最大子数列和
则dp[i]用以下形式表示:
dp[i] = 以第(i+1)个数结尾的子列和的最大值 = max{x,以第i个数结尾的子列和的最大值 + x} (其中,x为第(i+1)个数)
最后原始问题的解就是dp数组的最大元素值,即max(dp)

3.1.2 设置边界和初始条件/递推基

由于dp[i]的值与dp[i-1]有关系,循环过程中涉及到了 i -1,所以循环初始化 i = 1 ,那么就要人为的添加初始条件:dp[0] = nums[0];

3.1.3 方程/递推关系

递推关系即如何通过先前保存的子问题的解获得当前解。

首先确定计算顺序:
从dp[0]、dp[1]开始?还是从dp[n]、dp[n-1]开始?

判断计算顺序正确与否的原则是:
当我们要计算等式左边dp[i]时,等式右边都是已经得到结果的状态,这个计算顺序就是正确的。也就是数组从左到右/数组索引从小到大的计算方式。

递推关系:dp[i] = nums[i] + (dp[i-1] > 0 ? dp[i-1] : 0);
dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);

最后返回dp数组的最大元素值。

3.2 DP实现

算法时间复杂度为O(n),空间复杂度为O(n)。

/*
 *动态规划
 *核心:dp[i] = Math.max(dp[i-1] + nums[i], nums[i])
 *算法时间复杂度为O(n),空间复杂度为O(n)
 */
class Solution {
    public int maxSubArray(int[] nums) {
		int length = nums.length;
		Integer[] dp = new Integer[length]; // dp[i] means the maximum subarray ending with nums[i]
        dp[0] = nums[0];
		for(int i = 1; i < length; i++){
			//dp[i] = nums[i] + (dp[i-1] > 0 ? dp[i-1] : 0);
			dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);
		}
		return (int)Collections.max(Arrays.asList(dp)); // get maximum element value from dp[]
    }
}

上述代码中最后返回dp数组的最大元素值,即返回最大连续子数组之和,采用的是Collections.max()方法:(int)Collections.max(Arrays.asList(dp));

当然获取数组最大元素值还有很多其他的方式实现,比如:数组排序后返回第一个或最后一个元素值 等等。

换一个思路,在循环过程中增加一步:将每次得到的dp[i]与变量maxSum(最大子列和)进行比较,最后不需要求解max(dp),只需要返回maxSum即可。

算法时间复杂度为O(n),空间复杂度为O(n)。

/*
 *动态规划优化
 *核心:dp[i] = Math.max(dp[i-1] + nums[i], nums[i])
 *算法时间复杂度为O(n),空间复杂度为O(n)
 */
class Solution {
    public int maxSubArray(int[] nums) {
		int length = nums.length;
		int maxSum = nums[0];
		int[] dp = new int[length]; // dp[i] means the maximum subarray ending with nums[i]
        dp[0] = nums[0];
		for(int i = 1; i < length; i++){
			//dp[i] = nums[i] + (dp[i-1] > 0 ? dp[i-1] : 0);
			dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);
			maxSum = Math.max(maxSum, dp[i]);
		}
		return maxSum;
    }
}

以上代码的提交结果:
[LeetCode]53. Maximum Subarray(暴力穷举+Dynamic Programming+Kadane)_第1张图片

四、Kadane

4.1 算法描述

该问题最早于1977年提出,但是直到1984年才被Jay Kadane 发现了线性时间的最优解法,所以算法虽然长度很短,但其实并不容易理解。

Kadane算法扫描一次整个数列的所有数值,在每一个扫描点计算以该点数值为结束点的子数列的最大和(正数和)。该子数列由两部分组成:以前一个位置为结束点的最大子数列、该位置的数值。因为该算法用到了“最佳子结构”(以每个位置为终点的最大子数列都是基于其前一位置的最大子数列计算得出),该算法可看成动态规划的一个例子。

以上信息来自百度百科: 最大子数列问题.

有些其他算法题可以通过转化为最大子数组之和的问题,用kadane算法求解。

4.2 Kadane实现

Kadane算法是在DP基础上的进一步优化。也可以将Kadane看作是DP算法的一个应用。

由于在DP优化代码中每一次循环都将得到的dp[i]与变量maxSum(最大子列和)进行比较maxSum = Math.max(maxSum, dp[i]);,最后我们用不到这个dp数组;而且 dp[i] 只依赖于 dp[i-1],因此没有必要把前面所有的信息都存起来。所以为了节省空间考虑,可以直接使用一个变量 subSum 来存储以nums[i]作为末尾元素的最大子列和。

/*
 *Kadane算法(计算最大子列和)
 *核心:以第(i+1)个数结尾的子列和 = max{x,以第i个数结尾的子列和+x} (其中,x为第(i+1)个数)
 *		数列长度为(i+1)的最大子列和 = max{以第(i+1)个数结尾的子列和,数列长度为i的最大子列和}
 *算法复杂度O(N)
 */
class Solution {
    public int maxSubArray(int[] nums) {
		int maxSum = nums[0]; // maxSum不能初始化为0,因为当数组元素全小于0时会出错
		int subSum = nums[0];
		for(int i = 1; i < nums.length; i++){
			subSum = Math.max(nums[i], subSum+nums[i]);
			maxSum = Math.max(maxSum, subSum);
		}
		return maxSum;
    }
}

以上代码的提交结果:
[LeetCode]53. Maximum Subarray(暴力穷举+Dynamic Programming+Kadane)_第2张图片

你可能感兴趣的:([LeetCode]53. Maximum Subarray(暴力穷举+Dynamic Programming+Kadane))