从洛谷 P1115 出发:动态规划、前缀和与迭代,三种视角解构最大子段和

📅 2026/6/30 15:40:53
从洛谷 P1115 出发:动态规划、前缀和与迭代,三种视角解构最大子段和
1. 最大子段和问题从洛谷P1115说起第一次在洛谷刷到P1115这道题时我盯着最大子段和四个字发了半天呆。这不就是找数组里连续几个数加起来最大吗听起来简单但真正动手写代码时才发现里面藏着不少门道。后来在算法竞赛中多次遇到它的变种才意识到这是检验基础是否扎实的试金石。最大子段和问题的标准定义是给定一个整数数组找出一个连续子数组至少包含一个元素使得其元素和最大。比如数组[-2,1,-3,4,-1,2,1,-5,4]的最大子段和是6对应子数组[4,-1,2,1]。这个问题之所以重要是因为它不仅是动态规划的经典入门题还关联着前缀和、贪心算法等多个核心概念。2. 动态规划解法拆解问题的艺术2.1 状态定义的思考过程动态规划最难的就是状态定义。我最初的想法是定义dp[i][j]表示从i到j的子段和但这样时间复杂度直接飙到O(n²)。后来看到大佬的题解才恍然大悟——只需要关注以i结尾这个关键条件。正确的状态定义应该是dp[i]表示以第i个元素结尾的所有子段中的最大和。这种定义的精妙之处在于它将问题分解为了n个子问题数组有n个元素每个子问题只关注以特定位置结尾的情况。2.2 状态转移的两种可能对于每个元素a[i]只有两种选择自立门户自己单独成为一个子段此时和为a[i]抱大腿接在前一个子段后面此时和为dp[i-1] a[i]取这两种情况的最大值就是dp[i]的值。用代码表示就是dp[i] max(a[i], dp[i-1] a[i])我在第一次实现时犯了个错误——忘记比较a[i]和dp[i-1]a[i]直接写了dp[i]dp[i-1]a[i]。结果遇到全负数数组时就输出了错误答案。这个坑提醒我动态规划的状态转移一定要考虑所有可能性。2.3 完整实现与优化标准的动态规划实现需要O(n)空间存储dp数组。但观察状态转移方程会发现dp[i]只依赖于dp[i-1]所以可以优化到O(1)空间def max_subarray(nums): current_max global_max nums[0] for num in nums[1:]: current_max max(num, current_max num) global_max max(global_max, current_max) return global_max这个优化后的版本不仅空间效率高而且更符合直觉——就像是在遍历数组时不断记录当前的最大和。3. 前缀和解法数学思维的胜利3.1 从暴力枚举到前缀和优化最朴素的解法是枚举所有子段计算它们的和。这种方法时间复杂度是O(n³)三层循环显然不实用。前缀和技巧可以将计算子段和的时间降到O(1)。前缀和数组s的定义是s[i] a[1] a[2] ... a[i]。有了这个数组任何子段a[i..j]的和都可以表示为s[j] - s[i-1]。3.2 关键观察最小前缀和最大子段和问题转化为找到ij使得s[j]-s[i]最大。这等价于对于每个j找到ij使得s[i]最小。因此我们可以在遍历时维护一个最小前缀和min_prefix。实现时有个细节要注意min_prefix初始值应该设为0对应空前缀即子段从第一个元素开始的情况。我第一次实现时设为了无穷大结果漏掉了这种情况。3.3 前缀和解法的实现def max_subarray_prefix(nums): min_prefix prefix 0 max_sum float(-inf) for num in nums: prefix num max_sum max(max_sum, prefix - min_prefix) min_prefix min(min_prefix, prefix) return max_sum这个方法的时间复杂度是O(n)空间复杂度也是O(n)如果显式存储前缀和数组。不过和动态规划一样可以优化到O(1)空间因为只需要当前前缀和和最小前缀和。4. 迭代解法贪心思想的体现4.1 算法直觉何时重新开始迭代解法是最让我惊艳的。它的核心思想是如果当前子段的和已经为负那么它对后续子段只会产生负面影响应该舍弃。这就像是在玩股票当你持有的股票已经亏损和为负不如清仓重新开始如果还在盈利和为正就继续持有扩展当前子段。4.2 与动态规划的关系迭代解法实际上是动态规划的空间优化版本。current_sum对应dp[i]而max_sum记录全局最大值。不同的是迭代解法更直观地体现了贪心选择策略。4.3 边界条件的处理实现时需要注意全负数数组的情况。比如[-1,-2,-3]的最大子段和应该是-1。我的第一个版本在current_sum0时直接重置为0导致这个case出错。正确的做法是重置为当前元素值def max_subarray_iter(nums): current_sum max_sum nums[0] for num in nums[1:]: if current_sum 0: current_sum num else: current_sum num max_sum max(max_sum, current_sum) return max_sum5. 三种解法的对比与选择5.1 时间复杂度分析三种解法都是O(n)时间但常数因子不同。在我的测试中迭代解法通常最快因为它只需要一次遍历且操作最简单。5.2 空间复杂度对比动态规划和前缀和解法原始版本需要O(n)空间但都可以优化到O(1)。迭代解法天然就是O(1)空间。5.3 适用场景建议动态规划最容易理解和扩展适合需要记录完整dp数组的情况如需要找出具体子段前缀和适合需要频繁查询子段和的场景如多次查询不同区间的和迭代解法代码最简洁适合只需要知道最大和的情况在实际编程竞赛中我通常首选迭代解法因为它写起来快且不容易出错。但在教学时会从动态规划开始讲解因为它的思维过程更清晰。6. 实战中的常见陷阱6.1 全负数数组的处理这是最容易出错的case。很多初学者会默认最大和至少为0但在全负数时最大和应该是最大的那个负数。三种解法都需要特别注意这一点。6.2 空子段的考虑有些问题允许空子段和为0这时解法需要调整。比如迭代解法中current_sum可以从0开始max_sum初始值设为0。6.3 浮点数版本当数组包含浮点数时比较大小的方式需要更谨慎。特别是判断current_sum0时要考虑浮点精度问题。7. 问题变种与扩展最大子段和有很多有趣的变种我在比赛中遇到过这些二维最大子矩阵和可以看作是一维问题的扩展使用前缀和枚举的技巧环形数组的最大子段和分两种情况讨论或者将数组复制一份连接起来长度限制的最大子段和结合单调队列或二分查找理解基础问题的多种解法能帮助我们更好地应对这些变种。比如环形数组问题就可以用前缀和单调队列来解决。