最大子数组和

问题描述

给定一个有n(n>0)个整数的序列,要求其连续子数组,使得这子数组对应的元素和最大。
输入:整数序列{nums}。
输出:最大子数组和。

我们还可以求,m个连续不相交的子数组最大和,即最大m子段和

枚举法

枚举法是最容易想到的:
我们只需求的arr[i]+arr[i+1]+…+arr[j],0<=i<=j<=n的所有结果,并求出其中最大值。

  • 伪代码:
FUNCTION MaxSubArrayEx(nums) :
    // Input:整数数组 nums
    // Output:连续子数组的最大和
Begin
	n ← LENGTH(nums)
	Max ← nums[0] 
	FOR i FROM 0 TO n - 1 :
		temp ← 0
		FOR j FROM i TO n - 1 :
			temp ← temp + nums[j]
			IF temp>Max
				Max ← temp
			End IF
		End For
	End For
	RETURN maxSum
End MaxSubArrayEx
  • 代码实现:
int MaxSubArrayEx(vector<int>& nums) 
{
	int Max = nums[0];
	for (int i = 0; i < nums.size(); i++)
	{
		int temp = 0;
		for (int j = i; j < nums.size(); j++)
		{
			temp += nums[j];
			Max = max(temp, Max);
		}
	}
	return Max;
}

  • 复杂度分析:

显然我们需要求1+2+…+n=n(n+1)/2=O(n2).即时间复杂度为O(n2),空间复杂度为O(1).

分治法

首先很容易想到把数组从下标从[1,n]拆开为[1,m],[m+1,n]两个子数组,其中m=n/2。
然后分别求出两个子数组的最大子数组和,再合并答案。

但是合并过程中,我们发现原数组的最大子数组和对应的数组其下标有可能为[l,r],其中l<=m,r>=m+1.也就是说这个数组横穿了两个子数组,其中在左子数组的部分为[l,m]则这个子数组应当是左子数组以num[m-1]结尾的最大子数组和对应的子数组。

