算法优化示例_最大连续子序列和问题
问题描述:
给定(可能是负的)整数序列
A
1
,
A
2
,
A
3
,
⋯
,
A
N
A_1,A_2,A_3,\cdots,A_N
A1,A2,A3,⋯,AN,寻找并标识
∑
k
=
i
j
A
k
\sum_{k=i}^{j}A_k
∑k=ijAk的值为最大的序列。(如果所有的整数都是负的,那么最大连续子序列的和为0)
O ( N 3 ) O(N^3) O(N3)算法——枚举法
最简单也是最容易想到的算法就是直接枚举一个个子序列,从中找到和值最大的。
代码如下:
//最大连续子序列和的O(n^3)算法
#include<iostream>
#include<ctime>
using namespace std;
int maxSubsequenceSum(int a[], int size, int& start, int& end);
//函数声明,因为要输出结果序列的始末位置,所以start和int采用引用传递
int main() {
time_t begin, end;//计时
double ret;
begin = clock();
int array[] = { 21,-15,23,-12,4,-23,18,-10,8,-19,26,-5,24,-29,24,25,-15,3,30,8,-15,8,-19,12,-4,-29,27,-4,8,21,-28,-13,7,-1,-26,29,9,11,-3,29,-13,23,-22,24,9,-8,-22,1,23,-27 };
int start, end1, size, maxArraySum;
size = sizeof(array)/4;
maxArraySum = maxSubsequenceSum(array, size, start, end1);
cout << "最大连续子序列的和为" << maxArraySum<<endl;
cout << "为从第" << start+1 << "到" << end1+1 << "项的和" << endl;
end = clock();
ret = double(end - begin) /CLOCKS_PER_SEC;
cout << "runtime: " << ret << endl;
return 0;
}
//函数定义
int maxSubsequenceSum(int a[], int size, int& start, int& end) {
int maxSum = 0;//当前子序列的最大和
for (int i = 0; i < size; i++)//子序列的起始位置
{
for (int j = i; j < size; j++)//子序列的结束位置
{
int thisSum = 0;//计算当前子序列的和
for (int k = i; k <= j; k++)
{
thisSum += a[k];
}
if (thisSum > maxSum) {
maxSum = thisSum;//更新子序列和最大值
start = i;//记录起始位置
end = j;//记录结束位置
}
}
}
return maxSum;
}
运行结果如下:
代码分析:
实现原理简单,但效率不高
时间复杂度:
∑
i
=
0
n
∑
j
=
i
n
∑
k
=
i
j
1
=
n
(
n
+
1
)
(
n
+
2
)
/
6
\sum_{i=0}^n\sum_{j=i}^{n}\sum_{k=i}^j1=n(n+1)(n+2)/6
i=0∑nj=i∑nk=i∑j1=n(n+1)(n+2)/6
即
O
(
n
3
)
O(n^3)
O(n3)
三层嵌套循环导致了组合爆炸的出现,为了改进算法,可以考虑去掉一层循环,于是就有了下面的算法。
O ( N 2 ) O(N^2) O(N2)算法——改进枚举法
在计算第
i
i
i到第
j
j
j个元素的子序列和时用了一层循环,事实上,在计算从第
i
i
i到
j
−
1
j-1
j−1个元素的子序列和时,已经得到了第
i
i
i到
j
−
1
j-1
j−1个元素的子序列和。而
∑
k
=
i
j
A
k
=
∑
k
=
i
j
−
1
A
k
+
A
j
\sum_{k=i}^{j}A_k=\sum_{k=i}^{j-1}A_k+A_j
∑k=ijAk=∑k=ij−1Ak+Aj,因此只做一次加法就能得到结果。
代码如下:
//最大连续子序列和的O(n^2)算法
#include<iostream>
#include<ctime>
using namespace std;
int maxSubsequenceSum(int a[], int size, int& start, int& end);
//函数声明,因为要输出结果序列的始末位置,所以start和int采用引用传递
int main() {
time_t begin, end;//计时
double ret;
begin = clock();
int array[] = { 21,-15,23,-12,4,-23,18,-10,8,-19,26,-5,24,-29,24,25,-15,3,30,8,-15,8,-19,12,-4,-29,27,-4,8,21,-28,-13,7,-1,-26,29,9,11,-3,29,-13,23,-22,24,9,-8,-22,1,23,-27 };
int start, end1, size, maxArraySum;
size = sizeof(array)/4;
maxArraySum = maxSubsequenceSum(array, size, start, end1);
cout << "最大连续子序列的和为" << maxArraySum<<endl;
cout << "为从第" << start+1 << "到" << end1+1 << "项的和" << endl;
end = clock();
ret = double(end - begin) /CLOCKS_PER_SEC;
cout << "runtime: " << ret << endl;
return 0;
}
//函数定义
int maxSubsequenceSum(int a[], int size, int& start, int& end) {
int maxSum = 0;//当前子序列的最大和
for (int i = 0; i < size; i++)//子序列的起始位置
{
int thisSum = 0;//计算当前子序列的和
for (int j = i; j < size; j++)//子序列的结束位置
{
thisSum += a[j];
if (thisSum > maxSum) {
maxSum = thisSum;//更新子序列和最大值
start = i;//记录起始位置
end = j;//记录结束位置
}
}
}
return maxSum;
}
运行结果如下:
代码分析:
将方法一中的循环用加法替代,减少了算法复杂度
时间复杂度:
∑
i
=
0
n
∑
j
=
i
n
1
=
n
(
n
+
1
)
/
2
\sum_{i=0}^n\sum_{j=i}^n1=n(n+1)/2
i=0∑nj=i∑n1=n(n+1)/2
即
O
(
n
2
)
O(n^2)
O(n2)
O ( n l o g n ) O(nlogn) O(nlogn)算法——分治法
输入的序列可以划分为两部分,这样最大值和值的连续子序列可能出现在下面3种情况中:
- 情况一:最大和值的子序列位于前半部分
- 情况二:最大和值的子序列位于后半部分
- 情况三:从前半部分开始但在后半部分结束
前两种情况只需要通过递归调用即可以完成。问题是第三种情况如何解决?
可以从两半部分的边界开始,通过从右向左的扫描来找到左半部分的最大序列,通过从左向右的扫描来找到右半部分的最大序列,把这两个子序列组合起来,形成跨越分割边界的最长连续子序列。
算法步骤:
- 递归计算整个位于前半部分的最长连续子序列
- 递归计算整个位于后半部分的最长连续子序列
- 通过两个连续循环,计算从前半部分开始但是在后半部分结束的最长连续子序列的和
- 选择上述3个子问题中的最大值,作为整个问题的解
代码如下:
//最大连续子序列和的O(nlogn)算法
#include<iostream>
#include<ctime>
using namespace std;
//函数声明,因为要输出结果序列的始末位置,所以start和int采用引用传递
//首先定义了一个包裹函数,方便用户调用
int maxSum(int a[], int left, int right, int& start, int& end);
int maxSubsequenceSum(int a[], int size, int& start, int& end) {
return maxSum(a, 0, size - 1, start, end);
}
int main() {
time_t begin, end;//计时
double ret;
begin = clock();
int array[] = { 21,-15,23,-12,4,-23,18,-10,8,-19,26,-5,24,-29,24,25,-15,3,30,8,-15,8,-19,12,-4,-29,27,-4,8,21,-28,-13,7,-1,-26,29,9,11,-3,29,-13,23,-22,24,9,-8,-22,1,23,-27 };
int start, end1, size, maxArraySum;
size = sizeof(array) / 4;
maxArraySum = maxSubsequenceSum(array, size, start, end1);
cout << "最大连续子序列的和为" << maxArraySum << endl;
cout << "为从第" << start + 1 << "到" << end1 + 1 << "项的和" << endl;
end = clock();
ret = double(end - begin) / CLOCKS_PER_SEC;
cout << "runtime: " << ret << endl;
return 0;
}
//函数定义
int maxSum(int a[], int left, int right, int& start, int& end) {
int maxLeft, maxRight, center;//maxLeft和maxRight分别为左右半部最长子序列和
int leftSum = 0, rightSum = 0;//左右两部分的最大子序列和值
int maxLeftTmp = 0, maxRightTmp = 0;//情况三中,中点至左右的最大和值
int startL, startR, endL, endR;//左右两部分最大连续子序列的起点和终点
if (left == right) {//仅有一个元素递归终止
start = end = left;
return a[left] > 0 ? a[left] : 0;//?:语句——当元素大于0时,返回元素值,当元素小于0时,返回0
}
center = (left + right) / 2;
maxLeft = maxSum(a, left, center, startL, endL);//找前半部分的最大连续子序列
maxRight = maxSum(a, center + 1, right, startR, endR);//找后半部分的最大连续子序列
//找从前半部分开始从后半部分结束的最大连续子序列
start = center;
for (int i = center; i >= left; --i)
{
leftSum += a[i];
if (leftSum > maxLeftTmp) {
maxLeftTmp = leftSum;
start = i;
}
}
end = start + 1;
for (int i = center + 1; i <= right; ++i) {
rightSum += a[i];
if (rightSum > maxRightTmp) {
maxRightTmp = rightSum;
end = i;
}
}
//找三种情况的最大值
if (maxLeft > maxRight) {
if (maxLeft > maxLeftTmp + maxRightTmp) {
start = startL;
end = endL;
return maxLeft;
}
else return maxLeftTmp + maxRightTmp;
}
else {
if (maxRight > maxLeftTmp + maxRightTmp) {
start = startR;
end = endR;
return maxRight;
}
else return maxLeftTmp + maxRightTmp;
}
}
运行结果如下:
注:递归函数的时间复杂度计算较为复杂,之后再进行介绍
O ( n ) O(n) O(n)算法——排除不可能情况
通过分析排除许多不可能的子序列是进行算法复杂度降低的有效办法:
- 如果一个子序列的和是负的,则它不可能是最大连续子序列的开始部分,因为可以通过不包含它得到更大的连续子序列
- 所有与最大子序列毗邻的子序列一定有负的或者0和(否则会包含它们)
故当检测出一个负的子序列和时,不但可以直接从内层循环中跳出,还可以直接让 i i i增加到 j + 1 j+1 j+1。这样只需要对序列中的元素顺序检查一遍就可以了。
如{1,-3,4,-2,-1,6},当检测序列{1,-3}发现是负值后,则表示该子序列不可能直接包含在最大子序列中。 i i i可以直接从4开始检测。
代码如下:
//最大连续子序列和的O(n^3)算法
#include<iostream>
#include<ctime>
using namespace std;
int maxSubsequenceSum(int a[], int size, int& start, int& end);
//函数声明,因为要输出结果序列的始末位置,所以start和int采用引用传递
int main() {
time_t begin, end;//计时
double ret;
begin = clock();
int array[] = { 21,-15,23,-12,4,-23,18,-10,8,-19,26,-5,24,-29,24,25,-15,3,30,8,-15,8,-19,12,-4,-29,27,-4,8,21,-28,-13,7,-1,-26,29,9,11,-3,29,-13,23,-22,24,9,-8,-22,1,23,-27 };
int start, end1, size, maxArraySum;
size = sizeof(array) / 4;
maxArraySum = maxSubsequenceSum(array, size, start, end1);
cout << "最大连续子序列的和为" << maxArraySum << endl;
cout << "为从第" << start + 1 << "到" << end1 + 1 << "项的和" << endl;
end = clock();
ret = double(end - begin) / CLOCKS_PER_SEC;
cout << "runtime: " << ret << endl;
return 0;
}
//函数定义
int maxSubsequenceSum(int a[], int size, int& start, int& end) {
int maxSum, starttmp, thisSum;
start = end = maxSum = starttmp = thisSum = 0;
for (int j = 0; j < size; ++j)
{
thisSum += a[j];
if (thisSum < 0) {
thisSum = 0;
starttmp = j + 1;
}
else if (thisSum > maxSum) {
maxSum = thisSum;
start = starttmp;
end = j;
}
}
return maxSum;
}
运行结果如下:
代码分析:
牺牲了可读性换取时间,时间复杂度为
O
(
n
)
O(n)
O(n)