给定K个整数组成的序列{ N1, N2, …, NK },“连续子列”被定义为{ Ni, Ni+1, …, Nj },其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 11, -4, 13, -5, -2 },其连续子列{ 11, -4, 13 }有最大的和20。现要求你编写程序,计算给定整数序列的最大子列和。
本题旨在测试各种不同的算法在各种数据情况下的表现。各组测试数据特点如下:
数据1:与样例等价,测试基本正确性;
数据2:102个随机整数;
数据3:103个随机整数;
数据4:104个随机整数;
数据5:105个随机整数;
输入格式:
输入第1行给出正整数K (≤100000);第2行给出K个整数,其间以空格分隔。
输出格式:
在一行中输出最大子列和。如果序列中所有整数皆为负数,则输出0。
输入样例:
6
-2 11 -4 13 -5 -2
输出样例:
20
算法一:
用三重循环,外重循环遍历子列左端位置,中间层循环遍历子列右端位置,内层循环对i到j的子列进行求和。
/**
* 算法一,三重循环
* @param a 数组a
* @param k 数组a的长度
* @return MaxSum 最大子列和
*/
private static int MaxSubseqSum1(int[] a, int k) {
int ThisSum ,MaxSum = 0;
for (int i = 0; i MaxSum) {
MaxSum = ThisSum; //如果刚得到的这个子列和更大,则更新结果
}
}
}
return MaxSum;
}
代码提交之后,5个测试用例仅能通过两个,看来算法还需要在完善,如图:
算法二:
用两重循环,外重循环遍历子列左端位置,内层循环遍历子列右端位置,对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可,减少了一层循环。
/**
* 算法二,用两重循环
* @param a 数组a
* @param k 数组a的长度
* @return MaxSum 最大子列和
*/
private static int MaxSubseqSum2(int[] a, int k) {
int ThisSum ,MaxSum = 0;
for (int i = 0; i < k; i++) { //子列左端位置
ThisSum = 0; //ThisSum是从A[i]到A[j]的子列和
for (int j = i; j < k; j++) { //子列右端位置
ThisSum += a[j];
if (ThisSum > MaxSum ) {
MaxSum = ThisSum; //如果刚得到的这个子列和更大,则更新结果
}
}
}
return MaxSum;
}
算法二减少了一层循环,很明显这个算法的时间复杂度为
代码提交之后,5个测试用例仅能通过4个,当数据规模达到105个随机整数时,内存超限了,当如图:
算法三:
用分治法来处理,每次将序列从中间划分开,分别处理左子列、右子列、以及跨分界线的最大子列和,返回三者中的最大值。
举个例子:
/**
* 算法三,保持与前2种算法相同的函数接口
* @param a
* @param k
* @return
*/
private static int MaxSubseqSum3(int[] a, int k) {
return DivideAndConquer(a,0,k-1);
}
/**
* 返回3个整数中的最大值
* @param A
* @param B
* @param C
* @return
*/
private static int max3(int A,int B,int C) {
return A>B?A>C?A:C:B>C?B:C;
}
/**
* 分治法求a[left]到a[right]的最大子列和
* @param a 数组a
* @param left
* @param rignt
* @return
*/
private static int DivideAndConquer(int[] a, int left, int right) {
int MaxLeftSum,MaxRightSum; //存放左右子问题的解
int MaxLeftBorderSum,MaxRightBorderSum; //存放跨分界线的结果
int LeftBorderSum,RightBoederSum;
int center;
if (left == right) { //递归的终止条件,子列只有1个数字
if (a[left] > 0) {
return a[left];
}else {
return 0;
}
}
//下面是"分"的过程
center = (left + right)/2; //找到中分点
//递归求得两边子列的最大和
MaxLeftSum = DivideAndConquer(a, left, center);
MaxRightSum = DivideAndConquer(a, center+1, right);
//下面求跨分界线的最大子列和
//从中间往左边扫描
MaxLeftBorderSum = 0;
LeftBorderSum = 0;
for (int i = center; i >= left; i--) {
LeftBorderSum += a[i];
if (LeftBorderSum > MaxLeftBorderSum) {
MaxLeftBorderSum = LeftBorderSum;
}
}
//从中间往右边扫描
MaxRightBorderSum = 0;
RightBoederSum = 0;
for (int i = center+1; i <= right; i++) {
RightBoederSum += a[i];
if (RightBoederSum > MaxRightBorderSum) {
MaxRightBorderSum = RightBoederSum;
}
}
//下面返回"治"的结果
return max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum+MaxRightBorderSum);
}
由于分治法将问题划分为两个子问题,每个子问题的规模都为原来的一半,最后还要加上处理跨分界线的最大子列和的规模(即将整个序列扫描一遍,为N),所以算法的时间复杂度为O(N logN)。
算法四:
在线处理。“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。
/**
* 算法四,在线处理
* @param a 数组a
* @param k 数组a的元素个数
* @return MaxSum 最大子列和
*/
private static int MaxSubseqSum4(int[] a, int k) {
int ThisSum,MaxSum;
ThisSum = MaxSum = 0;
for (int i = 0; i < k; i++) {
ThisSum += a[i]; //向右累加
if (ThisSum > MaxSum) {
MaxSum = ThisSum; //发现更大和则更新当前结果
}else if (ThisSum < 0) {
ThisSum = 0; //如果当前子列和为负,则不可能使后面的部分和增大,抛弃之
}
}
return MaxSum;
}
算法四只有一层for循环,时间复杂度是O(N)。也是这个问题所能达到的最佳时间复杂度了,毕竟,要求出序列的最大子列和至少的把整个扫面一遍吧。这种方法可以算是近乎完美的一种求解最大子列和的算法。
这种算法效率之所以如此高,是因为任何负的子序列都不可能是最有子序列的前缀,所以我们可以知道,只要是首位元素为负数的肯定不是最大子列的组成部分,我们可以将其抛弃。即如果某个子列的首位为负数,那么它一定要借助后面的非负数改进。
而且这种算法还有一种优点就是,它只对数据扫描一次,在任何时刻,算法都可以对它已经读入的数据给出正确的答案。
另外,给出调用以上算法的main()方法:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int K = scanner.nextInt();
int[] a = new int[K];
for (int i = 0; i < K; i++) {
a[i] = scanner.nextInt();
}
//int ans = MaxSubseqSum1(a,K);
//int ans = MaxSubseqSum2(a,K);
//int ans = MaxSubseqSum3(a,K);
int ans = MaxSubseqSum4(a,K);
System.out.println(ans);
}