事实上,如果这个子数组并非左子数组以num[m-1]结尾的最大子数组和对应的子数组,那么存在子数组[l`,m]是以num[m-1]结尾的最大子数组和对应的子数组,也就是说[l`,m]对应数组元素的和大于[l,m]对应数组元素的和。那么[l`,r]对应数组元素的和大于[l,r]对应数组元素的和,该结论与[l,r]为原数组的最大子数组和对应的数组矛盾
因此[l,m] 是左子数组以num[m-1]结尾的最大子数组和对应的子数组。
同理,[m+1,r]是右子数组以nums[m]为起点的最大子数组和对应的子数组。

也就是说对于[l,r]这个子数组,我们需要目前维护三个变量lsum以l为起点的最大子数组和,rsum以r为起点的最大子数组和,以及msum即区间内的最大子数组和。

那么如何更新lsum和rsum呢?
看回原数组[1,n]其lsum对应的子数组为[1,x],x有两种情况:x<=m,那很显然[1,x]应当为左子数组的以nums[1]为起点的最大子数组和对应的子数组,证明同上;x>m,那就是[1,m]+[m+1,x].显然[m,x]应当为右子数组的以nums[m]为起点的最大子数组和对应的子数组,证明同上。
rsum的维护同理。那么我们就还需要维护一个变量isum即[l,r]的区间和。

注意到,l=r时,lsum=rsum=msum=isum=nums[l-1].

  • 伪代码:
FUNCTION Get(nums, l, r) :
Begin
	// 输入:数组 nums,区间 [l, r]
	// 输出:包含四个值的数组 [isum, lsum, msum, rsum]
	DECLARE sum[4]  // sum[0] = isum, sum[1] = lsum, sum[2] = msum, sum[3] = rsum
	IF l == r:
		sum[0] ← nums[l]
		sum[1] ← nums[l]
		sum[2] ← nums[l]
		sum[3] ← nums[l]
	ELSE :

		m ← l + ((r - l) / 2)
		L ← Get(nums, l, m)
		R ← Get(nums, m + 1, r)
		sum[0] ← L[0] + R[0]       
		sum[1] ← MAX(L[1], L[0] + R[1])  
		sum[2] ← MAX(L[2], R[2], L[3] + R[1]) 
		sum[3] ← MAX(R[3], R[0] + L[3]) 
		RETURN sum
	End IF
End Get

FUNCTION MaxSubArrayDc(nums) :
Begin
	// 输入:整数数组 nums
	// 输出:最大子数组和(分治法)
	result ← Get(nums, 0, LENGTH(nums) - 1)
	RETURN result[2]
End MaxSubArrayDc

  • 代码实现:
vector<int> Get(vector<int>& nums, int l, int r)
{
	vector<int>sum(4);
	//sum[0]为isum,sum[1]为lsum,sum[2]为msum,sum[3]为rsum
	if (l == r)
	{
		for (int i = 0; i < 4; i++)sum[i] = nums[l	];
	}
	else
	{
		int m = l + (r - l) / 2;
		vector<int>L = Get(nums, l, m);
		vector<int>R = Get(nums, m + 1, r);
		sum[0] = L[0] + R[0];
		sum[1] = max(L[1], L[0] + R[1]);
		sum[2] = max({ L[2],R[2],L[3] + R[1] });
		sum[3] = max(R[3], R[0] + L[3]);
	}
	return sum;
}
int MaxSubArrayDc(vector<int>& nums)
{
	return Get(nums, 0, nums.size()-1)[2];
}

  • 复杂度分析:

时间复杂度:将赋值操作看作基本操作,对于样本量n的基本操作次数f(n)有f(n)=2f(n/2)+4,f(1)=4.令n=2k,g(k)=f(n),则g(k)=2f(n/2)+4=2g(k-1)+4,将其展开可以得到g(k)=2kg(0)+4(1+2+4+…+2k-1)=8*2k-4=8n-4=f(n)=O(n).即时间复杂度为O(n).

空间复杂度:对于每一个函数Get会生成常数个变量,我们总共调用了2logn-1次Get,即n-1次.故空间复杂度为O(n).

动态规划

既然提到了动态规划法,我们首先就要想到能否通过这个问题的子问题来推导该问题的答案。也就是说能不能先求出n-1个整数的整数序列的最大子数组和,再推导出n个整数的整数序列的最大子数组和。
很显然这是行不通的,原因如下:
1)首先,n个整数的整数序列里面选出n-1个整数,有n种结果。
2)其次,这n种结果里面有n-2种子数组是不连续的,那么其最大子数组和就失去了意义,因为无法合并为n个整数的整数序列的最大子数组和。

但通过错误的尝试,我们发现了第一个关键点,就是子结构也就是n-1个整数的子数组应当在原数组中连续。那么我们考虑前n-1个整数的子数组也就是nums1,nums2,…,numsn-1.能否求得这个子数组的最大子数组和然后再和numsn合并为n个整数的序列的最大子数组和?
通过简单的分析后我们发现,这也是行不通的。因为我们不确保前n-1个整数序列的最大子数组和所对应的子数组是以numsn-1结尾的,那就不能简单的加上numsn来探讨。

那也就是说我们必须求出以numsn-1结尾的最大子数组和。
经过上述分析,我们终于得到了一种可行的状态表示。

dp[i]为以numsi结尾的最大子数组和。事实上这也是我们动态规划常用的经验状态表示。
那么对于以numsi+1结尾的最大子数组和对应的子数组有两种情况,要么就是只有他本身,要么就是numsi+1加上以numsi结尾的最大子数组和对应的子数组。
也就是说,状态转移方程为dp[i+1]=max{numsi+1,numsi+1+dp[i]}.初始化dp[1]=nums1。
那么我们所求的整数序列的最大子数组和就是max{dp}。注:下面代码中nums[i]=numsi+1.

  • 伪代码:
FUNCTION MaxSubArrayDp(nums) :
	// 输入:整数数组 nums
	// 输出:连续子数组的最大和
Begin
	n ← LENGTH(nums)
	DECLARE dp[1:n] 
	dp[1] ← nums[0] 
	maxSum ← dp[1]  

	FOR i FROM 2 TO n :
		currentElement ← nums[i - 1] 
		dp[i] ← MAX(currentElement, currentElement + dp[i - 1])
		maxSum ← MAX(maxSum, dp[i])
	End For
	RETURN maxSum
End MaxSubArrayDp

  • 代码实现:
int MaxSubArrayDp(vector<int>& nums)
{
	vector<int>dp(nums.size() + 1);
	dp[1] = nums[0];
	int Max = dp[1];
	for (int i = 2; i <= nums.size(); i++)
	{
		dp[i] = max(nums[i - 1], nums[i - 1] + dp[i - 1]);
		Max = max(dp[i], Max);
	}
	return Max;
}

  • 复杂度分析:

不难发现时间复杂度O(n),空间复杂度O(n).

数据测试

下面我们对数据量20,1000,10000的数据进行测试:

#include<iostream>
#include<vector>
#include<algorithm>
#include<climits>
#include<fstream>
#include<chrono>
#include<cstdlib>
using namespace std;
using namespace chrono;
vector<int> GenerateArray(int n, int minVal = -1000, int maxVal = 1000) {
	vector<int> nums(n);
	for (int i = 0; i < n; i++) {
		nums[i] = rand() % (maxVal - minVal + 1) + minVal;
	}
	return nums;
}

// 测试函数
void RunTest(int N, ofstream& csv_out) {
	cout << "\n===== N = " << N << " =====" << endl;
	vector<int> nums = GenerateArray(N);

	double timeEx = -1, timeDp = -1, timeDc = -1;

	if (N <= 1000) {
		auto start = high_resolution_clock::now();
		int resEx = MaxSubArrayEx(nums);
		auto end = high_resolution_clock::now();
		timeEx = duration<double, milli>(end - start).count();
		cout << "Exhaustive: Result = " << resEx << ", Time = " << timeEx << " ms" << endl;
	}

	auto start = high_resolution_clock::now();
	int resDp = MaxSubArrayDp(nums);
	auto end = high_resolution_clock::now();
	timeDp = duration<double, milli>(end - start).count();
	cout << "Dynamic Programming: Result = " << resDp << ", Time = " << timeDp << " ms" << endl;

	start = high_resolution_clock::now();
	int resDc = MaxSubArrayDc(nums);
	end = high_resolution_clock::now();
	timeDc = duration<double, milli>(end - start).count();
	cout << "Divide & Conquer: Result = " << resDc << ", Time = " << timeDc << " ms" << endl;

	// 校验一致性
	if (N <= 1000) {
		if (resDp == resDc && resDc == MaxSubArrayEx(nums))
			cout << "Result Check: PASSED" << endl;
		else
			cout << "Result Check: FAILED" << endl;
	}
	else {
		if (resDp == resDc)
			cout << "Result Check (DP vs DC): PASSED" << endl;
		else
			cout << "Result Check (DP vs DC): FAILED" << endl;
	}
}

int main() {
	srand(time(0));

	ofstream csv_out("time_results.csv");
	csv_out << "N,Exhaustive,DP,DivideConquer,MultiSegment\n";

	RunTest(20, csv_out);
	RunTest(1000, csv_out);
	RunTest(10000, csv_out);

	csv_out.close();
	return 0;
}

运行结果:
在这里插入图片描述
我们发现分治法和动态规划法的理论时间复杂度都是O(n),但我们的分治法明显比动态规划法慢得多,原因如下:

  1. 递归开销大(函数调用&栈帧)
    每个递归调用都要创建局部变量、保存上下文、返回值等;
    对于大型数据,如 N=10000,将递归调用近 2N次
    而动态规划只是一个简单的 for 循环,运行效率很高。
  2. 缺少尾递归优化
    C++ 没有强制优化尾递归;
    递归函数返回值是vector,需要不断地创建、拷贝,非常耗时。
  3. 合并操作创建了 vector(频繁分配内存)
    每一层都要创建一个新的 vector;
    会频繁触发内存分配和释放,比单纯使用整型变量慢很多。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值