更新题目 689. Maximum Sum of 3 Non-Overlapping Subarrays
题目 lc689 · 修改(rev 2) · · 2026-10-26 23:11
body
@@ -1,5 +1,12 @@ ## 题目 -在数组里找三个互不重叠、长度都是 k 的子数组,使它们的和最大,返回三个起点(和相同时取字典序最小的)。 +给一个整数数组 `nums` 和一个长度 `k`,找三个互不重叠、长度都是 `k` 的连续子数组,让三段的和加起来最大,返回三段的起点下标(从 0 开始,从左到右)。和最大的选法不止一种时,返回字典序最小的那组起点,也就是先比第一段的起点,小的胜出,相同再比第二段,依此类推。 + +LeetCode 的约束:`1 <= nums.length <= 2 * 10^4`,`1 <= nums[i] < 2^16`,`1 <= k <= floor(nums.length / 3)`,所以三段一定放得下。 + +| 输入 | 输出 | 为什么 | +| --- | --- | --- | +| `nums = [1,2,1,2,6,7,5,1]`,`k = 2` | `[0, 3, 5]` | 三段是 `[1,2]`、`[2,6]`、`[7,5]`,和 3 + 8 + 12 = 23。`[1, 3, 5]` 也是 23,但起点 0 比 1 小 | +| `nums = [1,2,1,2,1,2,1,2,1]`,`k = 2` | `[0, 2, 4]` | 每个长度 2 的窗口和都是 3,任选三段都是 9,取最靠左的一组 | [LeetCode 原题](https://leetcode.com/problems/maximum-sum-of-3-non-overlapping-subarrays/) @@ -7,53 +14,131 @@ ## 面经里怎么考的 -Visa Staff OA(2026-05,楼主挂了):楼主用谐音写「类似琉芭韭」,读作 689,改动是「k 是三个不同参数」,也就是三个子数组的长度各不相同。同一场另一题是有向带权图上的 Dijkstra(没给更多细节)。 +Visa Staff OA(2026-05,楼主挂了):楼主用谐音写「类似琉芭韭」,读作 689,说改动是「k 是三个不同参数」,现有记录里关于改动只有这一句。这里理解成三个子数组的长度各自给定,记作 `k1`、`k2`、`k3`,从左到右依次是长 `k1`、`k2`、`k3` 的三段【推断】。同一场另一题是有向带权图上的 Dijkstra(没给更多细节)。 + +现有记录没有写三个参数具体管什么、三段的先后顺序是否固定、要返回什么、并列时怎么选(未核实)。顺序可以任选的话,对 3! = 6 种顺序各算一遍,取和最大的那组。 ## 思路 -先用滑动窗口算出每个起点 i 的窗口和 `w[i]`。固定中间那段的起点 j,左边那段要在 `[0, j − k]` 里选 `w` 最大的起点,右边那段在 `[j + k, end]` 里选最大的;这两个「到某处为止的最大值位置」可以预先从左、从右各扫一遍算好。最后枚举 j。 +先用滑动窗口(sliding window)算出每个起点 `i` 的窗口和 `w[i]`,也就是 `nums[i]` 到 `nums[i + k − 1]` 的和。窗口右移一格时加上新进来的数、减去出去的数,整张表 `O(n)` 算完。之后题目变成:在 `w` 里挑三个下标 `i < j < l`,要求相邻两个至少差 `k`(保证不重叠),让 `w[i] + w[j] + w[l]` 最大。 -三段长度不同(k1、k2、k3)时思路不变:`w1`、`w2`、`w3` 各算一份,左边用 `w1` 的前缀最大,右边用 `w3` 的后缀最大,中间枚举 `w2` 的起点,范围按各自的长度改。 +关键一步是固定中间那段的起点 `j`。`j` 定了以后,左段只能从 `[0, j − k]` 里选,右段只能从 `[j + k, n − k]` 里选,两边互不影响,所以左边取这个范围里 `w` 最大的起点,右边也取它的范围里最大的,加上 `w[j]` 就是以 `j` 为中段时的最优解。任何一组合法答案都有一个中段,所以把所有 `j` 都枚举一遍,最大的那个就是全局最优。 + +「`w[0..x]` 里最大值在哪」和「`w[x..]` 里最大值在哪」可以预先各扫一遍存成两个数组:`best_left[x]` 从左往右扫,`best_right[x]` 从右往左扫。这样每个 `j` 只要 `O(1)` 查两次表,总时间 `O(n)`。 + +> [!say] I precompute every window sum. For each choice of the middle window, the best left window and the best right window are independent, so I look them up in a prefix-argmax array and a suffix-argmax array. That makes the whole scan linear. + +### 并列时怎么保证字典序最小 + +三处都要在并列时留下更小的下标: + +- `best_left` 从左往右扫,遇到严格更大(`>`)才换,相等时留着先见到的、更靠左的那个。 +- `best_right` 从右往左扫,遇到大于或等于(`>=`)就换。从右往左走,后见到的反而更靠左,所以相等也要换。 +- 枚举 `j` 从小到大,总和严格更大(`>`)才更新答案,相等时留着更小的 `j`。 + +这三条合起来确实给出字典序最小,理由如下。设算法返回 `(i, j, l)`,另有一组和同样最大的 `(i', j', l')`。第三条保证 `j` 是所有最优解里最小的中段,所以 `j <= j'`。 + +- 假如 `i' < i`:`i' < i <= j − k`,`i'` 也在 `j` 的左段范围里。`best_left` 在并列时留更小的下标,选了 `i` 而没选更小的 `i'`,说明 `w[i'] < w[i]`。又因为 `i <= j − k <= j' − k`,把 `(i', j', l')` 的左段换成 `i` 仍然合法,和变大了,与它是最优解矛盾。所以 `i <= i'`。 +- `i < i'` 时 `(i, j, l)` 已经更小。`i = i'` 时看中段,`j < j'` 也已经更小。`j = j'` 时把同样的道理用在右段上:`l'` 在 `j` 的右段范围里,`best_right` 在并列时留更小的下标,得到 `l <= l'`。 + +所以 `(i, j, l)` 的字典序不大于任何一组最优解。 + +### 三段长度不同时 + +思路不变,只是三段各用自己的窗口和表:`w1`、`w2`、`w3` 分别是长度 `k1`、`k2`、`k3` 的窗口和。左段用 `w1` 的前缀最大,右段用 `w3` 的后缀最大,中段枚举 `w2` 的起点 `j`。范围要按各自的长度改: + +- 中段起点 `j` 从 `k1` 到 `n − k2 − k3`。左边要放得下长 `k1` 的一段,右边要放得下 `k2 + k3`。 +- 左段起点不超过 `j − k1`,查 `best_left[j − k1]`。 +- 右段起点不小于 `j + k2`,查 `best_right[j + k2]`。 + +在三个长度按从左到右的顺序给出的前提下,这样做是对的:上面「固定中段、两边各取最大」的理由和字典序的论证都只用到「左段在中段之前结束、右段在中段之后开始」,没有用到三段等长。原题就是 `k1 = k2 = k3 = k` 的特例,所以下面的代码只写一份通用版本,原题调用它。 + +> [!say] If the three windows have different lengths, I keep one window-sum array per length. The middle start ranges from k1 to n minus k2 minus k3, the left window must end before the middle starts, and the right window must start after the middle ends. ## 代码 ```python -def max_sum_of_three_subarrays(nums, k): - n = len(nums) - w = [0] * (n - k + 1) - s = sum(nums[:k]); w[0] = s - for i in range(1, n - k + 1): - s += nums[i + k - 1] - nums[i - 1] - w[i] = s - left, best = [0] * len(w), 0 - for i in range(len(w)): - if w[i] > w[best]: - best = i - left[i] = best - right, best = [0] * len(w), len(w) - 1 - for i in range(len(w) - 1, -1, -1): - if w[i] >= w[best]: # >= 让并列时取更小的下标 - best = i - right[i] = best - ans = None - for j in range(k, len(w) - k): - i, l = left[j - k], right[j + k] - total = w[i] + w[j] + w[l] - if ans is None or total > ans[0]: - ans = (total, [i, j, l]) - return ans[1] +from typing import List, Sequence, Tuple + + +def window_sums(nums: Sequence[int], length: int) -> List[int]: + """sums[i] = nums[i] + ... + nums[i + length - 1], for every valid start i.""" + total = sum(nums[:length]) + sums = [total] + for start in range(1, len(nums) - length + 1): + total += nums[start + length - 1] - nums[start - 1] + sums.append(total) + return sums + + +def prefix_argmax(values: List[int]) -> List[int]: + """best[i] = index of the max of values[0..i]; ties keep the smaller index.""" + best, best_idx = [], 0 + for i, v in enumerate(values): + if v > values[best_idx]: # strict: an equal value later does not replace + best_idx = i + best.append(best_idx) + return best + + +def suffix_argmax(values: List[int]) -> List[int]: + """best[i] = index of the max of values[i..]; ties keep the smaller index.""" + best, best_idx = [0] * len(values), len(values) - 1 + for i in range(len(values) - 1, -1, -1): + if values[i] >= values[best_idx]: # >=: an equal value further left wins + best_idx = i + best[i] = best_idx + return best + + +def max_sum_of_three_windows(nums: Sequence[int], + lengths: Tuple[int, int, int]) -> List[int]: + """Starts of three non-overlapping windows, left to right, with lengths + lengths[0], lengths[1], lengths[2], maximizing the total sum. + Ties go to the lexicographically smallest list of starts. + Raises ValueError if the windows cannot fit into nums. + """ + k1, k2, k3 = lengths + if min(lengths) < 1 or k1 + k2 + k3 > len(nums): + raise ValueError("three windows of these lengths do not fit") + left_sums = window_sums(nums, k1) + mid_sums = window_sums(nums, k2) + right_sums = window_sums(nums, k3) + best_left = prefix_argmax(left_sums) + best_right = suffix_argmax(right_sums) + + best_total, best_starts = None, [] + for mid in range(k1, len(nums) - k2 - k3 + 1): + left = best_left[mid - k1] # left window must end before mid + right = best_right[mid + k2] # right window must start after mid's end + total = left_sums[left] + mid_sums[mid] + right_sums[right] + if best_total is None or total > best_total: # strict: keep earliest mid + best_total, best_starts = total, [left, mid, right] + return best_starts + + +def max_sum_of_three_subarrays(nums: Sequence[int], k: int) -> List[int]: + """LeetCode 689: three windows of the same length k.""" + return max_sum_of_three_windows(nums, (k, k, k)) ``` -本机跑过官方例子。 +OA 的代码写法也计分,所以代码这样拆: + +- 窗口和、前缀最大、后缀最大各是一个小函数,名字说明它算什么,docstring 写清并列时留哪个下标。主函数只剩「固定中段、查两张表、比总和」,和思路一节的三步一一对应。 +- 通用版本接收三个长度,按从左到右的顺序。OA 的三个参数如果就是这样给的,可以直接调用它【推断】。 +- 放不下三段(某个长度小于 1,或者三段长度加起来超过 `n`)时抛 `ValueError`,不返回一个看起来像答案的列表。LeetCode 保证放得下,不会走到这里。题面要求无解时返回空列表或 `-1` 的话,把这一行换掉即可。 + +2026-10-26 用 Python 3.13.17 跑过上面这段代码: + +- 两个官方例子分别得 `[0, 3, 5]` 和 `[0, 2, 4]`。`[5, 4, 3]` 配 `k = 1`(三段刚好占满)得 `[0, 1, 2]`。长度 5 的数组配 `k = 2` 抛 `ValueError`。 +- 和暴力解对拍。暴力解枚举所有合法的三元组,按(和取负,起点列表)取最小。等长版本 3000 组随机输入(`k` 从 1 到 3,元素取 0 到 3,小值域故意制造大量并列),三段不等长版本 3000 组(三个长度各从 1 到 3,元素取 −3 到 3,含负数),结果全部一致。 +- `n = 2 * 10^4`、元素在 1 到 `2^16 − 1` 之间的随机数组,`k = 1000` 和 `k = 1` 各约 11 毫秒。 ## 复杂度 -O(n)。 +时间 `O(n)`:三张窗口和表、两张最大值位置表各扫一遍,枚举中段一遍,每个中段查表 `O(1)`。额外空间 `O(n)`,存这五张表。 ## 易错点 -- 字典序最小:左边用 `>`、右边用 `>=`。 -- 三段长度不同的变种里,中间段的枚举范围要按 k1、k2、k3 分别算。 - -## 英文讲法 - -> [!say] I precompute every window sum, then for each choice of the middle window I take the best window entirely to its left and the best entirely to its right, using prefix and suffix argmax arrays. +- **并列时的比较符号**。左边的前缀最大用 `>`、右边的后缀最大用 `>=`,理由见「并列时怎么保证字典序最小」。2026-10-26 在两个官方例子上并排跑过:左边误用 `>=` 时,例子一得 `[1, 3, 5]`(`w[0]` 和 `w[1]` 都是 3,留了靠后的 1),正确版本得 `[0, 3, 5]`。右边误用 `>` 时,例子二得 `[0, 2, 7]`,正确版本得 `[0, 2, 4]`。每个例子只能抓到其中一个错,两个都要跑。 +- **三段长度不同时的下标范围**。中段从 `k1` 枚举到 `n − k2 − k3`,左段查 `j − k1`,右段查 `j + k2`。从等长版本改过来时,容易有一处还留着同一个 `k`。写完拿小例子手动核对一遍边界,见 [[p:me/python-pitfalls]]「边界和差一」。 +- **放不下三段**。LeetCode 保证 `3k <= n`,所以只照 LeetCode 写的版本往往不检查。如果答案存成 `ans = None`、枚举中段时更新、最后 `return ans[1]`,在 `n < 3k` 的输入上一个中段都枚举不到,`ans` 停在 `None`,最后一行抛 `TypeError`(2026-10-26 实测,`n = 5`、`k = 2`)。OA 题面没写保证放得下时,像上面的代码一样在开头检查。
meta
@@ -1,3 +1,6 @@ { - "lc_slug": "maximum-sum-of-3-non-overlapping-subarrays" + "lc_slug": "maximum-sum-of-3-non-overlapping-subarrays", + "depth_guide": "2026-10-23", + "rewritten_by": "cloud:20261026-1620", + "rewritten_at": "2026-10-27" }