LeetCode 2379 & 2841:定长滑动窗口进阶
-
- [2379. 得到 K 个黑块的最少涂色次数](#2379. 得到 K 个黑块的最少涂色次数)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 解题思路](#2. 解题思路)
- [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. 总结)
-
- [2379 vs 2841 对比](#2379 vs 2841 对比)
- 定长滑动窗口通用模板
- 核心要点

🎬 博主名称: 超级苦力怕
🔥 个人专栏: 《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.length1 <= n <= 100blocks[i]为'W'或'B'1 <= k <= n

2. 解题思路
题目本质要求我们找出长度为 k 的窗口中,白色块数量最少是多少。因为每个白色块都需要涂成黑色,所以窗口内白色块的数量就是需要操作的次数。
由于窗口长度固定为 k,我们用 定长滑动窗口 维护当前窗口内的白色块数量,一次遍历即可得出答案。
解法一:暴力枚举
算法思想:
- 枚举每一个长度为
k的区间起点i(0 <= 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,以及两个正整数 m 和 k。
如果一个长度为 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^41 <= nums[i] <= 10^91 <= 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 = 3,k = 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();
}
核心要点
- 定长滑动窗口的核心是 维护窗口长度固定为 k,每次右移加入一个元素就移出一个元素
- 2379 是定长窗口最基本的应用------在窗口内做简单计数
- 2841 引入了 HashMap 来维护窗口内的相异元素计数,是定长窗口与哈希表的经典结合
Map.merge(key, delta, Integer::sum)可以在一行内完成计数更新,当计数归零时remove掉 key 即可正确维护map.size()- 2841 中
nums[i]和sum可能较大,需使用long类型防止溢出
