LeetCode 2379 & 2841 题解:一文掌握定长滑动窗口的两类变体——简单计数与 HashMap 去重

LeetCode 2379 & 2841:定长滑动窗口进阶

    • [2379. 得到 K 个黑块的最少涂色次数](#2379. 得到 K 个黑块的最少涂色次数)
    • [2841. 几乎唯一子数组的最大和](#2841. 几乎唯一子数组的最大和)
      • [1. 题目介绍](#1. 题目介绍)
      • [2. 解题思路](#2. 解题思路)
        • 解法一:暴力枚举
        • [解法二:滑动窗口 + 哈希表(推荐)](#解法二:滑动窗口 + 哈希表(推荐))
    • [3. 示例代码](#3. 示例代码)
      • [3.1 2379 解法一:暴力枚举](#3.1 2379 解法一:暴力枚举)
      • [3.2 2379 解法二:滑动窗口](#3.2 2379 解法二:滑动窗口)
      • [3.3 2841 解法一:暴力枚举](#3.3 2841 解法一:暴力枚举)
      • [3.4 2841 解法二:滑动窗口 + 哈希表](#3.4 2841 解法二:滑动窗口 + 哈希表)
    • [4. 总结](#4. 总结)

🎬 博主名称: 超级苦力怕

🔥 个人专栏: 《LeetCode 题解》

🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!


本篇文章讲解的是 LeetCode 第 2379 题------得到 K 个黑块的最少涂色次数 和 第 2841 题------几乎唯一子数组的最大和 。两道题都是 定长滑动窗口 的经典应用,前者考察窗口内特定字符的计数,后者在窗口求和的基础上结合了哈希表统计相异元素个数。

本文将使用 Java 进行讲解,从暴力枚举出发,逐步过渡到滑动窗口,帮助你彻底掌握定长滑动窗口的两类常见变体。

2379. 得到 K 个黑块的最少涂色次数

1. 题目介绍

2379. 得到 K 个黑块的最少涂色次数

直达链接:LeetCode 2379

给你一个长度为 n 的字符串 blocks,其中 blocks[i]'W''B',分别表示白色块和黑色块。

同时给你一个整数 k,表示想要得到的 连续 黑色块的数量。

每一次操作中,你可以将一个白色块涂成黑色块。请你返回 至少 需要多少次操作,才能出现 k连续 的黑色块。

示例 1:

复制代码
输入:blocks = "WBBWWBBWBW", k = 7
输出:3
解释:一种方法是涂色下标 0、4 和 5,得到 blocks = "BBBBBBBWBW",此时 [0,6] 区间内全是黑色。

示例 2:

复制代码
输入:blocks = "WBWBBBW", k = 2
输出:0
解释:不需要任何操作,已经存在长度为 2 的连续黑色块(下标 3 和 4)。

提示:

  • n == blocks.length
  • 1 <= n <= 100
  • blocks[i]'W''B'
  • 1 <= k <= n

2. 解题思路

题目本质要求我们找出长度为 k 的窗口中,白色块数量最少是多少。因为每个白色块都需要涂成黑色,所以窗口内白色块的数量就是需要操作的次数。

由于窗口长度固定为 k,我们用 定长滑动窗口 维护当前窗口内的白色块数量,一次遍历即可得出答案。

解法一:暴力枚举

算法思想:

  • 枚举每一个长度为 k 的区间起点 i0 <= i <= n - k
  • 遍历区间 [i, i+k-1],统计其中 'W' 的个数
  • 取所有区间中的最小值

复杂度分析:

  • 时间复杂度:O(n·k),每个区间需要遍历 k 个元素
  • 空间复杂度:O(1)
解法二:滑动窗口(推荐)

维护一个长度为 k 的窗口,窗口每次右移时,加入右侧新元素、移除左侧旧元素,并更新窗口内 'W' 的计数。

实现思路:

  • 初始化窗口:统计前 k 个字符中 'W' 的数量作为 cnt
  • 窗口右移:cnt 减去移出字符的贡献,加上新进入字符的贡献
  • 每一步更新最小值

这样每个字符只被访问两次(进窗口、出窗口),时间复杂度优化到 O(n)。

示例推演 (以示例 1 为例:blocks = "WBBWWBBWBW"k = 7

复制代码
n=10, k=7

1. 初始窗口 [0,6]:"WBBWWBB"
   - W 数量 cnt = 3,ans = 3

2. 窗口右移 → [1,7]:"BBWWBBW"
   - 移出 blocks[0]='W',加入 blocks[7]='W'
   - cnt 保持 = 3,ans = 3

3. 窗口右移 → [2,8]:"BWWBBWB"
   - 移出 blocks[1]='B',加入 blocks[8]='B'
   - cnt 保持 = 3,ans = 3

4. 窗口右移 → [3,9]:"WWBBWBW"
   - 移出 blocks[2]='B',加入 blocks[9]='W'
   - cnt = 4,ans 保持 = 3

最终答案:3

复杂度分析:

  • 时间复杂度:O(n),每个元素至多参与一次加入和一次移出窗口
  • 空间复杂度:O(1)

2841. 几乎唯一子数组的最大和

1. 题目介绍

2841. 几乎唯一子数组的最大和

直达链接:LeetCode 2841

给你一个长度为 n 的整数数组 nums,以及两个正整数 mk

如果一个长度为 k 的子数组中 不同元素的数目 至少为 m,则称该子数组是 几乎唯一 的。

请你返回 最大 的几乎唯一子数组的元素和。如果不存在这样的子数组,返回 0

注意: 子数组是数组中连续的非空序列。

示例 1:

复制代码
输入:nums = [2,6,7,3,1,7], m = 3, k = 4
输出:18
解释:总共有 3 个长度为 k 的几乎唯一子数组:
- [2,6,7,3] 元素和 18,不同元素数目 4 >= 3
- [6,7,3,1] 元素和 17,不同元素数目 4 >= 3
- [7,3,1,7] 元素和 18,不同元素数目 3 >= 3
最大和为 18。

示例 2:

复制代码
输入:nums = [5,9,9,2,4,5,4], m = 1, k = 3
输出:23
解释:m=1 时任意子数组都满足条件。
长度为 3 的子数组中,[5,9,9] 的和最大,为 23。

示例 3:

复制代码
输入:nums = [1,2,1,2,1,2,1], m = 3, k = 3
输出:0
解释:所有长度为 3 的子数组最多只有 2 个不同元素,不存在几乎唯一子数组。

提示:

  • 1 <= n == nums.length <= 2 * 10^4
  • 1 <= nums[i] <= 10^9
  • 1 <= m <= k <= n

2. 解题思路

这道题比 2379 更进一步:不仅要用定长滑动窗口维护区间和,还要维护窗口内 不同元素的个数

不同元素的计数天然适合用 哈希表。窗口右移时:

  • 加入新元素:哈希表中对应计数 +1,若从 0 变为 1,则不同元素数 +1
  • 移出旧元素:哈希表中对应计数 -1,若从 1 变为 0,则不同元素数 -1

当窗口内不同元素数 ≥ m 时,用当前窗口和更新答案。

解法一:暴力枚举

算法思想:

  • 枚举每一个长度为 k 的区间起点
  • 用哈希集合统计区间内的不同元素数
  • 若满足条件,计算区间和并更新最大值

复杂度分析:

  • 时间复杂度:O(n·k),每个区间都要遍历 k 个元素重新统计
  • 空间复杂度:O(k),哈希集合存储区间元素
解法二:滑动窗口 + 哈希表(推荐)

实现思路:

  • 使用 HashMap<Integer, Integer> 记录窗口内每个元素的出现次数
  • sum 维护窗口元素和,distinct 维护不同元素个数
  • 窗口扩张阶段(i < k - 1):只加入不移出
  • 窗口稳定阶段(i >= k - 1):加入新元素 → 判断并更新答案 → 移出左端元素

示例推演 (以示例 1 为例:nums = [2,6,7,3,1,7]m = 3k = 4

复制代码
1. 形成初始窗口 [0,3]:[2,6,7,3]
   - sum = 18,distinct = 4 >= 3 ✓
   - ans = 18

2. 窗口右移 → [1,4]:[6,7,3,1]
   - 加入 nums[4]=1,map: {6,7,3,1},distinct=4
   - 移出 nums[0]=2,map: {6,7,3,1},distinct=4
   - sum = 6+7+3+1 = 17,distinct=4 >= 3 ✓
   - ans 保持 = 18

3. 窗口右移 → [2,5]:[7,3,1,7]
   - 加入 nums[5]=7,map: {7:2,3,1},distinct=3
   - 移出 nums[1]=6,map: {7:2,3,1},distinct=3
   - sum = 7+3+1+7 = 18,distinct=3 >= 3 ✓
   - ans 保持 = 18

最终答案:18

复杂度分析:

  • 时间复杂度:O(n),每个元素恰好进入和离开窗口各一次,哈希表操作 O(1)
  • 空间复杂度:O(k),哈希表最多存储 k 个键值对

3. 示例代码

3.1 2379 解法一:暴力枚举

java 复制代码
class Solution {
    public int minimumRecolors(String blocks, int k) {
        int n = blocks.length();
        int ans = Integer.MAX_VALUE;

        for (int i = 0; i <= n - k; i++) {
            int cnt = 0;
            for (int j = i; j < i + k; j++) {
                if (blocks.charAt(j) == 'W') {
                    cnt++;
                }
            }
            ans = Math.min(ans, cnt);
        }

        return ans;
    }
}

3.2 2379 解法二:滑动窗口

java 复制代码
class Solution {
    public int minimumRecolors(String blocks, int k) {
        int n = blocks.length();
        int cnt = 0;

        // 初始化窗口:统计前 k 个字符中的 'W'
        for (int i = 0; i < k; i++) {
            if (blocks.charAt(i) == 'W') {
                cnt++;
            }
        }

        int ans = cnt;

        // 窗口右移
        for (int i = k; i < n; i++) {
            // 移出左端
            if (blocks.charAt(i - k) == 'W') {
                cnt--;
            }
            // 加入右端
            if (blocks.charAt(i) == 'W') {
                cnt++;
            }
            ans = Math.min(ans, cnt);
        }

        return ans;
    }
}

3.3 2841 解法一:暴力枚举

java 复制代码
import java.util.HashSet;

class Solution {
    public long maxSum(int[] nums, int m, 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;
            for (int j = i; j < i + k; j++) {
                set.add(nums[j]);
                sum += nums[j];
            }
            if (set.size() >= m) {
                ans = Math.max(ans, sum);
            }
        }

        return ans;
    }
}

3.4 2841 解法二:滑动窗口 + 哈希表

核心思想 :维护一个长度为 k 的窗口,动态维护窗口元素和与不同元素个数,当不同元素数 ≥ m 时更新答案。

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {
    public long maxSum(int[] nums, int m, int k) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        long sum = 0;
        long 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() >= m) {
                ans = Math.max(ans, sum);
            }

            // 移出左端元素
            int left = nums[i - k + 1];
            sum -= left;
            int cnt = map.get(left);
            if (cnt == 1) {
                map.remove(left);
            } else {
                map.put(left, cnt - 1);
            }
        }

        return ans;
    }
}

更简洁的写法

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {
    public long maxSum(int[] nums, int m, 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;

            if (map.size() >= m) {
                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;
    }
}

4. 总结

2379 vs 2841 对比

维度 2379. 得到 K 个黑块的最少涂色次数 2841. 几乎唯一子数组的最大和
难度 简单 中等
窗口统计量 特定字符 'W' 的数量 元素和 + 不同元素个数
辅助结构 无(一个 int 变量) HashMap
判定条件 所有窗口取最小值 distinct ≥ m 时取最大和
核心技巧 定长滑窗 + 计数 定长滑窗 + 哈希表

定长滑动窗口通用模板

java 复制代码
// 初始化:填充窗口前 k 个元素
for (int i = 0; i < k; i++) {
    // 加入元素,更新窗口状态
}

// 处理初始窗口的答案
updateAnswer();

// 窗口右移
for (int i = k; i < n; i++) {
    加入 nums[i];
    移出 nums[i - k];
    updateAnswer();
}

核心要点

  1. 定长滑动窗口的核心是 维护窗口长度固定为 k,每次右移加入一个元素就移出一个元素
  2. 2379 是定长窗口最基本的应用------在窗口内做简单计数
  3. 2841 引入了 HashMap 来维护窗口内的相异元素计数,是定长窗口与哈希表的经典结合
  4. Map.merge(key, delta, Integer::sum) 可以在一行内完成计数更新,当计数归零时 remove 掉 key 即可正确维护 map.size()
  5. 2841 中 nums[i]sum 可能较大,需使用 long 类型防止溢出
相关推荐
Liangwei Lin3 小时前
LeetCode 20. 有效的括号
算法
IronMurphy3 小时前
【算法四十四】322. 零钱兑换
算法
凯瑟琳.奥古斯特3 小时前
力扣2760 C++滑动窗口解法
数据结构·c++·算法·leetcode·职场和发展
Hesionberger3 小时前
LeetCode96: 不同的二叉搜索树(多解)
算法
_深海凉_3 小时前
LeetCode热题100-不同路径
算法·leetcode·职场和发展
ZPC82103 小时前
CPU 核心隔离 + 线程绑核 + 实时优先级 SCHED_FIFO
人工智能·算法·计算机视觉·机器人
andafaAPS4 小时前
安达发|aps自动排产排程排单软件:日化生产高效运转“数字魔法”
大数据·人工智能·算法·aps软件·安达发aps·aps自动排产排程排单软件
黎阳之光4 小时前
全域实景立体管控:数字孪生与视频孪生技术体系白皮书
大数据·人工智能·算法·安全·数字孪生
88号技师4 小时前
2026年4月一区SCI-狒狒优化算法Baboon optimization algorithm-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
凯瑟琳.奥古斯特5 小时前
BFS解力扣1654最短跳跃次数
数据结构·算法·广度优先