LeetCode 2461 & 1423:定长滑窗变体精讲,从 HashMap 判重到正难则反的转化技巧

LeetCode 2461 & 1423:定长滑动窗口变体------判重与互补转化

    • [2461. 长度为 K 子数组中的最大和](#2461. 长度为 K 子数组中的最大和)
      • [1. 题目介绍](#1. 题目介绍)
      • [2. 解题思路](#2. 解题思路)
        • 解法一:暴力枚举
        • [解法二:滑动窗口 + 哈希表(推荐)](#解法二:滑动窗口 + 哈希表(推荐))
    • [1423. 可获得的最大点数](#1423. 可获得的最大点数)
    • [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. 总结)

🎬 博主名称: 超级苦力怕

🔥 个人专栏: 《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^5
  • 1 <= 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^5
  • 1 <= cardPoints[i] <= 10^4
  • 1 <= 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 张连续卡牌的最小和
  • 原问题的最优解 = 总和 − 补集的最劣解
  • 这种"正难则反"的转化思维在实际面试中非常常见

核心要点

  1. 2461 中 map.size() 直接反映窗口内不同元素个数,无需额外变量跟踪是否有重复
  2. 2461 中 nums[i]sum 可能超过 int 范围,需使用 long 类型
  3. 1423 的核心是 转化:拿走 k 张(首尾任意) → 剩下 n-k 张(必然连续)
  4. Map.merge(key, delta, Integer::sum) 一行完成计数更新,返回 0 时 remove 即可正确维护 size
  5. 1423 中 k == n 是边界情况,需单独处理(剩余窗口长度为 0)
相关推荐
WL_Aurora1 小时前
【每日一题】二分算法
python·算法
昵称小白1 小时前
子串专题部分
数据结构·算法·哈希算法
H_BB2 小时前
第17届蓝桥杯备战历程
c++·算法·职场和发展·蓝桥杯
anew___2 小时前
算法分析与设计课程全算法核心概述|期末复习+知识梳理
算法
daad7772 小时前
记录一次上下文切换次数的统计
服务器·c++·算法
fliter2 小时前
Cloudflare 推出 Flagship:为 AI 时代重新设计的功能开关服务
后端·算法
生成论实验室2 小时前
《源·觉·知·行·事·物:生成论视域下的统一认知语法》第十七章 科学与人心的重聚
人工智能·算法·架构·知识图谱·创业创新
chao1898442 小时前
局部保局投影(LPP)算法实现
算法
ShoreKiten2 小时前
cpp考前急救
数据结构·c++·算法