《编程之美》解题报告:2.14 求数组的子数组之和的最大值

引言

   

本文用了五种方法来求解这道题,从最初的n^3的复杂度到后面的n的复杂度,分别用到了递推公式、分而治之以及动态规划的方法去一步步降低算法的复杂度,《编程之美》书中关于分而治之的代码并没有提供,本文中将其补全,动态规划的代码与书中有所出入,个人感觉这样更好理解一些。

   

解题报告

   

首先我们很容易想到的一个解法就是三层遍历,首先子数组必定是连续的一串值,相当于从原数组array的下标范围0~n-1中选出ij,去算arra[i]~array[j]的和,于是我们可以得到最初的第一个解法

   

public static int sol1(int[] array) {

int lenght = array.length;

int result = Integer.MIN_VALUE;

int tempSum = 0;

for (int i = 0; i < lenght; i++) {

for (int j = i; j < lenght; j++) {

tempSum = 0;

for (int k = i; k <= j; k++) {

tempSum += array[k];

}

if (tempSum > result) {

result = tempSum;

}

}

}

return result;

}

   

接下来,由于array[i]~array[j]的和可以由array[i]~array[j-1]的和再与array[j]相加得到,所以我们可以将上面的代码减少一层循环,从而将复杂度降低到n^2

   

public static int sol2(int[] array) {

int lenght = array.length;

int result = Integer.MIN_VALUE;

for (int i = 0; i < lenght; i++) {

int tempSum = 0;

for (int j = i; j < lenght; j++) {

tempSum += array[j];

if (tempSum > result) {

result = tempSum;

}

}

}

return result;

}

   

现在已经降低到n^2了,但是我们并没有用到分而治之的方法,一般这种二分法可以将复杂度再一次降低到nlogn,那么如何去使用呢。

   

考虑将所给数组array(array[0]~array[n-1])分为长度相等的两段数组(array[0],...,array[n/2-1])(array[n/2],...,array[n-1])

   

然后分别求出这两段数组各自最大子段和,得到 MaxLeftBorderSum和MaxRightBorderSum

   

则原数组(array[0],...,array[n-1])的最大子段和分为以下三种情况:

   

a.(array[0],...,array[n-1])的最大子段和与(array[0],...,array[n/2-1])的最大子段和相同;

b.(array[0],...,array[n-1])的最大子段和与(array[n/2],...,array[n-1])的最大子段和相同;

c.(array[0],...,array[n-1])的最大子段跨过其中间两个元素array[n/2-1]array[n/2].

   

对应ab两个问题是规模减半的两个相同的子问题,可以用递归求得。

对于c,需要找到以array[n/2-1]结尾的和最大的一段数组和S1=(array[i],...,array[n/2-1])和以array[n/2]开始和最大的一段和S2=(array[n/2],...,array[j]),那么第三种情况的最大值为S1+S2

   

public static int sol3(int[] array, int left, int right) {

if (left == right) {// 只有一个元素

if (array[left] > 0)

return array[left];

else

return 0;

}

int start = left;

int mid = (left + right) / 2;

int end = right;

int MaxLeftBorderSum, MaxRightBorderSum; // 从中间分别到左右两侧的最大连续子序列值,对应c

int LeftBorderSum, RightBorderSum;

int MaxLeftSum = sol3(array, start, mid);

int MaxRightSum = sol3(array, mid + 1, end);

MaxLeftBorderSum = 0;

LeftBorderSum = 0;

for (int i = mid; i >= left; i--) {

LeftBorderSum += array[i];

if (LeftBorderSum > MaxLeftBorderSum)

MaxLeftBorderSum = LeftBorderSum;

}

MaxRightBorderSum = 0;

RightBorderSum = 0;

for (int i = mid + 1; i <= right; i++) {

RightBorderSum += array[i];

if (RightBorderSum > MaxRightBorderSum)

MaxRightBorderSum = RightBorderSum;

}

int max1 = MaxLeftSum > MaxRightSum ? MaxLeftSum : MaxRightSum;

int max2 = MaxLeftBorderSum + MaxRightBorderSum;

return max1 > max2 ? max1 : max2;

}

   

以上已经利用分而治之的方法将复杂度从n^2降低到了nlogn,考虑到还有一种降低复杂度的方法那就是动态规划,所以我们尝试用动态规划的思想去进一步降低复杂度

   

动态规划的思想是将一个大问题(N个元素数组)转换为一个较小的问题(n-1个元素数组)。

   

我们假设result[0]为已经找到数组[0,1,...n-1]中子数组和最大的,即保存当前找到的最大子数组。sum[i]为包含第i个元素且和最大的连续子数组。对于数组中的第i+1个元素有两种选择:

   

a.作为新子数组的第一个元素

b.放入前面已经找到的最大的子数组sum[i-1]中。

   

public static int sol4(int[] array) {

int n = array.length;

int[] sum = new int[n];

int[] result = new int[n];

sum[0] = array[0];

result[0] = array[0];

for (int i = 1; i < n; i++) {

sum[i] = max(array[i], array[i] + sum[i - 1]);

// A[i]>A[i]+sum[i-1],则作为新子数组的第一个元素

// 否则放入前面已经找到的最大的子数组sum[i-1]

result[i] = max(sum[i], result[i - 1]);

}

return result[n - 1];

}

   

以上的代码其实sum数组和result数组可以只用一个变量就可以了,不用真的用到一个数组,那么将这一点简化,可以得到简化版的最终方法,复杂度为n

   

static int sol5(int[] array, int n) {

int sum = array[0];

int result = array[0];

for (int i = 1; i < n; i++) {

sum = max(array[i], array[i] + sum);

result = max(sum, result);

}

return result;

}

posted @ 2015-05-20 09:52  keedor  阅读(208)  评论(0编辑  收藏  举报