LeetCode 面试题 17.18. 最短超串
题目描述

题意分析
在
big中找到包含small全部元素的最短连续子数组,目标元素在窗口中的先后顺序不受限制。small非空且元素互不相同,每种只需至少出现一次;返回包含两端的下标[left, right],等长时选左端点更小的区间,无解返回空数组。
解法:滑动窗口维护有效区间
核心思路
[!blue]
固定左端点时,向右扩展窗口只会增加元素,已经满足的覆盖需求不会丢失;固定右端点时,向右移动左端点会让窗口更短,直到某种目标缺失。这种单调变化适合用两个只向前移动的指针维护窗口
[left, right]。
need记录目标元素及需求次数一,window只统计这些目标在当前窗口中的次数。match表示已经满足的目标种类数,不是目标元素的总出现次数:某种目标从零次变成一次时,match才加一,多出的副本不会继续增加它。于是match == need.size()等价于窗口覆盖完整。右端每加入一个元素就更新对应计数。窗口一旦完整,先记录当前区间,再移除左端元素并推进
left;如果移除的是非目标,或这个目标仍有副本,窗口依然完整,可以继续缩短。只有某种目标次数从一变为零时,match才减一,此时停止收缩,等待右端再次补足它。左端移走后不需要回退:每个被移走的左端点,都已经在当前右端点下形成过合法区间并被记录。未来用同一个左端点,只会得到更长的区间,不可能改进最短答案。对仍保留的左端点,右端继续扩展直到首次可行,再尽量收缩,所以不会漏掉更短解。
最佳答案只在长度严格缩短时更新。右端按递增顺序处理,等长窗口的左端也随右端递增,先遇到的就是起点更早的那个;不在等长时覆盖,便满足题目的并列要求。若始终没有完整窗口,
bestL保持为负,返回空数组。
解题步骤
- 为
small中的每个目标建立需求一,初始化空窗口、match = 0和未找到答案的标记。- 右端逐项扩展,只对目标元素更新频次,并在首次达标时增加
match。- 只要窗口完整,先比较区间长度,再移除左端元素,必要时减少
match。- 覆盖缺失后停止收缩,继续扩展右端;最终返回记录的两端下标或空数组。
代码实现
// 用哈希表统计窗口内每个目标元素出现次数,并维护已满足的目标数 match。
class Solution {
public int[] shortestSeq(int[] big, int[] small) {
Map<Integer, Integer> need = new HashMap<>();
for (int num : small) {
need.put(num, 1);
}
Map<Integer, Integer> window = new HashMap<>();
int match = 0;
int needSize = need.size();
int left = 0;
int bestL = -1;
int bestR = -1;
int bestLen = Integer.MAX_VALUE;
for (int right = 0; right < big.length; right++) {
int val = big[right];
if (need.containsKey(val)) {
int cnt = window.getOrDefault(val, 0) + 1;
window.put(val, cnt);
// 频次第一次达到要求才增加达标种类,多余副本不重复计数。
if (cnt == need.get(val)) {
match++;
}
}
while (match == needSize && left <= right) {
// 移出左端前先记录合法窗口,只在严格缩短时覆盖答案。
if (right - left + 1 < bestLen) {
bestLen = right - left + 1;
bestL = left;
bestR = right;
}
int drop = big[left];
if (need.containsKey(drop)) {
int cnt = window.get(drop) - 1;
window.put(drop, cnt);
// 移出后仍有足够副本时,这类目标依然达标。
if (cnt < need.get(drop)) {
match--;
}
}
left++;
}
}
if (bestL == -1) {
return new int[0];
}
return new int[] {
bestL,
bestR,
};
}
}
// 用哈希表统计窗口内每个目标元素出现次数,并维护已满足的目标数 match。
func shortestSeq(big []int, small []int) []int {
need := map[int]int{}
for _, num := range small {
need[num] = 1
}
window := map[int]int{}
match := 0
needSize := len(need)
left := 0
bestL := -1
bestR := -1
bestLen := int(^uint(0) >> 1)
for right, val := range big {
if _, ok := need[val]; ok {
window[val]++
// 频次第一次达到要求才增加达标种类,多余副本不重复计数。
if window[val] == need[val] {
match++
}
}
for match == needSize && left <= right {
// 移出左端前先记录合法窗口,只在严格缩短时覆盖答案。
if right-left+1 < bestLen {
bestLen = right - left + 1
bestL = left
bestR = right
}
drop := big[left]
if _, ok := need[drop]; ok {
window[drop]--
// 移出后仍有足够副本时,这类目标依然达标。
if window[drop] < need[drop] {
match--
}
}
left++
}
}
if bestL == -1 {
return []int{}
}
return []int{
bestL,
bestR,
}
}
复杂度分析
- 时间复杂度:期望 $O(n+m)$,其中
n、m分别为big、small的长度。建表处理m个元素,两个指针各最多经过big一次,哈希操作期望为常数时间。- 空间复杂度:$O(m)$,需求表和窗口表只保存目标元素。
关键点总结
[!green]
match按目标种类是否达标变化,多余副本不重复计数。- 完整时收缩,不完整时扩展;移走的左端点无需重新考虑。
- 长度使用
right - left + 1,只在严格缩短时覆盖答案。
易错点总结
[!yellow]
- 每加入或移出一个目标都修改
match,会把多余副本误当作新的满足或新的缺失。- 必须在移除左端前记录当前合法窗口,移除后它可能已经不再完整。
- 只收缩一次就扩展右端,可能保留不必要的前缀,漏掉当前能取得的更短区间。
- 用小于等于更新最佳长度,会覆盖起点更早的等长结果。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 76. 最小覆盖子串 | 困难 | 同样寻找最短覆盖区间,本题small元素互异,只需每种出现一次;原题目标字符串可能含重复需求。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!