LeetCode 1477. 找两个和为目标值且不重叠的子数组
题目描述


题意分析
在正整数数组中选出两个互不重叠的连续子数组,它们各自的和都必须等于
target,使长度之和最小。两段可以相邻,但不能共享下标;找不到两段时返回-1。
解法:滑动窗口 + 前缀最短记录
核心思路
[!blue]
先考虑怎样找出和为目标值的候选段。所有元素都为正,右端扩展会增大窗口和,左端右移会减小窗口和。因此每加入一个右端元素,只要总和超过目标,就不断移除左端,直到不超过目标。若恰好相等,就得到以当前右端结尾的合法段。
固定右端时,左端右移会让和严格减小,所以至多存在一个等于目标的窗口。已经因和过大而移走的左端,也不必回退:以后右端加入更多正数,从那些旧左端开始的和只会更大。由此两个指针只前进,就能找到全部候选段;某个元素本身超过目标时,窗口会缩为空,再继续处理后续元素。
找到右侧候选
[left, right]后,不能随意取此前发现的最短段,因为它仍可能与当前段重叠。定义best[i]为完全落在前缀[0, i]内、和为目标值的最短子数组长度。当前右段只能与best[left - 1]配对,这个位置之前结束的任何左段都与当前段没有公共下标。用
bestSoFar维护扫描到当前右端时已发现的最短合法段长度。命中窗口时,先用此前已经保存的best[left - 1]更新两段总长度,再用当前长度更新bestSoFar;不论是否命中,每轮都把它写入best[right],让所有历史前缀都有准确记录。不存在合法段时用INF标记,配对前先检查它,避免把不存在的左段算进去。任意一组可行答案都能分成左段和右段。扫描到它的右段终点时,该右段一定会被找到,而
best[left - 1]记录的左段不会比这组答案使用的左段更长。因此枚举全部右段并取最小值,就不会漏掉全局最优组合。
解题步骤
- 初始化空窗口,把当前前缀最短长度
bestSoFar和总答案设为INF。- 向右加入元素,总和超过目标时不断收缩左端。
- 总和等于目标时,若
left > 0且左侧存在合法段,用当前长度加best[left - 1]更新答案。- 用当前合法段更新
bestSoFar,每轮都写入best[right]。- 扫描完仍没有两段组合就返回
-1,否则返回最小总长度。
代码实现
class Solution {
public int minSumOfLengths(int[] arr, int target) {
int n = arr.length;
// 不可达记录只作哨兵,配对前需确认左侧候选存在。
final int INF = Integer.MAX_VALUE / 2;
// best[i] 表示 arr[0..i] 内和为 target 的最短子数组长度。
int[] best = new int[n];
int bestSoFar = INF;
int ans = INF;
int sum = 0;
int left = 0;
for (int right = 0; right < n; right++) {
sum += arr[right];
while (sum > target) {
// 元素全为正,收缩左边界一定使窗口和减小。
sum -= arr[left];
left++;
}
if (sum == target) {
int len = right - left + 1;
if (left > 0 && best[left - 1] != INF) {
// 读 best[left-1] 这个历史快照,左段必然结束于窗口开始之前。
ans = Math.min(ans, len + best[left - 1]);
}
// 当前长度只供后续位置配对使用。
bestSoFar = Math.min(bestSoFar, len);
}
best[right] = bestSoFar;
}
return ans == INF ? -1 : ans;
}
}
func minSumOfLengths(arr []int, target int) int {
n := len(arr)
// 不可达记录只作哨兵,配对前需确认左侧候选存在。
const inf = 1 << 29
// best[i] 表示 arr[0..i] 内和为 target 的最短子数组长度。
best := make([]int, n)
bestSoFar := inf
ans := inf
sum := 0
left := 0
for right := 0; right < n; right++ {
sum += arr[right]
for sum > target {
// 元素全为正,收缩左边界一定使窗口和减小。
sum -= arr[left]
left++
}
if sum == target {
length := right - left + 1
if left > 0 && best[left-1] != inf && length+best[left-1] < ans {
// 读 best[left-1] 这个历史快照,左段必然结束于窗口开始之前。
ans = length + best[left-1]
}
// 当前长度只供后续位置配对使用。
if length < bestSoFar {
bestSoFar = length
}
}
best[right] = bestSoFar
}
if ans == inf {
return -1
}
return ans
}
复杂度分析
- 时间复杂度:$O(n)$,两个指针都最多经过数组一次,前缀最短值和答案每轮只需常数次更新。
- 空间复杂度:$O(n)$,保存每个历史前缀的最短合法段长度。
关键点总结
[!green]
- 元素为正,保证窗口和的单调性与固定右端下候选的唯一性。
best[i]是整个前缀中的最短段,不只统计恰好在i结束的段。- 用
best[left - 1]配对,将不重叠条件直接写进查询范围。
易错点总结
[!yellow]
- 用全局
bestSoFar直接配对,可能选到与当前窗口重叠的段。left = 0时没有左侧空间,不能访问负下标。- 只在命中时写
best[right],会丢掉中间前缀继承的最优记录。- 第一次找到两段就返回,可能错过后面更短的组合。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 1031. 两个无重叠子数组的最大和 | 中等 | 原题两段长度固定并最大化和,本题两段和固定并最小化总长度,需维护前缀最短合格段。 |
| 560. 和为 K 的子数组 | 中等 | 目标和区间可由前缀关系定位,本题还要确保两个区间不重叠,不能直接选全局最短两段。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!