【算法】删除子数组的最大得分 & 最多 K 个重复元素的最长子数组------不定长滑动窗口与哈希频率约束
-
- [1695. 删除子数组的最大得分](#1695. 删除子数组的最大得分)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 题目示例](#2. 题目示例)
- [3. 算法思路](#3. 算法思路)
- [4. 核心代码](#4. 核心代码)
- [5. 示例测试(总代码)](#5. 示例测试(总代码))
- [2958. 最多 K 个重复元素的最长子数组](#2958. 最多 K 个重复元素的最长子数组)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 题目示例](#2. 题目示例)
- [3. 算法思路](#3. 算法思路)
- [4. 核心代码](#4. 核心代码)
- [5. 示例测试(总代码)](#5. 示例测试(总代码))
- 总结

🎬 博主名称: 超级苦力怕
🔥 个人专栏: 《LeetCode 题解》
🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!
本篇文章讲解的是 LeetCode 第 1695 题------删除子数组的最大得分 和 第 2958 题------最多 K 个重复元素的最长子数组 。两道题看似目标不同------一个求最大元素和,一个求最长子数组长度------但本质上考察的都是 不定长滑动窗口 + HashMap 频率约束 的核心模式:右指针不断扩展窗口并更新哈希表,左指针在频率超限时收缩,动态维护窗口内每个元素的出现次数 ≤ 上限,从而在 O(n) 时间内得出答案。
1695 的约束是"窗口内所有元素唯一"(频率 ≤ 1),目标是最大化窗口元素之和;2958 将约束泛化为"每个元素频率 ≤ k",目标是最大化窗口长度。两道题共用同一套滑动窗口模板,仅约束条件和统计目标不同。
本文将使用 Java 进行讲解,从暴力枚举出发,逐步过渡到滑动窗口 + HashMap,帮助你掌握不定长滑窗中最常见的一类模式------频率约束下的最长/最优合法子数组。
1695. 删除子数组的最大得分
1. 题目介绍
1695. 删除子数组的最大得分
直达链接:LeetCode 1695
给定一个正整数数组 nums,你需要删除一个 包含唯一元素 的子数组。删除子数组的得分就是子数组中各元素之 和。
请你返回 只删除一个 子数组可获得的 最大得分 。换言之,你需要从 nums 中选出一个元素互不相同的连续子数组,使得其元素之和最大。

提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^4
2. 题目示例
示例 1:
输入:nums = [4,2,4,5,6]
输出:17
解释:最优子数组是 [2,4,5,6],元素之和为 2 + 4 + 5 + 6 = 17。
示例 2:
输入:nums = [5,2,1,2,5,2,1,2,5]
输出:8
解释:最优子数组是 [5,2,1] 或 [1,2,5],元素之和为 8。
3. 算法思路
本题的核心约束是 窗口内元素必须互不相同。如果窗口里出现了重复元素,就说明当前窗口非法,需要收缩左边界,直到重复的那个元素被移出窗口为止。
暴力枚举法(超时)
枚举所有子数组的起止位置 [l, r],用哈希表判断是否包含重复元素,若是则更新最大和。时间复杂度 O(n²),在 n = 10^5 时不可行。
滑动窗口 + HashMap
滑动窗口是这道题的最优解法。用一个哈希表 freq 记录窗口内每个元素的出现次数,右指针 right 每步先加入新元素,若该元素出现次数 > 1(即窗口内出现重复),则不断右移左指针 left、将移出元素从 freq 中减掉,直到重复消失为止。窗口合法的每一个时刻,都尝试用当前窗口元素之和更新答案。
步骤拆解:
- 初始化
left = 0,sum = 0,maxSum = 0,HashMap<Integer, Integer> freq right从0遍历到n-1:sum += nums[right],freq[nums[right]]++- 若
freq[nums[right]] > 1:循环执行sum -= nums[left],freq[nums[left]]--,left++,直到该元素频率降回 1 maxSum = max(maxSum, sum)
- 返回
maxSum
| 操作 | 时间复杂度 |
|---|---|
| 右指针遍历 | O(n),每个元素入窗一次 |
| 左指针收缩 | O(n),每个元素出窗一次 |
| 总体 | O(n) |
4. 核心代码
java
class Solution {
public int maximumUniqueSubarray(int[] nums) {
int n = nums.length;
int left = 0;
int sum = 0;
int maxSum = 0;
HashMap<Integer, Integer> freq = new HashMap<>();
for (int right = 0; right < n; right++) {
// 1. 右指针入窗
sum += nums[right];
freq.put(nums[right], freq.getOrDefault(nums[right], 0) + 1);
// 2. 若出现重复,收缩左边界直到重复消失
while (freq.get(nums[right]) > 1) {
sum -= nums[left];
freq.put(nums[left], freq.get(nums[left]) - 1);
left++;
}
// 3. 此时窗口内元素互不相同,更新最大得分
maxSum = Math.max(maxSum, sum);
}
return maxSum;
}
}
5. 示例测试(总代码)
java
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
// 示例1测试
int[] nums1 = {4, 2, 4, 5, 6};
System.out.println("示例1输出:" + sol.maximumUniqueSubarray(nums1)); // 预期输出 17
// 示例2测试
int[] nums2 = {5, 2, 1, 2, 5, 2, 1, 2, 5};
System.out.println("示例2输出:" + sol.maximumUniqueSubarray(nums2)); // 预期输出 8
}
}
2958. 最多 K 个重复元素的最长子数组
1. 题目介绍
2958. 最多 K 个重复元素的最长子数组
直达链接:LeetCode 2958
给定一个整数数组 nums 和一个非负整数 k。一个子数组 合法 当且仅当其中 每个元素 的出现次数都 不超过 k。
返回最长合法子数组的长度。

提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^91 <= k <= nums.length
2. 题目示例
示例 1:
输入:nums = [1,2,3,1,2,3,1,2], k = 2
输出:6
解释:最长合法子数组是 [1,2,3,1,2,3],其中 1、2、3 各出现恰好 2 次。
示例 2:
输入:nums = [1,2,1,2,1,2,1,2], k = 1
输出:2
解释:k = 1 时合法子数组中所有元素必须互不相同,最长的是 [1,2](长度 2)。
示例 3:
输入:nums = [5,5,5,5,5,5,5], k = 4
输出:4
解释:最长合法子数组是 [5,5,5,5](长度 4),其中 5 出现 4 次 ≤ k。
3. 算法思路
这道题是 1695 的泛化版本:1695 要求元素频率 ≤ 1,而本题将上限泛化为 k。核心模式完全一致------右扩左缩,哈希表维护窗口内频率。
为什么不能预先固定窗口大小?
因为不同元素的出现位置是任意的,合法窗口的结束位置取决于各个元素的分布,无法提前预判窗口应该有多长。因此必须使用 不定长滑动窗口 :右指针每次扩展一步,一旦有新元素频率超过 k,就移动左指针直到该元素频率降回 k。窗口合法的每一步都更新最大长度。
步骤拆解:
- 初始化
left = 0,maxLen = 0,HashMap<Integer, Integer> freq right从0遍历到n-1:freq[nums[right]]++- 若
freq[nums[right]] > k:循环执行freq[nums[left]]--,left++,直到该元素频率降回 k maxLen = max(maxLen, right - left + 1)
- 返回
maxLen
关键细节: 收缩条件只检查 nums[right] 对应的频率。因为只有新加入元素才可能突破频率上限------窗口在加入 nums[right] 之前是合法的,之前的元素频率最多恰好是 k,不会超过 k。这个性质保证了 while 循环的收缩目标单一且高效。
4. 核心代码
java
class Solution {
public int maxSubarrayLength(int[] nums, int k) {
int n = nums.length;
int left = 0;
int maxLen = 0;
HashMap<Integer, Integer> freq = new HashMap<>();
for (int right = 0; right < n; right++) {
// 1. 右指针入窗
freq.put(nums[right], freq.getOrDefault(nums[right], 0) + 1);
// 2. 若新元素频率超 k,收缩左边界直到合规
while (freq.get(nums[right]) > k) {
freq.put(nums[left], freq.get(nums[left]) - 1);
left++;
}
// 3. 此时窗口内所有元素频率 ≤ k,更新最大长度
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
5. 示例测试(总代码)
java
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
// 示例1测试
int[] nums1 = {1, 2, 3, 1, 2, 3, 1, 2};
System.out.println("示例1输出:" + sol.maxSubarrayLength(nums1, 2)); // 预期输出 6
// 示例2测试
int[] nums2 = {1, 2, 1, 2, 1, 2, 1, 2};
System.out.println("示例2输出:" + sol.maxSubarrayLength(nums2, 1)); // 预期输出 2
// 示例3测试
int[] nums3 = {5, 5, 5, 5, 5, 5, 5};
System.out.println("示例3输出:" + sol.maxSubarrayLength(nums3, 4)); // 预期输出 4
}
}
总结
| 题号 | 题名 | 约束 | 优化目标 | 窗口收缩条件 |
|---|---|---|---|---|
| 1695 | 删除子数组的最大得分 | 元素频率 ≤ 1 | 最大元素和 | freq[nums[right]] > 1 |
| 2958 | 最多 K 个重复元素的最长子数组 | 元素频率 ≤ k | 最长长度 | freq[nums[right]] > k |
两道题的代码结构几乎完全相同,差异仅在于两点:
- 统计目标不同 :1695 累加
sum并取maxSum;2958 计算窗口长度right - left + 1并取maxLen - 约束阈值不同 :1695 是 2958 在
k = 1时的特例
💡 核心结论: 不定长滑动窗口 + HashMap 频率约束是处理"元素出现次数有限制"类子数组问题的通用模板。右指针负责扩展并更新频次,左指针在频次超限时收缩。每个元素最多入窗一次、出窗一次,总时间复杂度 O(n)。
💡 扩展方向: 将这套模板记熟后,遇到同类变体------如"窗口内最多包含 k 种不同元素""恰好包含 k 种不同元素"------只需调整 freq 的检查条件和目标统计方式即可。
