题目:给定N个整数的序列{A1,A2,...,AN},求函数
分析:A1,A2,...,AN是可正可负的整数序列,本题欲求从i到j的子序列和的最大值(若结果为负则输出0,这是与牛课网上最大连续数列和这道题的不同之处,只需将本题代码中max_sum的初始值由0改为INT_MIN即可求解牛客网上那道题),不必求出是哪个序列。最容易想到的办法是暴力求解法,即求出所有子序列的和,找出它们的最大值,时间复杂度T(N)=O(N2);其次我们可以采取分而治之的方法把欲求解的问题递归分解为若干子问题,时间复杂度T(N)=O(NlogN);最后我们可以用更简单的算法——在线处理(动态规划)来解决这个问题,“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解,用这种算法解决该问题的时间复杂度T(N)=O(N)。
算法一(暴力求解法):
int MaxSubseqsum1(int A[], int N) { int cur_sum, max_sum = 0; // 当序列元素全为负时,输出0 // 对于牛课上那题,max_sum初始值改为INT_MIN for (int i = 0; i < N; i++) // i是子列左端位置 { cur_sum = 0; // cur_sum是子列A[i]...A[j]的和 for (int j = i; j < N; j++) // j是子列右端位置 { cur_sum += A[j]; if (cur_sum > max_sum) max_sum = cur_sum; } } return max_sum; }
显然这种算法的时间复杂度为=O(N2)
算法二(分治法):
最大子序列可能在三个地方出现,序列的左半部、右半部或者跨越序列中点而占据左右两部分。前两种情况递归求解,第三种情况的最大和可以通过求出左半部分最大和(包含左半部分最后一个元素)以及右半部分最大和(包含右半部分的第一个元素)相加而得到。
int MaxSubseqsum2(int A[], int N) { return maxSumRec(A, 0, N - 1); } int maxSumRec(int A[], int left, int right) // 递归求解,复杂度O(nlogn) { if (left == right) // 递归终止条件,子列只有1个数字 { if (A[left] > 0) return A[left]; // 对于牛课上那题,直接return A[left]; else return 0; } /* 下面是"分"的过程 */ int mid = (left + right) / 2; int maxLeftSum = maxSumRec(A, left, mid); // 递归求解左半部分 int maxRightSum = maxSumRec(A, mid + 1, right); // 递归求解右半部分 /* 下面求跨分界线的最大子列和 */ // 求出左半部分包含最后一个元素的最大子序列和 int leftBorderSum = 0, maxLeftBorderSum = 0; // 对于牛课上那题,maxLeftBorderSum初始值改为INT_MIN for (int i = mid; i >= left; i--) // 从中线向左扫描 { leftBorderSum += A[i]; if (leftBorderSum > maxLeftBorderSum) maxLeftBorderSum = leftBorderSum; } // 求出右半部分包含第一个元素的最大子序列和 int rightBorderSum = 0, maxRightBorderSum = 0; // 对于牛课上那题,maxRightBorderSum初始值改为INT_MIN for (int j = mid + 1; j <= right; j++) // 从中线向右扫描 { rightBorderSum += A[j]; if (rightBorderSum > maxRightBorderSum) maxRightBorderSum = rightBorderSum; } return max3(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum); } int max3(int a, int b, int c) // 求出三者中的最大值 { int max = a; if (max < b) max = b; if (max < c) max = c; return max; }
分治法的原理图如下:
推算算法的时间复杂度,求解整个整数序列可分写为求解左半部分、右半部分以及跨越序列中点的子序列:T(N)=2T(N/2)+O(N) => T(N)=2T(N/2)+c1N (c1为一常数) => T(N)=22T(N/22)+2c1N => ...... => T(N)=2kT(1)+kc1N (N/2k=1 => k=log2N) => T(N)=c2N+c1Nlog2N
综上,T(N)=O(NlogN) (也可以用主方法直接得出)
算法三(在线处理/动态规划):主要思路是最大子列不可能存在累加和为负的前缀
int MaxSubseqsum3(int A[], int N) { int cur_sum = 0, max_sum = 0;// 对于牛课上那题,max_sum初始值改为INT_MIN for (int i = 0; i < N; i++) { cur_sum += A[i]; // 向右累加 if (cur_sum > max_sum) max_sum = cur_sum; // 发现更大和则更新当前结果 if (cur_sum < 0) // 如果当前子列和为负 cur_sum = 0; // 则不可能作为最大子列的前缀,扔掉 } return max_sum; }
显然时间复杂度T(N)=O(N),需要注意的是:虽然,如果有以A[i]结尾的某子列和是负数就表明了这个子列不可能是与A[i]后面的数形成的最大子列,但是并不表明A[i]前面的某个子列就不是最大子列。
用这种算法也可以求出最大子列(位置),代码如下:
int max_left, max_right; int MaxSubseqsum3(int A[], int N) { int cur_sum = 0, max_sum = 0;// 对于牛课上那题,max_sum初始值改为INT_MIN int left = 0, right; // 记录当前子列和左端和右端 for (int i = 0; i < N; i++) { right = i; cur_sum += A[i]; // 向右累加 if (cur_sum > max_sum) { max_sum = cur_sum; // 发现更大和则更新当前结果 max_left = left; // 更新最大子列左端 max_right = right; // 更新最大子列右端 } if (cur_sum < 0) // 如果当前子列和为负 { cur_sum = 0; // 则不可能作为最大子列的前缀,扔掉 left = i + 1; // 更新当前子列左端 } } return max_sum; }