题干
求给定序列的最大连续和
给定一个整数序列
A
1
,
A
2
,
.
.
.
,
A
n
A_1, A_2, ..., A_n
A1,A2,...,An, 求一个子序列
A
i
,
A
i
+
1
,
.
.
.
,
A
j
−
1
,
A
j
A_i, A_{i+1}, ..., A_{j-1}, A_j
Ai,Ai+1,...,Aj−1,Aj, 使得其和最大.
解答
首先可以确定, 这题用数组就可以解决, 既然可以用数组, 那就可以用二分法等比较高效的算法实现.
暴力法
暴力法就不多介绍了, 外循环遍历数组A, 内循环遍历子序列, 最内循环计算子序列的和. 计算的和的次数为 T ( n ) = ∑ i = 1 n ∑ j = i n j − i + 1 = n ( n + 1 ) ( n + 2 ) 6 T(n) = \sum_{i=1}^n\sum_{j=i}^n j-i+1 = \frac{n(n+1)(n+2)}{6} T(n)=i=1∑nj=i∑nj−i+1=6n(n+1)(n+2) 时间复杂度 O ( n ) = n 3 O(n)=n^3 O(n)=n3
int maxSum = A[1]; // 注意①
for(int i=1; i<n; i++){
for(int j=i; j<n; j++){ //取连续子序列
int sumTemp = 0;
for(int k=i; k<=j; k++){ //累加元素和
sumTemp += A[k];
}
if(sumTemp > maxSum){
maxSum = sumTemp; //更新最大值
}
}
}
需要注意的是, ①处初始化的时候最保险的做法是将其初始化为A[1], 不能简单的初始化为0, 1等任何整数. 因为如果初始化为0, 那么对于序列-1, -2, -3来说, 最后求得最大连续和仍然为0, 显然是错误的. 这么一想理论上初始化为A[1], A[2], A[n]都是可以的.
分治法
分治法的思想为
将问题划分成若干个子问题
递归求解子问题
合并子问题的解得到原问题的解
在这, 我们可以将序列 A 1 , A 2 , . . . , A n A_{1},A_{2}, ..., A_{n} A1,A2,...,An不断进行二分, 递归求解完全位于左半子序列或者右半子序列的最佳序列, 最后求出起点位于左半序列、终点位于右半序列的最大连续和序列, 并和子问题的最优解进行比较, 得到原问题的解. 时间复杂度为 O ( n l o g 2 n ) O(nlog_2{n}) O(nlog2n)
分治法实现如下
//返回序列A在区间[x, y]中的最大连续和, 这里 y >= x
int maxSumFunc(int* A, int x, int y){
// 序列在[x, y]区间上只有一个元素, 直接返回
if(y==x){
return A[x]; // 注意①
}else{
// 分治法第一步, 划分成[x, m]和[m+1, y]
int middle = (x+y)/2;
// 分治法第二步, 递归求解, 其中max()函数是c++头文件algorithm中定义的, 默认返回两个参数中较大的一个
int maxTemp = max(maxSumFunc(A, x, middle), maxSumFunc(A, middle+1, y));
int sumTemp=0;
//分治法第三步, 合并(1), 从分界点开始往左的最大连续和maxSumLeft;
int maxSumLeft = A[middle];
for(int i=middle; i>=x; i--){
maxSumLeft = max(maxSumLeft, sumTemp+=A[i]);
}
//分治法第三步, 合并(2), 从分界点开始往右的最大连续和maxSumRight;
int maxSumRight = A[middle+1];
sumTemp=0;
for(int i=middle+1; i<=y; i++){
maxSumRight = max(maxSumRight, sumTemp+=A[i]);
}
//把子问题的解与 maxSumLeft+maxSumRight比较
return max(maxTemp, maxSumLeft+maxSumRight);
}
}
注意点:
注意①处: 不能写成return max(0, A[x]), 可能会有人以为如果当前元素小于0, 不应该将其加入最佳序列中, 这是错误的, 首先这样做会导致求解过程中的子序列断断续续, 也许求出来的值会更大, 但已经不是题目要求的连续子序列. 其次最佳序列中是有可能包含负数的, 比如2, -1, 3, -5, 4, 和最大的连续子序列为2, -1, 3, 包含负数-1.
完整代码
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <time.h>
#include <windows.h>
using namespace std;
/*
求最大连续和
*/
const int n = 256;
int getRandomInt(){
return rand()%21 - 10; //生成[-10, 10]之间的随机数
}
void init(int* array){ // 初始化序列array
array[0] = 0; // array[0]在这题中是多余的
srand((unsigned)time(NULL)); //使用系统时间作为随机种子, 种子只需要给一次,如果这句写在for循环或者getRandomInt()中, 则每次返回的随机数都是同样的
for(int i=1; i<n; i++){
array[i] = getRandomInt();
}
return;
}
void display(int* array){ //输出序列array
printf("序列A为:\n");
for(int i=1; i<n; i++){
printf("%d ", array[i]);
}
printf("\n");
return;
}
//返回序列A在区间[x, y]中的最大连续和, 这里 y >= x
int maxSumFunc(int* A, int x, int y){
// 序列在[x, y]区间上只有一个元素, 直接返回
if(y==x){
return A[x]; // 注意①
}else{
// 分治法第一步, 划分成[x, m]和[m+1, y]
int middle = (x+y)/2;
// 分治法第二步, 递归求解, 其中max()函数是c++头文件algorithm中定义的, 默认返回两个参数中较大的一个
int maxTemp = max(maxSumFunc(A, x, middle), maxSumFunc(A, middle+1, y));
int sumTemp=0;
//分治法第三步, 合并(1), 从分界点开始往左的最大连续和maxSumLeft;
int maxSumLeft = A[middle];
for(int i=middle; i>=x; i--){
maxSumLeft = max(maxSumLeft, sumTemp+=A[i]);
}
//分治法第三步, 合并(2), 从分界点开始往右的最大连续和maxSumRight;
int maxSumRight = A[middle+1];
sumTemp=0;
for(int i=middle+1; i<=y; i++){
maxSumRight = max(maxSumRight, sumTemp+=A[i]);
}
//把子问题的解与 maxSumLeft+maxSumRight比较
return max(maxTemp, maxSumLeft+maxSumRight);
}
}
// 暴力法破解
void voiSolution(int* A){
_LARGE_INTEGER start; //开始时间
_LARGE_INTEGER over; //结束时间
LARGE_INTEGER f; //计时器频率
QueryPerformanceFrequency(&f);
double dqFreq=(double)f.QuadPart;
QueryPerformanceCounter(&start); //计时开始
int maxSum = A[1];
for(int i=1; i<n; i++){
for(int j=i; j<n; j++){ //取连续子序列
int sumTemp = 0;
for(int k=i; k<=j; k++){ //累加元素和
sumTemp += A[k];
}
if(sumTemp > maxSum){
maxSum = sumTemp; //更新最大值
}
}
}
QueryPerformanceCounter(&over); //计时结束
double runtime=1000000*(over.QuadPart-start.QuadPart)/dqFreq;//乘以1000000把单位由秒化为微秒,精度为1000 000/(cpu主频)微秒
printf("暴力发破解最大和为%d, 运行时间%fus \n", maxSum, runtime);
return;
}
// 分治法破解
void divSolution(int* A){
_LARGE_INTEGER start; //开始时间
_LARGE_INTEGER over; //结束时间
LARGE_INTEGER f; //计时器频率
QueryPerformanceFrequency(&f);
double dqFreq=(double)f.QuadPart;
QueryPerformanceCounter(&start); //计时开始
int maxSum = maxSumFunc(A, 1, n-1); //求得A序列[1, n-1]上的最大连续和
QueryPerformanceCounter(&over); //计时结束
double runtime=1000000*(over.QuadPart-start.QuadPart)/dqFreq;//乘以1000000把单位由秒化为微秒,精度为1000 000/(cpu主频)微秒
printf("分治法破解最大和为%d ,运行时间%fus \n", maxSum, runtime);
return;
}
int main() {
int A[n];
init(A);
display(A);
voiSolution(A);
divSolution(A);
return 0;
}
运行结果
当n不太大时, 暴力法与分治法的时间效率差的不多, 都在us和ms级别, 但是当n=2049时, 暴力法需要几百万us,也就是几秒, 然而分治法只需要几百us。当n=65537时, 暴力法的时间已经达到了两分钟, 而分治法依然在us级别.