原理
-
最大子段和问题定义
给定一个整数序列,要求找出该序列中具有最大和的连续子段(子数组)。例如,对于序列[-2, 1, -3, 4, -1, 2, 1, -5, 4]
,其最大子段和为6
,对应的子段是[4, -1, 2, 1]
。 -
动态规划思路及状态定义
- 状态定义:使用
dp[i]
表示以第i
个元素结尾的最大子段和。例如,dp[3]
就代表以序列中第 3 个元素结尾的连续子段能取得的最大和(这里序列索引从 0 开始计数)。 - 状态转移方程推导依据:对于每个元素
nums[i]
(i > 0
),有两种选择来构成以它结尾的最大子段和。一种是将当前元素nums[i]
加入到以nums[i - 1]
结尾的最大子段中(即dp[i - 1] + nums[i]
),另一种就是当前元素nums[i]
单独作为一个子段(即nums[i]
本身),取这两种情况中的较大值作为dp[i]
的值,也就是dp[i] = max(dp[i - 1] + nums[i], nums[i])
。因为要使得以nums[i]
结尾的子段和最大,要么接上前面的子段(前提是能使和更大),要么就自己单独成段(前面的子段和为负时,接上反而会使和变小)。 - 最终结果获取:在计算出所有以每个元素结尾的最大子段和
dp[i]
后,整个序列的最大子段和就是所有dp[i]
中的最大值,通过遍历dp
数组不断比较更新来找到这个最大值,存储在变量result
中并最终返回。
- 状态定义:使用
步骤
- 边界情况处理及初始化(函数开头部分)
- 首先判断输入的整数序列
nums
是否为空,如果为空(即nums.size() == 0
),按照题目要求直接返回0
,表示空序列的最大子段和为0
。 - 创建一个与输入序列
nums
长度相同的dp
向量(vector
),用于存储以每个元素结尾的最大子段和。然后将dp(nums.size()); dp[0]
初始化为nums[0]
,因为以第一个元素结尾的最大子段和就是这个元素本身,同时将result
也初始化为dp[0]
,此时result
暂时记录着当前找到的最大子段和(初始就是第一个元素的值)。
- 首先判断输入的整数序列
- 计算以每个元素结尾的最大子段和并更新最大子段和记录值(循环部分)
- 通过
for
循环从索引1
开始遍历整个输入序列nums
(for(int i = 1; i < nums.size(); i++)
),对于每个索引i
:- 根据状态转移方程
dp[i] = max(dp[i - 1] + nums[i], nums[i])
来计算以第i
个元素结尾的最大子段和,也就是比较将当前元素加入到前一个元素结尾的最大子段中后的和与当前元素单独作为子段的和,取较大值赋给dp[i]
。 - 接着比较当前计算得到的
dp[i]
和已记录的最大子段和result
的大小,如果dp[i] > result
,则更新result
的值为dp[i]
,确保result
始终记录着到当前元素为止所找到的最大子段和。
- 根据状态转移方程
- 通过
- 返回结果(函数末尾)
- 当循环结束,即遍历完整个输入序列后,
result
中存储的就是整个序列的最大子段和,将其作为函数的返回值返回,即return result;
。
- 当循环结束,即遍历完整个输入序列后,
图示法表示步骤(以序列 [-2, 1, -3, 4, -1, 2, 1, -5, 4]
为例)
- 初始化
- 输入序列
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
,长度为9
。 - 创建
dp
向量,dp[0] = nums[0] = -2
,同时result = dp[0] = -2
。
- 输入序列
- 计算
dp[1]
(以nums[1]
即元素1
结尾的最大子段和)- 根据状态转移方程,
dp[1] = max(dp[0] + nums[1], nums[1]) = max(-2 + 1, 1) = 1
,此时dp[1] = 1
。然后比较dp[1]
和result
的大小,因为1 > -2
,所以更新result = 1
。
- 根据状态转移方程,
- 计算
dp[2]
(以nums[2]
即元素-3
结尾的最大子段和)dp[2] = max(dp[1] + nums[2], nums[2]) = max(1 + (-3), -3) = -2
,dp[2] = -2
。再比较dp[2]
和result
的大小,-2 < 1
,result
保持为1
。
- 计算
dp[3]
(以nums[3]
即元素4
结尾的最大子段和)dp[3] = max(dp[2] + nums[3], nums[3]) = max(-2 + 4, 4) = 4
,dp[3] = 4
。比较后更新result = 4
,因为4 > 1
。
- 计算
dp[4]
(以nums[4]
即元素-1
结尾的最大子段和)dp[4] = max(dp[3] + nums[4], nums[4]) = max(4 + (-1), -1) = 3
,dp[4] = 3
。由于3 < 4
,result
不变。
- 计算
dp[5]
(以nums[5]
即元素2
结尾的最大子段和)dp[5] = max(dp[4] + nums[5], nums[5]) = max(3 + 2, 2) = 5
,dp[5] = 5
。更新result = 5
,因为5 > 4
。
- 计算
dp[6]
(以nums[6]
即元素1
结尾的最大子段和)dp[6] = max(dp[5] + nums[6], nums[6]) = max(5 + 1, 1) = 6
,dp[6] = 6
。更新result = 6
,因为6 > 5
。
- 计算
dp[7]
(以nums[7]
即元素-5
结尾的最大子段和)dp[7] = max(dp[6] + nums[7], nums[7]) = max(6 + (-5), -5) = 1
,dp[7] = 1
。由于1 < 6
,result
不变。
- 计算
dp[8]
(以nums[8]
即元素4
结尾的最大子段和)dp[8] = max(dp[7] + nums[8], nums[8]) = max(1 + 4, 4) = 5
,dp[8] = 5
。因为5 < 6
,result
保持为6
。
- 返回结果
- 循环结束,
result = 6
,返回6
,即为序列[-2, 1, -3, 4, -1, 2, 1, -5, 4]
的最大子段和。
- 循环结束,
代码程序以及代码关键行注释
- class Solution {
- public:
- int maxSubArray(vector<int>& nums) {
- if (!nums.size()) return 0; // 如果输入序列为空,返回0
-
- vector<int> dp(nums.size()); // 创建dp向量,用于存储以每个元素结尾的最大子段和
- dp[0] = nums[0]; // 初始化dp[0]为第一个元素,因为以第一个元素结尾的最大子段和就是它本身
- int result = dp[0]; // 初始化result为dp[0],用于记录当前找到的最大子段和
-
- for (int i = 1; i < nums.size(); i++) {
- dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 根据状态转移方程计算以第i个元素结尾的最大子段和
- if (dp[i] > result) result = dp[i]; // 如果当前计算的最大子段和大于已记录的result,更新result
- }
-
- return result; // 返回整个序列的最大子段和
- }
- };
完整代码程序
- #include <iostream>
- #include <vector>
- #include <algorithm>
-
- using namespace std;
-
- class Solution {
- public:
- int maxSubArray(vector<int>& nums) {
- if (!nums.size()) return 0;
- vector<int> dp(nums.size());
- dp[0] = nums[0];
- int result = dp[0];
- for (int i = 1; i < nums.size(); i++) {
- dp[i] = max(dp[i - 1] + nums[i], nums[i]);
- if (dp[i] > result) result = dp[i];
- }
- return result;
- }
- };
-
- int main() {
- vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; // 可自行修改输入序列
- Solution solution;
- int maxSum = solution.maxSubArray(nums);
- cout << "最大子段和为: " << maxSum << endl;
- return 0;
- }
时间复杂度分析
- 计算过程:
- 在
maxSubArray
函数中,通过一个for
循环遍历输入序列nums
,循环次数为n
(n
是nums
的长度,即序列元素个数),每次循环内部执行的操作主要是调用max
函数来比较两个值并进行赋值,以及可能的result
值更新操作,这些操作的时间复杂度都是常数级别,即O(1)
。
- 在
- 时间复杂度:
- 综合来看,整个算法的时间复杂度为 ,其中
n
为输入序列的长度。这意味着随着序列长度的增加,计算最大子段和所需的时间会呈线性增长,时间效率相对较高。
- 综合来看,整个算法的时间复杂度为 ,其中
- 影响因素及优化方向:
- 时间复杂度主要受输入序列长度
n
的影响,序列越长,遍历计算所需时间越长。对于该算法,其已经是解决最大子段和问题的较优时间复杂度解法之一,不过在一些特殊场景下,如果需要同时处理大量不同的序列,可能可以考虑对代码进行进一步的底层优化(比如利用一些特定的编译器优化选项来提升代码执行效率),或者结合并行计算等技术(如果硬件支持且问题适合并行处理)来加速计算过程,但这些优化方式通常需要更深入的技术知识以及对具体运行环境的考虑。
- 时间复杂度主要受输入序列长度
总结
- 算法特点及适用场景:
- 该算法基于动态规划思想,通过简洁合理地定义状态和状态转移方程,高效地解决了最大子段和问题,时间复杂度为 ,在处理较长序列时也能保持较好的时间效率。适用于各种需要在给定序列中寻找具有最大和的连续子段的场景,比如在数据分析中,分析股票价格走势序列以找到一段连续时间内收益最大的区间,或者分析传感器采集的数据序列中某段连续数据的最大累加值等情况,应用范围较广。
- 注意事项及可能遇到的问题:
- 在代码实现方面,要注意对输入序列为空的边界情况处理,确保程序的健壮性。同时,虽然算法本身逻辑相对简单,但在理解状态转移方程时要明确
dp[i]
的含义以及两种选择构成最大子段和的情况,避免在实现过程中出现逻辑错误导致结果不准确。另外,在实际应用中,如果输入序列的数据规模极大(例如海量数据场景),可能需要进一步考虑内存占用情况以及结合更高级的优化手段(如前面提到的并行计算等)来满足性能要求。
- 在代码实现方面,要注意对输入序列为空的边界情况处理,确保程序的健壮性。同时,虽然算法本身逻辑相对简单,但在理解状态转移方程时要明确
评论记录:
回复评论: