当前位置: 首页 > news >正文

最大子数组和

  • 问题描述
  • 枚举法
  • 分治法
  • 动态规划
  • 数据测试

问题描述

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

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

枚举法

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

  • 伪代码:
FUNCTION MaxSubArrayEx(nums) :// Input:整数数组 nums// Output:连续子数组的最大和
Beginn ← LENGTH(nums)Max ← nums[0] FOR i FROM 0 TO n - 1 :temp ← 0FOR j FROM i TO n - 1 :temp ← temp + nums[j]IF temp>MaxMax ← tempEnd IFEnd ForEnd ForRETURN 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] = rsumIF 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 sumEnd IF
End GetFUNCTION 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]为rsumif (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// 输出:连续子数组的最大和
Beginn ← 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 ForRETURN 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;elsecout << "Result Check: FAILED" << endl;}else {if (resDp == resDc)cout << "Result Check (DP vs DC): PASSED" << endl;elsecout << "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;
    会频繁触发内存分配和释放,比单纯使用整型变量慢很多。

相关文章:

  • 智能家居“心脏“升级战:GD25Q127CSIG国产芯片如何重构家庭物联生态
  • LeetCode:513、找树左下角的值
  • ngx_http_keyval_module动态键值管理
  • Windows DOS下的常用命令 及 HTML
  • HarmonyOS NEXT应用开发-Notification Kit(用户通知服务)notificationManager.getSlot
  • 安卓常用第三方库
  • 【Web/HarmonyOS】采用ArkTS+Web组件开发网页嵌套的全屏应用
  • 养生:通往健康生活的桥梁
  • 养生:开启健康生活的全新篇章
  • 文件上传总结
  • 基于卡尔曼滤波的传感器融合技术的多传感器融合技术(附战场环境模拟可视化代码及应用说明)
  • MATLAB机器人系统工具箱中的loadrobot和importrobot
  • QMK键盘固件中LED锁定指示灯的配置与使用详解(实操部分+拓展)
  • AI 搜索引擎 MindSearch
  • Xubuntu系统详解
  • Java设计模式之适配器模式:从入门到精通
  • 利用散点图探索宇航员特征与太空任务之间的关系
  • TCPIP详解 卷1协议 十 用户数据报协议和IP分片
  • Python语言在地球科学交叉领域中的应用——从数据可视化到常见数据分析方法的使用【实例操作】
  • 本地文件查重管理工具EasyFileCount v3.0.5.1绿色版,支持查找大重复文件+自动分类
  • 央行等印发《关于金融支持广州南沙深化面向世界的粤港澳全面合作的意见》
  • 法院就“行人相撞案”道歉:执法公正,普法莫拉开“距离”
  • “电竞+文旅”释放价值,王者全国大赛带火赛地五一游
  • “犍陀罗艺术与亚洲文明”在浙大对外展出
  • 王毅同巴基斯坦副总理兼外长达尔通电话
  • 2025上海十大动漫IP评选活动启动