两个无重叠子数组的最大和 图解题解
这道题到底在问什么
- 输入
- nums=[6,7,1,9,2,3,1], firstLen=1, secondLen=2
- 输出
- 22
最优解:为什么这么做
一句话答案:LeetCode 1031 两个无重叠子数组的最大和:前缀和让定长窗口和一减就得,再扫两趟分别处理短段在前、长段在前,每趟边扫边维护左侧最优前段,时间 O(n)、空间 O(n)。
两个无重叠子数组的最大和在挑什么
给一个整数数组 nums 和两个长度 firstLen、secondLen,要找两段连续、互不重叠的子数组(子数组就是数组里挨着的一截),长度分别是这两个值,让两段元素和最大。题面 nums=[6,7,1,9,2,3,1]、firstLen=1、secondLen=2,答案 22。长度 firstLen 的段既能在另一段左边,也能在右边。
为什么逐对枚举两段位置会慢到跑不动
两段各自的起点都有大约 n 个,两两配对再查重叠就是 n² 量级,每对还要现算两段和,整体奔着 O(n³)(大 O 记号描述规模变大时操作数怎么涨)去,n 一大就跑不动。慢在重复:同一段的和被反复从头累加,同一种前后配置被换着枚举多遍。
前缀和凭什么让定长窗口和一减就得
最好省的是「反复算一段的和」。先建前缀和数组 s(前缀和就是从头累加到每个位置的总和):s[0]=0 当哨兵、代表前 0 个数的和,s[k] 就是前 k 个数的总和。有了它,第 l 到 r 这段和就是 s[r+1]-s[l],一次减法 O(1) 拿到。框住第 0 到第 1 格,和是 s[2]-s[0]=13-0=13,正好是 6 加 7。
一段固定另一段取左侧最优,两种顺序为何都要扫
配对不漏。先钉死一种顺序:长度 firstLen 的段在 secondLen 段左边。从左往右滑右段,滑到每处,左边可放的 firstLen 段里只有和最大的才可能最优——用滚动变量 t(一路扫、只记左侧至今最大的段和)盯住。右段每滑一步,先把新空出、能当左段的位置并入 t 取最大,再用 t 加当前右段和刷新答案,一趟就覆盖了「firstLen 段在前」的所有配对。
但 firstLen 段也可能在后。角色对调再扫第二趟:secondLen 段在左、t 改记左侧最优的 secondLen 段和,firstLen 段在右滑。两趟共用 ans 取最大,两种顺序就都兜住。
拿题面数组把两趟扫描亲手走一遍
先滚出 s=[0,6,13,14,23,25,28,29]。第一趟 firstLen=1 段在前、secondLen=2 段右滑:[7,1] 和 8、t=6 合 14;[1,9] 和 10、t=7 合 17;[9,2] 和 11、t=7 合 18;[2,3] 时 t 升到 9 但只合 14;[3,1] 合 13——第一趟最好 18。第二趟对调:右段到 [9] 时左侧最优的长度 2 段是 [6,7]、t=13 合 22,其余都不过 22。取大得 22,来自第二趟——长度 2 段 [6,7] 反在前。
只扫一趟为什么会把答案 22 算成 18
只扫第一趟就返回会把答案定在 18:那趟默认长度 1 的段永远在前,可最优解偏是长度 2 的 [6,7] 在前、长度 1 的 [9] 在后,得第二趟才扫到。复杂度上,前缀和一遍、两趟各扫一遍都是线性,时间 O(n);额外空间主要是那条长度 n+1 的前缀和数组,空间 O(n)。两个边界:每趟开头必须把 t 清零,否则第二趟会把上一趟的旧最优错当本趟左段;两段长度之和等于数组长度时,答案就是整个数组的和。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3把整套思路压成两句:前缀和让窗口和「一减就得」;扫的时候一个窗口固定、另一个取左侧历史最优 t。两种前后顺序都要扫一趟。下面每帧都在套它。
- 4先建前缀和 s,从一个哨兵 s0 = 0 开始,它代表「前 0 个数的和」。有了这个起点,后面每个 s 都能在上一个的基础上加一个数得到。
- 5把第 0 个数 6 累进来:s1 等于上一项 s0 的 0 再加 6,得 6。前缀和就是这样一路滚出来的。
- 6把第 1 个数 7 累进来:s2 等于上一项 s1 的 6 再加 7,得 13。前缀和就是这样一路滚出来的。
- 7把第 2 个数 1 累进来:s3 等于上一项 s2 的 13 再加 1,得 14。前缀和就是这样一路滚出来的。
- 8把第 3 个数 9 累进来:s4 等于上一项 s3 的 14 再加 9,得 23。前缀和就是这样一路滚出来的。
- 9把第 4 个数 2 累进来:s5 等于上一项 s4 的 23 再加 2,得 25。前缀和就是这样一路滚出来的。
- 10把第 5 个数 3 累进来:s6 等于上一项 s5 的 25 再加 3,得 28。前缀和就是这样一路滚出来的。
- 11把第 6 个数 1 累进来:s7 等于上一项 s6 的 28 再加 1,得 29。前缀和就是这样一路滚出来的。
- 12验证一下这个减法:框住第 0 到第 1 格这段,它的和就是 s2 减 s0,等于 13 减 0,正好 13,也就是 6 加 7。以后任何定长窗口和都这么一减得到,O(1)。
- 13先扫第一趟,约定:长度 1 的窗口落在长度 2 的窗口「前面」。我们让第二个窗口从左往右扫,第一个窗口就在它左边取历史最优,用滚动变量 t 记着。t 和这一趟的答案 ansA 都从 0 起。
- 14第二窗滑到 [1,2],这两个数的和是 8。它左边能放的最好第一窗(绿色)和是 t = 6,两段相加 14。比之前更大,ansA 刷新成 14。(这一步顺手把第一窗最优 t 更新到了 6。)
- 15第二窗滑到 [2,3],这两个数的和是 10。它左边能放的最好第一窗(绿色)和是 t = 7,两段相加 17。比之前更大,ansA 刷新成 17。(这一步顺手把第一窗最优 t 更新到了 7。)
- 16第二窗滑到 [3,4],这两个数的和是 11。它左边能放的最好第一窗(绿色)和是 t = 7,两段相加 18。比之前更大,ansA 刷新成 18。
- 17第二窗滑到 [4,5],这两个数的和是 5。它左边能放的最好第一窗(绿色)和是 t = 9,两段相加 14。没超过当前 ansA = 18,先记着继续。(这一步顺手把第一窗最优 t 更新到了 9。)
- 18第二窗滑到 [5,6],这两个数的和是 4。它左边能放的最好第一窗(绿色)和是 t = 9,两段相加 13。没超过当前 ansA = 18,先记着继续。
- 19再扫第二趟,把顺序反过来:这回长度 2 的窗口落在「前面」,长度 1 的窗口在它右边扫。角色对调:t 现在记「左边最好的长度 2 窗口和」,t 和 ansB 重新从 0 起。
- 20第一窗滑到 [2,2],和是 1。它左边最好的长度 2 窗口(绿色)和是 t = 13,两段相加 14。刷新 ansB 到 14。(这一步把第二窗最优 t 更新到了 13。)
- 21第一窗滑到 [3,3],和是 9。它左边最好的长度 2 窗口(绿色)和是 t = 13,两段相加 22。刷新 ansB 到 22。
- 22第一窗滑到 [4,4],和是 2。它左边最好的长度 2 窗口(绿色)和是 t = 13,两段相加 15。没超过 ansB = 22。
- 23第一窗滑到 [5,5],和是 3。它左边最好的长度 2 窗口(绿色)和是 t = 13,两段相加 16。没超过 ansB = 22。
- 24第一窗滑到 [6,6],和是 1。它左边最好的长度 2 窗口(绿色)和是 t = 13,两段相加 14。没超过 ansB = 22。
- 25回放最优解:绿色是长度 2 的窗口 [6,7],和 13,落在左边;右边方框是长度 1 的窗口 [9],和 9。两段相加 22。注意它来自第二趟,长度 2 的窗口反而在前。这正是为什么两种顺序都得扫:只扫第一趟只会拿到 18。
⚠️ 容易写错的地方
✗ 错:只扫一种顺序
✓ 对:正反两趟都扫,最后取 max
两段谁前谁后都合法,漏一种就可能错过最优(本例只扫第一趟只得 18)
✗ 错:窗口和每次重新累加
✓ 对:前缀和两端相减 O(1) 取窗口和
重复累加会退化成 O(n²),前缀和把它压回 O(n)
✗ 错:前缀和下标错位
✓ 对:窗口 [l,r] 的和 = s[r+1] − s[l]
写成 s[r] − s[l] 会少算右端那个数
✗ 错:两趟之间忘了重置 t
✓ 对:每趟开头 t 归 0
上一趟的最优窗口和留着,会污染这一趟的判断
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxSumTwoNoOverlap(self, nums: List[int], firstLen: int, secondLen: int) -> int:
n = len(nums)
s = list(accumulate(nums, initial=0))
ans = t = 0
i = firstLen
while i + secondLen - 1 < n:
t = max(t, s[i] - s[i - firstLen])
ans = max(ans, t + s[i + secondLen] - s[i])
i += 1
t = 0
i = secondLen
while i + firstLen - 1 < n:
t = max(t, s[i] - s[i - secondLen])
ans = max(ans, t + s[i + firstLen] - s[i])
i += 1
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxSumTwoNoOverlap(vector<int>& nums, int firstLen, int secondLen) {
int n = nums.size();
vector<int> s(n + 1);
for (int i = 0; i < n; ++i) {
s[i + 1] = s[i] + nums[i];
}
int ans = 0;
for (int i = firstLen, t = 0; i + secondLen - 1 < n; ++i) {
t = max(t, s[i] - s[i - firstLen]);
ans = max(ans, t + s[i + secondLen] - s[i]);
}
for (int i = secondLen, t = 0; i + firstLen - 1 < n; ++i) {
t = max(t, s[i] - s[i - secondLen]);
ans = max(ans, t + s[i + firstLen] - s[i]);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxSumTwoNoOverlap(int[] nums, int firstLen, int secondLen) {
int n = nums.length;
int[] s = new int[n + 1];
for (int i = 0; i < n; ++i) {
s[i + 1] = s[i] + nums[i];
}
int ans = 0;
for (int i = firstLen, t = 0; i + secondLen - 1 < n; ++i) {
t = Math.max(t, s[i] - s[i - firstLen]);
ans = Math.max(ans, t + s[i + secondLen] - s[i]);
}
for (int i = secondLen, t = 0; i + firstLen - 1 < n; ++i) {
t = Math.max(t, s[i] - s[i - secondLen]);
ans = Math.max(ans, t + s[i + firstLen] - s[i]);
}
return ans;
}
}复杂度
时间
O(n)
建前缀和一遍 + 两趟各扫一遍,都是线性
空间
O(n)
前缀和数组 s 占 n+1 个位置;滚动变量 t、ans 是常数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 两个无重叠子数组的最大和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
两趟扫描能不能合并成一趟?+
不能直接合。一趟只能固定一种前后顺序(比如让长度 firstLen 的段恒在左),当 firstLen 与 secondLen 不等时,最优解可能落在另一种顺序上(题面就是长度 2 的段在前),单趟扫不到。只有 firstLen 等于 secondLen 时两种顺序完全对称,一趟才够。所以一般写两趟、共享同一个 ans 取最大最稳妥。
既然是定长窗口,为什么不直接滑动窗口边加边减,还要建前缀和?+
可以。定长窗口右移时,和的变化就是「加进右边新元素、减掉左边旧元素」,用滚动量也能 O(1) 维护,本质和前缀和等价。前缀和的好处是任意区间和随取随用、下标对齐直观(s[r+1]-s[l]);本题要在两趟里反复取不同长度、不同位置的窗口和,用前缀和写起来不容易错。两种写法复杂度都是 O(n)。
每一步为什么先更新 t、再用 t 去刷新答案,顺序反了会怎样?+
t 代表「当前右段左侧、能合法放下的最优前段和」。右段滑到新位置时,左边恰好又多空出一个能当前段的位置,得先把它并入 t,t 才是完整的左侧最优;再用 t 加当前右段和刷新答案,两段才紧挨着不重叠。若先算答案再更新 t,就漏掉了紧贴右段左边那个前段候选,会少算一种配对。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 两个无重叠子数组的最大和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。