3 条题解
-
0
解释kadane算法
当然,很乐意为你详细解释Kadane 算法。这是一种非常巧妙、高效的算法,专门用来解决最大子段和 (Maximum Subarray Sum) 问题。
我们可以从三个层面来理解它:直观的比喻、算法的步骤 和 正确性的证明。
1. 直观的比喻:一场“接力赛”
想象一下,数组中的每个数字代表赛道上一段路程的收益(正数)或成本(负数)。你要组织一支接力队,从起点跑到终点,目标是找到一段连续的路程,使得跑完这段路的总收益最大。
-
current_max(当前选手的成绩):- 一个选手从某个起点开始跑。他手里的“成绩”就是他跑过的路段的收益总和。
- 每跑过一段
a[i],他的成绩就更新为成绩 + a[i]。
-
global_max(历史最佳成绩):- 这是记录板,记下所有选手跑出的历史最佳成绩。
- 每当一个选手跑完一段路,我们都把他的当前成绩和记录板上的最佳成绩比较一下,更新记录板。
-
核心决策:何时“交接棒”?
- 一个选手跑着跑着,如果发现自己的累计成绩变成了负数,这意味着他跑的这段路是“亏本”的。
- 关键点:如果下一个选手从他这里接棒,就相当于背负了他的负资产!这显然不划算。
- 所以,明智的策略是:一旦某个选手的累计成绩变为负数,就让他立刻放弃,下一个选手从全新的起点(当前位置的下一段)开始跑,成绩从 0 重新计算。
Kadane 算法的本质,就是模拟这场接力赛。
current_max就是当前选手的成绩,global_max就是记录板。当current_max变为负数时,就相当于交接棒,current_max被重置。
2. 算法的步骤
Kadane 算法非常简洁,只需要两个变量和一次遍历。
输入:一个整数数组
A,长度为n。 输出:A的最大子段和。变量:
global_max(全局最大和):用来存储最终结果。初始化为数组的第一个元素A[0]或一个极小的数(比如负无穷)。current_max(当前子段和):用来累加当前正在考虑的连续子段的和。初始化为0或A[0]。
流程:
// 初始化 global_max = A[0] current_max = A[0] // 从第二个元素开始遍历 for i from 1 to n-1: // 决策:是继续累加,还是从当前元素 a[i] 重新开始? // 这等价于 DP 方程:dp[i] = max(a[i], dp[i-1] + a[i]) current_max = max(A[i], current_max + A[i]) // 更新全局最大值 global_max = max(global_max, current_max) // 返回结果 return global_max另一种等价的贪心写法(更符合“接力赛”比喻):
// 初始化 global_max = -infinity current_max = 0 // 遍历所有元素 for x in A: // 累加当前元素 current_max = current_max + x // 更新全局最大值 if current_max > global_max: global_max = current_max // 决策:如果当前累加和变成了负数,就放弃它 if current_max < 0: current_max = 0 // 特殊情况处理:如果数组全是负数 // 上述循环会得到 0,但正确答案是最大的那个负数。 // 所以需要在循环结束后或初始化时处理。 // 一个简单的处理方法是 global_max 初始化为 A[0],然后从 A[1] 开始循环。 // 最健壮的写法是上面第一种,因为它天然处理了全负数的情况。以数组
[-2, 1, -3, 4, -1, 2, 1, -5, 4]为例,走一遍流程 (第一种写法):iA[i]current_max + A[i]current_max = max(A[i], ...)global_max = max(global_max, ...)0 -2 (init) -2 (init) 1 -2 + 1 = -1 1 (因为 1 > -1) max(-2, 1) = 1 2 -3 1 + (-3) = -2 -2 (因为 -2 > -3) max(1, -2) = 1 3 4 -2 + 4 = 2 4 (因为 4 > 2) max(1, 4) = 4 4 -1 4 + (-1) = 3 3 (因为 3 > -1) max(4, 3) = 4 5 2 3 + 2 = 5 5 (因为 5 > 2) max(4, 5) = 5 6 1 5 + 1 = 6 6 (因为 6 > 1) max(5, 6) = 6 7 -5 6 + (-5) = 1 1 (因为 1 > -5) max(6, 1) = 6 8 4 1 + 4 = 5 5 (因为 5 > 4) max(6, 5) = 6 最终
global_max是 6。
3. 正确性的证明(为什么这个贪心是对的?)
我们可以用反证法来理解。
假设 Kadane 算法找到的最大子段和是
S,它对应的子数组是A[i...j]。但假设存在一个真正的最大子段和S',它对应的子数组是A[p...q],并且S' > S。-
q和j的关系:- Kadane 算法在遍历到
q时,会计算出以q结尾的最大子段和,记为current_max_q。 global_max变量会记录下遍历到q时所见过的所有current_max的最大值。- 真正的以
q结尾的最大子段和就是S'本身 (A[p...q])。 - Kadane 算法的
current_max_q一定等于S'。为什么?因为current_max_q是通过max(A[q], current_max_{q-1} + A[q])递推得到的。如果A[p...q-1]的和是正的,那么current_max_{q-1}就会包含这部分和,current_max_q也就等于A[p...q]的和。如果A[p...q-1]的和是负的,那么真正的最大子段和就不可能是从p开始,而应该是从q之后的某个位置开始,与假设矛盾。 - 既然
current_max_q = S',那么global_max在q这一步之后,至少会是S'。这与我们假设的S' > S(S是最终的global_max) 矛盾。
- Kadane 算法在遍历到
-
核心思想:
- 算法的关键在于维护了这样一个循环不变式 (Loop Invariant):在遍历到第
i个元素时,current_max变量中存储的是以A[i]结尾的最大子段和。 - 全局最大子段和,必然是所有“以
A[i]结尾的最大子段和”中的那个最大值。 - 因此,我们只需要在每次计算完
current_max后,用它来更新global_max,就能保证在遍历结束后,global_max就是我们要求的最终答案。
- 算法的关键在于维护了这样一个循环不变式 (Loop Invariant):在遍历到第
总结:Kadane 算法通过一次遍历,巧妙地利用了“如果一个子段的和为负,它对后面的任何子段都只会产生负贡献”这一贪心性质,从而实现了 时间复杂度和 空间复杂度的求解。
-
-
0
你好!很高兴你对这个问题进行了深入的思考。用双指针的思想来理解【最大子段和】的 解法,是一个非常深刻且能直击算法本质的视角。
我们平常学习的最大子段和解法,通常是两种:
- DP 解法:
dp[i] = max(a[i], dp[i-1] + a[i]) - 贪心解法:维护一个
current_sum,如果current_sum变成负数,就把它重置为 0。
这两种解法都非常简洁高效,但有时会让人觉得有点“神奇”,不够直观。现在,我们用“动态调整起点的双指针”这个模型来重新剖析它,你会发现,它完美地解释了上述两种解法的内在逻辑。
1. 暴力双指针: 的起点
首先,我们来思考这个问题的最暴力、最直观的解法。 一个连续子段由起点
l和终点r决定。我们可以枚举所有的起点l和终点r。// O(N^3) 暴力 long long max_sum = -INF; for (int l = 0; l < n; l++) { for (int r = l; r < n; r++) { long long current_sum = 0; for (int k = l; k <= r; k++) { current_sum += a[k]; } max_sum = max(max_sum, current_sum); } }用前缀和可以优化到 ,但这仍然不够快。问题出在:我们固定了
l,然后移动r,这种方式无法利用之前计算的信息来优化l的选择。
2. 解法的双指针视角
现在,我们换一个思路。我们只用一个指针
r从左到右扫描,作为子段的终点。对于每一个固定的终点r,我们都希望能快速找到一个最佳的起点l( ),使得sum(l, r)最大。核心问题:当
r向右移动到r+1时,最佳起点l会如何变化?让我们在草稿纸上推演。 数组
a:[-2, 1, -3, 4, -1, 2, 1, -5, 4]-
r = 0(a = -2)- 终点是 0。最佳起点
l只能是 0。 - 最大和是
sum(0, 0) = -2。
- 终点是 0。最佳起点
-
r = 1(a = 1)- 终点是 1。起点
l可以是 0 或 1。 sum(0, 1) = -2 + 1 = -1sum(1, 1) = 1- 最佳起点是
l=1,最大和是1。
- 终点是 1。起点
-
r = 2(a = -3)- 终点是 2。起点
l可以是 0, 1, 2。 sum(0, 2) = -2 + 1 - 3 = -4sum(1, 2) = 1 - 3 = -2sum(2, 2) = -3- 最佳起点是
l=1,最大和是-2。
- 终点是 2。起点
等等!我们好像发现了什么规律!
当我们计算以
r为终点的最大和时,sum(l, r) = sum(l, r-1) + a[r]。 为了让sum(l, r)最大,我们其实就是在找一个l,让sum(l, r-1)最大。 这不就是上一步r-1的结果吗?并不完全是。因为
sum(l, r-1)是以r-1结尾的最大和,而我们需要的l是一个全局最佳起点。真正的突破口在这里:
让我们定义
prefix_sum[i]为a[0...i]的和。 那么sum(l, r) = prefix_sum[r] - prefix_sum[l-1]。对于固定的终点
r,要最大化sum(l, r),就等价于最小化prefix_sum[l-1],其中l-1 < r。所以,当指针
r从左到右扫描时,我们只需要在r的左边,实时维护一个到目前为止的最小前缀和min_prefix_sum。算法流程:
- 初始化
max_so_far = -INF(全局最大和)。 - 初始化
min_prefix_sum = 0(代表一个空前缀,即l=0时prefix_sum[-1]的情况)。 - 初始化
current_prefix_sum = 0。 - 用指针
r从0遍历到n-1: a.current_prefix_sum += a[r]。 b. 以r为终点的最大子段和就是current_prefix_sum - min_prefix_sum。 c. 用这个值更新max_so_far。 d. 用current_prefix_sum更新min_prefix_sum。
这个算法就是 Kadane 算法的“前缀和”版本!
3. 连接到经典的贪心/DP解法
现在,我们把上面的“前缀和”版本稍微变形一下。
令
current_max_ending_at_r表示以r结尾的最大子段和。current_max_ending_at_r = current_prefix_sum - min_prefix_sum_before_r那么,
current_max_ending_at_r+1是什么呢?current_max_ending_at_r+1 = current_prefix_sum_at_r+1 - min_prefix_sum_before_r+1= (current_prefix_sum_at_r + a[r+1]) - min(min_prefix_sum_before_r, current_prefix_sum_at_r)这个公式太复杂了。我们回到更直观的定义。
令
current_sum表示以当前r结尾的最大子段和。当r移动到r+1时:- 新的“以
r+1结尾的最大子段和”有两种可能:- 自立门户:就只包含
a[r+1]自己。 - 加入组织:在“以
r结尾的最大子段和”后面,再接上a[r+1]。
- 自立门户:就只包含
- 我们取这两者中较大的那个:
new_current_sum = max(a[r+1], current_sum + a[r+1])。
这,就是DP方程
dp[i] = max(a[i], dp[i-1] + a[i])的完美解释!那贪心解法呢?
current_sum = max(a[i], current_sum + a[i])如果current_sum + a[i]比a[i]自己还小,说明什么?说明current_sum一定是负数! 所以,这个max判断,等价于:current_sum += a[i]if (current_sum < a[i]) { current_sum = a[i] }这又等价于我们贪心解法里的:current_sum += a[i]- 如果之前的和是负的,
current_sum就被重置了。
回到双指针的视角:
- 右指针
r:就是我们循环的i,它一直在前进,探索新的终点。 - 左指针
l:它在哪里?它隐藏在current_sum这个变量里!- 当
current_sum保持为正并累加时,意味着最佳起点l保持不变。 - 当
current_sum因为累加了一个负数而即将变为负数时,我们发现current_sum + a[i]<a[i],于是我们执行current_sum = a[i]。 - 这个操作,在双指针的视角下,就相当于我们放弃了旧的起点
l,把新的最佳起点l移动到了i!
- 当
总结
- 右指针
r:固定,从左到右扫描,作为区间的终点。 - 左指针
l:动态调整。它指向当前最优子段的起点。- 只要以
r结尾的累加和current_sum还是正的,就说明从l到r这个子段是有“正贡献”的,值得保留。所以l不动。 - 一旦
current_sum变成了负数,就说明从l到r这段已经成了“负资产”,对后面的累加只会起到拖累作用。我们必须果断地**“断臂求生”**,把这段负资产扔掉,将新的起点l移到r+1的位置,从一个新的、干净的状态开始。
- 只要以
这个“当累加和为负时就重置起点”的策略,就是最大子段和 解法的精髓,也是 Kadane 算法的核心思想。它完美地体现了双指针技巧中“根据单调性(这里是和的正负)来移动指针以维护最优解”的理念。
- DP 解法:
- 1
信息
- ID
- 4894
- 时间
- 1000ms
- 内存
- 125MiB
- 难度
- 3
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者