问题描述
一个含有n个元素的整数数组,数组元素可以是正数也可以是负数,数组中连续的一个或多个元素称为改数组的子数组,子数组所有元素之和称为子数组和,求一个整数数组子数组和的最大值。例如数组array={1,-2,4,-3,5,-6},那么数组array的最大子数组和为: 4+(-3)+5=6。
分析求解
方法一:蛮力法
最容易想到,也是最简单的想法就是遍历这个数组的所有子数组,比较得出最大子数组的和。思路是:
子序列长度为1的所有子数组有n个:{a0},{a1},···,{an}
子序列长度为2的所有子数组有n-1个:{a0 , a1},{a1 , a2},···,{an-1 , an}
···
子序列长度为n的所有子数组有1个:{a0 , a1 , ··· , an}
算法:
/**
* get max sum of an array's subsequence by Brute Force.
*/
public static int getMaxSubArrayBF(int[] array) {
int maxSum = Integer.MIN_VALUE;
int tmpSum = 0;
/**
* i : 子数组长度
* j : 子数组起始位置
*/
for (int i = 1; i <= array.length; i++) {
for (int j = 0; j < array.length - i + 1; j++) {
tmpSum = 0;
for (int k = 0; k < i; k++) {
tmpSum += array[j + k];
if(maxSum < tmpSum)
maxSum = tmpSum;
}
}
}
return maxSum;
}
算法时间复杂度为O(n^3)。
方法二:改进蛮力法
方法一最明显的一个缺点就是重复计算,例如sum[i , j] = sum[i , j - 1] + array[j],也就是说计算较长的子数组和会用到较短子数组和,因此可以在计算子数组之和时重复利用已计算的子数组之和,减小时间复杂度。
算法:
/**
* get max sum of an array's subsequence by Majorizing Brute Force.
*/
public static int getMaxSubArrayMBF(int[] array) {
int maxSum = Integer.MIN_VALUE;
int tmpSum = 0;
// i : 子数组起始位置
// j : 子数组结束位置
for (int i = 0; i < array.length; i++) {
tmpSum = 0;
for (int j = i; j < array.length; j++) {
tmpSum += array[j];
if(maxSum < tmpSum)
maxSum = tmpSum;
}
}
return maxSum;
}
虽然还是蛮力法,但算法时间复杂度为O(n^2)。
方法三:动态规划
可以采用动态规划的方法来求解这一问题,首先以数组最后一个元素array[n-1]为例说明与最大子数组的关系,有两种情况:
- 最大子数组包含元素array[n - 1],即最大子数组是以元素array[n - 1]结尾的;
- 最大子数组不包含元素array[n - 1],那么求array[0 , 1 , ··· , n-1]最大子数组问题也就是求其子数组array[0 , 1 , ··· , n - 2]的最大子数组问题。
通过上述分析可知,这里可以将问题分解为规模较小的同类型子问题,而且规模较大问题可能会用到规模较小问题的结果,可以采用动态规划的思想:
记数组array[0 , 1 , ··· , i - 1]中包含其结尾元素array[i - 1]的最大的一段子数组和为endMaxSum[i - 1],因为必然包含元素array[i - 1],所以endMaxSum要么是包含array[i - 1]的一个子数组,要么就是单独一个元素array[i - 1]构成的子数组,因此:
endMaxSum[i - 1] = max{endMaxSum[i - 2] + array[i-1] , array[i - 1]}
假设已经计算出数组array[0 , 1 , ··· , i - 1]的最大子数组和为tmpMaxSum[i - 1],那么数组array[0 , 1 , ··· , i]的最大子数组和:
tmpMaxSum[i] = max{endMaxSum[i] , tmpMaxSum[i - 1]}
算法:
/**
* get max sum of an array's subsequence by Dynamic Programming
*/
public static int getMaxSubArrayDP(int[] array) {
int[] endMaxSum = new int[array.length];
int[] tmpMaxSum = new int[array.length];
endMaxSum[0] = tmpMaxSum[0] = array[0];
for (int i = 1; i < array.length; i++) {
endMaxSum[i] = max(endMaxSum[i - 1] + array[i], array[i]);
tmpMaxSum[i] = max(endMaxSum[i], tmpMaxSum[i - 1]);
}
return tmpMaxSum[array.length - 1];
}
private static int max(int a, int b) {
return a > b ? a : b;
}
算法时间复杂度为O(n)。该算法需要注意的是endMaxSum[i - 1]有两个特征:一定包含结尾元素array[i - 1],一定是一个连续的子数组(一个元素也可以构成子数组)。
方法四:改进动态规划
方法三有两个缺点:第一是使用了两个长度为n的数组endMaxSum和tmpMaxSum,增加了空间开销;第二是只能计算最大子数组之和,而无法得到最大子数组的位置。通过分析方法三的两个公式就很容易得出改进思路。
算法:
/**
* get max sum of an array's subsequence by Majorizing Dynamic Programming
*/
public static int getMaxSubArrayMDP(int[] array) {
int startPos = 0;
int endPos = 0;
//相当于方法三中的tmpMaxSum
int maxSum = array[0];
//相当于方法三中的endMaxSum
int endMaxSum = array[0];
for (int i = 1; i < array.length; i++) {
if(endMaxSum > 0) {
endMaxSum = endMaxSum + array[i];
if(endMaxSum > maxSum) {
maxSum = endMaxSum;
endPos = i;
}
}
else {
endMaxSum = array[i];
if(endMaxSum > maxSum) {
maxSum = endMaxSum;
startPos = i;
endPos = i;
}
}
}
System.out.println("From " + startPos + " to " + endPos);
return maxSum;
}
算法时间复杂度仍然是O(n),但是空间复杂度降低到O(1),而且可以得到最大子数组的位置。
总结
动态规划法(Dynamic Programming)与分治法类似,是将问题一层一层地分解为规模逐渐减小的同类型的子问题。其与分治法的一个重要不同点在于:用分治法分解后得到的子问题通常都是相互独立的,而用动态规范发分解后得到的子问题很多都是重复的。由于各个子问题相互独立,分治法通常采用递归调用发生而避免重复计算,动态规划法通常采用递推的方法,从规模最小的子问题开始算起,依次计算规模逐渐扩大的子问题,由于计算大子问题时往往要使用小规模子问题的结果,因此每次计算完毕后都要把所得的结果保存起来,按此方法不断扩大计算规模,直至达到所求问题的规模。
若一个问题可以分解为若干个高度重复的子问题,且问题也具有最优子结构性质,就可以用动态规划法求解,以递推的方式逐层计算最优值并记录必要的信息,最后根据记录的信息构造最优解。解题步骤可归纳为:
- 找出最优解的性质,并刻画其结构特性;
- 递归地定义最优值(写出动态规划方程);
- 自底向上(规模从小到大)地递推方式计算出最优值;
- 根据计算最优值时得到的信息,以递归方法构造一个最优解。