LeetCode 2461 & 1423:定长滑动窗口变体------判重与互补转化
-
- [2461. 长度为 K 子数组中的最大和](#2461. 长度为 K 子数组中的最大和)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 解题思路](#2. 解题思路)
-
- 解法一:暴力枚举
- [解法二:滑动窗口 + 哈希表(推荐)](#解法二:滑动窗口 + 哈希表(推荐))
- [1423. 可获得的最大点数](#1423. 可获得的最大点数)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 解题思路](#2. 解题思路)
- [3. 示例代码](#3. 示例代码)
-
- [3.1 2461 解法一:暴力枚举](#3.1 2461 解法一:暴力枚举)
- [3.2 2461 解法二:滑动窗口 + 哈希表](#3.2 2461 解法二:滑动窗口 + 哈希表)
- [3.3 1423 解法一:前缀和枚举](#3.3 1423 解法一:前缀和枚举)
- [3.4 1423 解法二:滑动窗口(补集转化)](#3.4 1423 解法二:滑动窗口(补集转化))
- [4. 总结](#4. 总结)
-
- [2461 vs 1423 对比](#2461 vs 1423 对比)
- 两种定长滑动窗口变体
- 核心要点

🎬 博主名称: 超级苦力怕
🔥 个人专栏: 《LeetCode 题解》
🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!
本篇文章讲解的是 LeetCode 第 2461 题------长度为 K 子数组中的最大和 和 第 1423 题------可获得的最大点数 。两道题看似不同,本质上都离不开 定长滑动窗口 :2461 在窗口内用哈希表判重,1423 则需要将问题巧妙转化为求窗口最小和。
本文将使用 Java 进行讲解,从暴力枚举逐步过渡到滑动窗口,帮助你掌握定长滑窗的两类进阶变体------判重约束与互补转化。
2461. 长度为 K 子数组中的最大和
1. 题目介绍
2461. 长度为 K 子数组中的最大和
直达链接:LeetCode 2461
给你一个整数数组 nums 和一个整数 k。请你找出 nums 中长度为 k 的子数组中,所有元素 互不相同 的子数组的 最大和 。如果不存在这样的子数组,返回 0。
注意: 子数组是数组中连续的非空序列。
示例 1:
输入:nums = [1,5,4,2,9,9,9], k = 3
输出:15
解释:长度为 3 且所有元素互不相同的子数组:
- [1,5,4] 元素和 10
- [5,4,2] 元素和 11
- [4,2,9] 元素和 15
其中最大和为 15。
示例 2:
输入:nums = [4,4,4], k = 3
输出:0
解释:不存在长度为 3 且所有元素互不相同的子数组,返回 0。
提示:
1 <= k <= nums.length <= 10^51 <= nums[i] <= 10^5

2. 解题思路
这道题是定长滑动窗口 + 判重约束的典型题。窗口长度固定为 k,我们不仅需要维护窗口内元素和,还需要确保窗口内 所有元素互不相同。
判重的常用手段是 哈希表(HashMap) ,记录每个元素在窗口内的出现次数。当 map.size() == k 时,说明窗口内恰好有 k 个不同元素,即全部互不相同,此时用窗口和更新答案。
解法一:暴力枚举
算法思想:
- 枚举每一个长度为
k的区间起点 - 用哈希集合判断区间内是否有重复元素
- 若无重复,计算区间和并更新最大值
复杂度分析:
- 时间复杂度:O(n·k),每个区间需遍历 k 个元素
- 空间复杂度:O(k),哈希集合存储区间元素
解法二:滑动窗口 + 哈希表(推荐)
窗口右移时动态维护窗口和 sum 与频率表 map:
- 加入右端元素:
sum += nums[i],map中计数 +1 - 移出左端元素:
sum -= nums[i-k],map中计数 -1,计数归零时移除 key - 当
map.size() == k,说明窗口内 k 个元素全不相同,更新最大和
示例推演 (nums = [1,5,4,2,9,9,9],k = 3)
1. 初始窗口 [0,2]:[1,5,4]
- sum = 10,map = {1,5,4},size = 3 == k ✓
- ans = 10
2. 窗口右移 → [1,3]:[5,4,2]
- 加入 nums[3]=2,移出 nums[0]=1
- sum = 11,map = {5,4,2},size = 3 == k ✓
- ans = 11
3. 窗口右移 → [2,4]:[4,2,9]
- 加入 nums[4]=9,移出 nums[1]=5
- sum = 15,map = {4,2,9},size = 3 == k ✓
- ans = 15
4. 窗口右移 → [3,5]:[2,9,9]
- 加入 nums[5]=9,移出 nums[2]=4
- sum = 20,map = {2,9:2},size = 2 ≠ k ✗
5. 窗口右移 → [4,6]:[9,9,9]
- 加入 nums[6]=9,移出 nums[3]=2
- sum = 27,map = {9:3},size = 1 ≠ k ✗
最终答案:15
复杂度分析:
- 时间复杂度:O(n),每个元素进入和离开窗口各一次,哈希表操作 O(1)
- 空间复杂度:O(k),哈希表最多存 k 个不同元素
1423. 可获得的最大点数
1. 题目介绍
1423. 可获得的最大点数
直达链接:LeetCode 1423
几张卡牌排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。
每次行动,你可以从 开头 或者 末尾 拿走一张卡牌,最终你必须正好拿 k 张卡牌。你的点数就是你拿到的所有卡牌的点数之和。
请你返回你可以获得的最大点数。
示例 1:
输入:cardPoints = [1,2,3,4,5,6,1], k = 3
输出:12
解释:第一次拿最后一张,点数 1;
第二次拿最后一张,点数 6;
第三次拿最后一张,点数 5。
总点数 1+6+5 = 12。
示例 2:
输入:cardPoints = [2,2,2], k = 2
输出:4
解释:无论拿哪两张,点数都是 4。
示例 3:
输入:cardPoints = [9,7,7,9,7,7,9], k = 7
输出:55
解释:必须拿完所有卡牌,点数即为数组总和。
提示:
1 <= cardPoints.length <= 10^51 <= cardPoints[i] <= 10^41 <= k <= cardPoints.length

2. 解题思路
这道题的难点在于每次可以从 开头或末尾 拿牌,这意味着拿走的 k 张牌不是连续的------它们由数组开头的一段和末尾的一段拼接而成。
如果直接枚举拿几张开头、几张末尾,看似需要 O(k) 种情况,但 k 最坏可达 10^5,配合每次求和仍然很慢。
关键转化: 拿走 k 张牌后,剩下的 n-k 张牌必然是一段 连续 的子数组。因此:
最大点数 = 所有卡牌总和 − 剩下卡牌的最小点数
即:
ans = totalSum - minSum(长度为 n-k 的子数组)
当 k == n 时,拿走所有卡牌,剩下 0 张,直接返回总和即可。
解法一:前缀和枚举
算法思想:
- 枚举从开头拿
i张(0 <= i <= k),则从末尾拿k-i张 - 开头点数:前
i张的和(前缀和) - 末尾点数:后
k-i张的和(后缀和) - 取所有情况的最大值
复杂度分析:
- 时间复杂度:O(k),共 k+1 种情况,前缀/后缀和 O(1) 查询
- 空间复杂度:O(n),前缀和数组
解法二:滑动窗口(推荐)------转化为求补集最小和
维护一个长度为 n-k 的滑动窗口,找到窗口内元素和的最小值,然后用总和减去最小值。
实现思路:
- 计算
totalSum= 数组总和 - 若
k == n,直接返回totalSum - 维护长度为
len = n - k的定长窗口 - 用
minSum记录窗口和的最小值 - 答案 =
totalSum - minSum
示例推演 (cardPoints = [1,2,3,4,5,6,1],k = 3)
totalSum = 1+2+3+4+5+6+1 = 22
剩下长度 len = n - k = 7 - 3 = 4
1. 初始窗口 [0,3]:[1,2,3,4]
- windowSum = 10,minSum = 10
2. 窗口右移 → [1,4]:[2,3,4,5]
- 加入 5,移出 1
- windowSum = 14,minSum = 10
3. 窗口右移 → [2,5]:[3,4,5,6]
- 加入 6,移出 2
- windowSum = 18,minSum = 10
4. 窗口右移 → [3,6]:[4,5,6,1]
- 加入 1,移出 3
- windowSum = 16,minSum = 10
ans = totalSum - minSum = 22 - 10 = 12
对应拿走末尾 3 张:[5,6,1],点数和 = 12 ✓
复杂度分析:
- 时间复杂度:O(n),一次遍历维护窗口
- 空间复杂度:O(1),只用了几个变量
3. 示例代码
3.1 2461 解法一:暴力枚举
java
import java.util.HashSet;
class Solution {
public long maximumSubarraySum(int[] nums, int k) {
int n = nums.length;
long ans = 0;
for (int i = 0; i <= n - k; i++) {
HashSet<Integer> set = new HashSet<>();
long sum = 0;
boolean valid = true;
for (int j = i; j < i + k; j++) {
if (!set.add(nums[j])) {
valid = false;
break;
}
sum += nums[j];
}
if (valid) {
ans = Math.max(ans, sum);
}
}
return ans;
}
}
3.2 2461 解法二:滑动窗口 + 哈希表
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public long maximumSubarraySum(int[] nums, int k) {
int n = nums.length;
Map<Integer, Integer> map = new HashMap<>();
long sum = 0, ans = 0;
for (int i = 0; i < n; i++) {
// 加入右端元素
sum += nums[i];
map.merge(nums[i], 1, Integer::sum);
if (i < k - 1) {
continue;
}
// 窗口长度 = k,判重
if (map.size() == k) {
ans = Math.max(ans, sum);
}
// 移出左端元素
int left = nums[i - k + 1];
sum -= left;
if (map.merge(left, -1, Integer::sum) == 0) {
map.remove(left);
}
}
return ans;
}
}
3.3 1423 解法一:前缀和枚举
java
class Solution {
public int maxScore(int[] cardPoints, int k) {
int n = cardPoints.length;
int[] pre = new int[n + 1];
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i] + cardPoints[i];
}
int ans = 0;
for (int i = 0; i <= k; i++) {
int leftSum = pre[i]; // 开头 i 张
int rightSum = pre[n] - pre[n - (k - i)]; // 末尾 k-i 张
ans = Math.max(ans, leftSum + rightSum);
}
return ans;
}
}
3.4 1423 解法二:滑动窗口(补集转化)
核心思想:拿走 k 张后剩下 n-k 张连续卡牌。最大化拿走的点数 = 最小化剩下的点数。问题转化为求长度为 n-k 的窗口最小和。
java
class Solution {
public int maxScore(int[] cardPoints, int k) {
int n = cardPoints.length;
int totalSum = 0;
for (int p : cardPoints) {
totalSum += p;
}
if (k == n) {
return totalSum;
}
int len = n - k; // 剩下卡牌的长度
int windowSum = 0;
// 初始化窗口
for (int i = 0; i < len; i++) {
windowSum += cardPoints[i];
}
int minSum = windowSum;
// 窗口右移
for (int i = len; i < n; i++) {
windowSum += cardPoints[i] - cardPoints[i - len];
minSum = Math.min(minSum, windowSum);
}
return totalSum - minSum;
}
}
更简洁的写法:
java
class Solution {
public int maxScore(int[] cardPoints, int k) {
int n = cardPoints.length;
int total = 0;
for (int p : cardPoints) total += p;
if (k == n) return total;
int len = n - k;
int sum = 0;
for (int i = 0; i < len; i++) sum += cardPoints[i];
int min = sum;
for (int i = len; i < n; i++) {
sum += cardPoints[i] - cardPoints[i - len];
if (sum < min) min = sum;
}
return total - min;
}
}
4. 总结
2461 vs 1423 对比
| 维度 | 2461. 长度为K子数组中的最大和 | 1423. 可获得的最大点数 |
|---|---|---|
| 难度 | 中等 | 中等 |
| 窗口类型 | 定长滑窗 + 判重 | 定长滑窗 + 补集转化 |
| 辅助结构 | HashMap(频率表) | 无需额外结构 |
| 判定条件 | map.size() == k |
求剩余窗口最小和 |
| 核心技巧 | 滑动窗口 + 哈希表判重 | 正难则反,转化为求最小值 |
两种定长滑动窗口变体
本文两道题展示了定长滑窗的两种重要变体:
变体一:窗口内判重(2461)
- 用 HashMap 维护窗口内每个元素的频次
map.size() == k等价于窗口内元素全部互不相同- 窗口右移时动态维护频次表,计数归零时及时
remove
变体二:补集转化(1423)
- 正面枚举开头的张数需要 O(k) 种情况
- 转化为求剩余
n-k张连续卡牌的最小和 - 原问题的最优解 = 总和 − 补集的最劣解
- 这种"正难则反"的转化思维在实际面试中非常常见
核心要点
- 2461 中
map.size()直接反映窗口内不同元素个数,无需额外变量跟踪是否有重复 - 2461 中
nums[i]和sum可能超过 int 范围,需使用long类型 - 1423 的核心是 转化:拿走 k 张(首尾任意) → 剩下 n-k 张(必然连续)
Map.merge(key, delta, Integer::sum)一行完成计数更新,返回 0 时remove即可正确维护size- 1423 中
k == n是边界情况,需单独处理(剩余窗口长度为 0)
