算法笔记一 求序列的最大连续和

算法笔记一 求序列的最大连续和

题干

求给定序列的最大连续和
给定一个整数序列 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,...,Aj1,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=1nj=inji+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级别.

©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页