【算法】爱生气的书店老板 & 使库存平衡的最少丢弃次数
-
- [1052. 爱生气的书店老板](#1052. 爱生气的书店老板)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 题目示例](#2. 题目示例)
- [3. 算法思路](#3. 算法思路)
- [4. 核心代码](#4. 核心代码)
- [5. 示例测试(总代码)](#5. 示例测试(总代码))
- [3679. 使库存平衡的最少丢弃次数](#3679. 使库存平衡的最少丢弃次数)
-
- [1. 题目介绍](#1. 题目介绍)
- [2. 题目示例](#2. 题目示例)
- [3. 算法思路](#3. 算法思路)
-
- 解法一:暴力模拟
- [解法二:滑动窗口 + 频率哈希表(推荐)](#解法二:滑动窗口 + 频率哈希表(推荐))
- [4. 核心代码](#4. 核心代码)
- [5. 示例测试(总代码)](#5. 示例测试(总代码))
- 总结

🎬 博主名称: 超级苦力怕
🔥 个人专栏: 《LeetCode 题解》
🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!
本篇文章讲解的是 LeetCode 第 1052 题------爱生气的书店老板 和 第 3679 题------使库存平衡的最少丢弃次数 。两道题看似场景迥异------一个在最大化顾客满意度,一个在最小化丢弃次数------但本质上考察的都是 定长滑动窗口 在不同约束条件下的灵活运用。
1052 是经典的"窗口增益最大化"问题:先算出不生气时的基准满意人数,再通过滑动窗口找到使用"冷静技巧"能额外挽回的最大顾客数。3679 则将滑动窗口与 贪心决策 相结合:每天在窗口频率约束下,决定物品的保留或丢弃。
本文将使用 Java 进行讲解,从暴力枚举出发,逐步过渡到滑动窗口,帮助你掌握定长滑窗的两类进阶变体------增益最大化与频率约束下的贪心取舍。
1052. 爱生气的书店老板
1. 题目介绍
1052. 爱生气的书店老板
直达链接:LeetCode 1052
书店开了 n 分钟。给定一个长度为 n 的整数数组 customers,其中 customers[i] 是第 i 分钟进入书店的顾客数量。同时给定一个二进制数组 grumpy:若书店老板在第 i 分钟生气,则 grumpy[i] = 1,否则为 0。
当书店老板生气时,那一分钟的顾客会不满意;老板不生气时,顾客满意。书店老板知道一个秘密技巧,可以让自己 连续 minutes 分钟不生气,但只能使用 一次。
返回这一天营业下来,最多有多少顾客能够满意。

提示:
n == customers.length == grumpy.length1 <= minutes <= n <= 2 * 10^40 <= customers[i] <= 1000grumpy[i]为0或1
2. 题目示例
示例 1:
输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], minutes = 3
输出:16
解释:书店老板在最后 3 分钟保持冷静。
感到满意的最大顾客数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16。
示例 2:
输入:customers = [1], grumpy = [0], minutes = 1
输出:1
3. 算法思路
本题的核心在于:老板的"冷静技巧"只能连续使用一次,需要找到最优的使用时机。
首先理清两个事实:
- 老板原本就不生气的那些分钟 (
grumpy[i] == 0),顾客天然满意,无论技巧是否覆盖 - 老板生气的那些分钟 (
grumpy[i] == 1),只有被技巧覆盖时,顾客才会满意
因此,解题分为两步:
- 第一步 :计算"基准满意人数"------所有
grumpy[i] == 0的分钟对应的customers[i]之和,记为base - 第二步 :使用一个长度为
minutes的滑动窗口,计算窗口内grumpy[i] == 1的customers[i]之和(即通过技巧能"额外挽回"的顾客数),取最大值记为extra - 最终答案 :
base + extra
解法一:暴力枚举
算法思想:
- 枚举所有长度为
minutes的窗口起点 - 对每个窗口,遍历窗口内所有分钟,累加
grumpy[i] == 1时的customers[i] - 取最大值与
base相加
复杂度分析:
- 时间复杂度:O(n * minutes),每个窗口需遍历
minutes个元素 - 空间复杂度:O(1)
解法二:滑动窗口(推荐)
维护长度为 minutes 的滑动窗口,动态维护窗口内"可挽回顾客数":
- 右端点
i从 0 遍历到n-1 - 加入右端元素:若
grumpy[i] == 1,则extra += customers[i] - 当窗口长度达到
minutes(即i >= minutes - 1):- 用
extra更新最大额外挽回数 - 移出左端元素:若
grumpy[i - minutes + 1] == 1,则extra -= customers[i - minutes + 1]
- 用
示例推演 (customers = [1,0,1,2,1,1,7,5],grumpy = [0,1,0,1,0,1,0,1],minutes = 3)
基准满意人数 base:
grumpy[0]=0 → +1
grumpy[2]=0 → +1
grumpy[4]=0 → +1
grumpy[6]=0 → +7
base = 1 + 1 + 1 + 7 = 10
滑动窗口找最大额外挽回(只统计 grumpy[i]==1 的 customers[i]):
1. 窗口 [0,2]:[0,1,0] → extra = customers[1] = 0
窗口内 grumpy=1 的有: i=1(customers[1]=0) → extra=0
2. 窗口 [1,3]:[1,0,1] → 加入 customers[3]=2(grumpy[3]=1), 移出 customers[0]=1(grumpy[0]=0)
extra = 0 + 2 - 0 = 2
3. 窗口 [2,4]:[0,1,0] → 加入 customers[4]=1(grumpy[4]=0), 移出 customers[1]=0(grumpy[1]=1)
extra = 2 + 0 - 0 = 2
4. 窗口 [3,5]:[1,0,1] → 加入 customers[5]=1(grumpy[5]=1), 移出 customers[2]=1(grumpy[2]=0)
extra = 2 + 1 - 0 = 3
5. 窗口 [4,6]:[0,1,0] → 加入 customers[6]=7(grumpy[6]=0), 移出 customers[3]=2(grumpy[3]=1)
extra = 3 + 0 - 2 = 1
6. 窗口 [5,7]:[1,0,1] → 加入 customers[7]=5(grumpy[7]=1), 移出 customers[4]=1(grumpy[4]=0)
extra = 1 + 5 - 0 = 6
maxExtra = max(0, 2, 2, 3, 1, 6) = 6
答案 = base + maxExtra = 10 + 6 = 16 ✓
复杂度分析:
- 时间复杂度:O(n),每个元素至多进入和离开窗口各一次
- 空间复杂度:O(1),只使用常数额外空间
4. 核心代码
解法一:暴力枚举
java
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int n = customers.length;
int base = 0;
for (int i = 0; i < n; i++) {
if (grumpy[i] == 0) {
base += customers[i];
}
}
int maxExtra = 0;
for (int i = 0; i <= n - minutes; i++) {
int extra = 0;
for (int j = i; j < i + minutes; j++) {
if (grumpy[j] == 1) {
extra += customers[j];
}
}
maxExtra = Math.max(maxExtra, extra);
}
return base + maxExtra;
}
}
解法二:滑动窗口(推荐)
java
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int n = customers.length;
int base = 0;
for (int i = 0; i < n; i++) {
if (grumpy[i] == 0) {
base += customers[i];
}
}
int extra = 0;
for (int i = 0; i < minutes; i++) {
if (grumpy[i] == 1) {
extra += customers[i];
}
}
int maxExtra = extra;
for (int i = minutes; i < n; i++) {
if (grumpy[i] == 1) {
extra += customers[i];
}
if (grumpy[i - minutes] == 1) {
extra -= customers[i - minutes];
}
maxExtra = Math.max(maxExtra, extra);
}
return base + maxExtra;
}
}
更简洁的写法:
java
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int n = customers.length;
int base = 0;
for (int i = 0; i < n; i++) {
if (grumpy[i] == 0) base += customers[i];
}
int extra = 0, maxExtra = 0;
for (int i = 0; i < n; i++) {
if (grumpy[i] == 1) extra += customers[i];
if (i < minutes - 1) continue;
maxExtra = Math.max(maxExtra, extra);
if (grumpy[i - minutes + 1] == 1) extra -= customers[i - minutes + 1];
}
return base + maxExtra;
}
}
5. 示例测试(总代码)
java
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
// 示例1测试
int[] customers1 = {1, 0, 1, 2, 1, 1, 7, 5};
int[] grumpy1 = {0, 1, 0, 1, 0, 1, 0, 1};
System.out.println("示例1输出:" + sol.maxSatisfied(customers1, grumpy1, 3)); // 预期输出16
// 示例2测试
int[] customers2 = {1};
int[] grumpy2 = {0};
System.out.println("示例2输出:" + sol.maxSatisfied(customers2, grumpy2, 1)); // 预期输出1
}
}
3679. 使库存平衡的最少丢弃次数
1. 题目介绍
3679. 使库存平衡的最少丢弃次数
直达链接:LeetCode 3679
给你两个整数 w 和 m,以及一个整数数组 arrivals,其中 arrivals[i] 表示第 i 天到达的物品类型(天数从 1 开始编号)。
物品的管理遵循以下规则:
- 每个到达的物品可以被 保留 或 丢弃,物品只能在到达当天被丢弃
- 对于每一天
i,考虑天数范围为[max(1, i - w + 1), i](即截止到第i天为止最近的w天):- 对于 任何 这样的时间窗口,在被保留的到达物品中,每种类型最多只能出现
m次 - 如果在第
i天保留该到达物品会导致其类型在该窗口中出现次数 超过m次,那么该物品 必须 被丢弃
- 对于 任何 这样的时间窗口,在被保留的到达物品中,每种类型最多只能出现
返回为满足每个 w 天的窗口中每种类型最多出现 m 次,最少 需要丢弃的物品数量。

提示:
1 <= arrivals.length <= 10^51 <= arrivals[i] <= 10^51 <= w <= arrivals.length1 <= m <= w
2. 题目示例
示例 1:
输入:arrivals = [1,2,1,3,1], w = 4, m = 2
输出:0
解释:
- 第1天,物品1到达;窗口中该类型不超过m次,保留。
- 第2天,物品2到达;第1-2天的窗口是可以接受的。
- 第3天,物品1到达,窗口[1,2,1]中物品1出现两次,符合限制。
- 第4天,物品3到达,窗口[1,2,1,3]中物品1出现两次,仍符合。
- 第5天,物品1到达,窗口[2,1,3,1]中物品1出现两次,依然有效。
没有任何物品被丢弃,返回0。
示例 2:
输入:arrivals = [1,2,3,3,3,4], w = 3, m = 2
输出:1
解释:
- 第1天,物品1到达。保留它。
- 第2天,物品2到达,窗口[1,2]是可以的。
- 第3天,物品3到达,窗口[1,2,3]中物品3出现一次。
- 第4天,物品3到达,窗口[2,3,3]中物品3出现两次,允许。
- 第5天,物品3到达,窗口[3,3,3]中物品3出现三次,超过限制,因此该物品必须被丢弃。
- 第6天,物品4到达,窗口[3,4]是可以的。
第5天的物品3被丢弃,这是最少必须丢弃的数量,返回1。
3. 算法思路
这道题是 定长滑动窗口 + 贪心决策 的组合问题。与 1052 不同之处在于,我们不仅需要维护窗口内的统计信息,还需要在每个元素进入窗口时做出 保留/丢弃 的决策。
关键约束回顾:每个物品只能在到达当天被丢弃,不能"反悔"。这意味着我们必须当天做出正确决策。
贪心策略的正确性:
- 假设当前物品可以保留(
cnt[type] < m),我们应该保留它吗? - 答案是 应该。因为保留不会让未来付出更大代价------所有同类型物品是等价的,提前丢弃一个物品只会在"未来窗口"中减少该类型的计数,但这个效果同样可以通过在未来丢弃一个物品来达到(当必要时)
- 因此,能保留就保留 的贪心策略是最优的
解法一:暴力模拟
算法思想:
- 对每一天,枚举以该天为右端点的长度为
w的窗口 - 统计窗口内每种类型保留物品的数量
- 若当前物品会导致超限,则丢弃
复杂度分析:
- 时间复杂度:O(n * w),每天都需要统计窗口内 w 天的物品
- 空间复杂度:O(n)
解法二:滑动窗口 + 频率哈希表(推荐)
核心思路:
- 维护一个长度为
w的滑动窗口,用left和i分别表示窗口的左右边界 - 使用
Map<Integer, Integer> cnt记录窗口内每种类型 保留物品 的数量 - 使用
boolean[] discarded记录每一天的物品是否被丢弃 - 遍历每一天
i:- 窗口收缩 :若窗口大小超过
w(即i - left + 1 > w),将左端元素移除窗口。注意:只有被保留的物品才需要从cnt中减掉 - 贪心决策 :查询
cnt[arrivals[i]],若< m,保留(cnt计数 +1);否则丢弃(ans++,标记discarded[i] = true)
- 窗口收缩 :若窗口大小超过
示例推演 (arrivals = [1,2,3,3,3,4],w = 3,m = 2)
初始:left=0, cnt={}, ans=0, discarded=[false,false,false,false,false,false]
1. i=0, type=1, 窗口[0,0], cnt[1]=0 < 2 → 保留, cnt={1:1}
2. i=1, type=2, 窗口[0,1], cnt[2]=0 < 2 → 保留, cnt={1:1, 2:1}
3. i=2, type=3, 窗口[0,2], cnt[3]=0 < 2 → 保留, cnt={1:1, 2:1, 3:1}
4. i=3, type=3, 窗口大小=4 > w=3 → 收缩
移除 left=0(arrivals[0]=1), 未被丢弃 → cnt[1]--, cnt={1:0,2:1,3:1}, left=1
窗口[1,3], cnt[3]=1 < 2 → 保留, cnt={1:0,2:1,3:2}
5. i=4, type=3, 窗口大小=4 > 3 → 收缩
移除 left=1(arrivals[1]=2), 未被丢弃 → cnt[2]--, cnt={1:0,2:0,3:2}, left=2
窗口[2,4], cnt[3]=2 ≥ 2 → 必须丢弃!ans=1, discarded[4]=true
6. i=5, type=4, 窗口大小=4 > 3 → 收缩
移除 left=2(arrivals[2]=3), 未被丢弃 → cnt[3]--, cnt={1:0,2:0,3:1,4:0}, left=3
窗口[3,5], cnt[4]=0 < 2 → 保留, cnt={3:1,4:1}
最终 ans = 1 ✓
复杂度分析:
- 时间复杂度:O(n),每个元素进入和离开窗口各一次,HashMap 操作 O(1)
- 空间复杂度:O(n),
discarded数组 O(n);cnt哈希表最大 O(w)
4. 核心代码
解法一:暴力模拟
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int minDiscards(int[] arrivals, int w, int m) {
int n = arrivals.length;
boolean[] discarded = new boolean[n];
int ans = 0;
for (int i = 0; i < n; i++) {
Map<Integer, Integer> cnt = new HashMap<>();
int left = Math.max(0, i - w + 1);
for (int j = left; j < i; j++) {
if (!discarded[j]) {
cnt.merge(arrivals[j], 1, Integer::sum);
}
}
int cur = cnt.getOrDefault(arrivals[i], 0);
if (cur >= m) {
discarded[i] = true;
ans++;
}
}
return ans;
}
}
解法二:滑动窗口 + 贪心(推荐)
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int minDiscards(int[] arrivals, int w, int m) {
int n = arrivals.length;
Map<Integer, Integer> cnt = new HashMap<>();
boolean[] discarded = new boolean[n];
int ans = 0;
int left = 0;
for (int i = 0; i < n; i++) {
if (i - left + 1 > w) {
if (!discarded[left]) {
cnt.merge(arrivals[left], -1, Integer::sum);
if (cnt.get(arrivals[left]) == 0) {
cnt.remove(arrivals[left]);
}
}
left++;
}
int cur = cnt.getOrDefault(arrivals[i], 0);
if (cur < m) {
cnt.merge(arrivals[i], 1, Integer::sum);
} else {
discarded[i] = true;
ans++;
}
}
return ans;
}
}
5. 示例测试(总代码)
java
public class Main {
public static void main(String[] args) {
Solution sol = new Solution();
// 示例1测试
int[] arr1 = {1, 2, 1, 3, 1};
System.out.println("示例1输出:" + sol.minDiscards(arr1, 4, 2)); // 预期输出0
// 示例2测试
int[] arr2 = {1, 2, 3, 3, 3, 4};
System.out.println("示例2输出:" + sol.minDiscards(arr2, 3, 2)); // 预期输出1
}
}
总结
1052 vs 3679 对比
| 维度 | 1052. 爱生气的书店老板 | 3679. 使库存平衡的最少丢弃次数 |
|---|---|---|
| 难度 | 中等 | 中等 |
| 窗口类型 | 定长滑窗 + 增益最大化 | 定长滑窗 + 频率约束 + 贪心 |
| 辅助结构 | 无(直接累加) | HashMap(频率表)+ boolean[](丢弃标记) |
| 优化目标 | 最大化额外满意顾客数 | 最小化丢弃次数 |
| 核心技巧 | 基准分离:base + maxExtra | 贪心保留 + 仅在超限时丢弃 |
| 是否需跟踪丢弃 | 否 | 是(窗口收缩时需区分保留/丢弃) |
两种滑动窗口变体
本文两道题展示了定长滑动窗口的两种重要变体:
变体一:增益最大化(1052)
- 将问题拆分为"基准值" + "窗口增益"
- 基准值是固定的(老板原本不生气的满意度),窗口增益是我们要最大化的目标
- 窗口内只统计"可被挽回"的元素(
grumpy[i] == 1的customers[i]) - 这种"固定 + 可变"的拆分思路在滑动窗口问题中非常普遍
变体二:频率约束下的贪心决策(3679)
- 窗口不仅是统计工具,更是决策的约束条件
- 每个元素的去留必须在进入窗口的瞬间决定,无法反悔
- 贪心策略"能留则留"的正确性依赖于同类型物品的等价性
- 需要额外空间追踪丢弃状态,确保窗口收缩时正确维护频率表
核心要点
- 1052 的关键是 分离基准与增益 :先算
base(天生满意的),再用滑动窗口找最大extra(技巧挽回的),两者互不干扰 - 1052 中
grumpy[i] == 0的顾客无论技巧是否覆盖都满意,所以在滑动窗口内只需关注grumpy[i] == 1的分钟 - 3679 的 贪心正确性 源于:所有同类型物品等价,提前丢弃一个不会比延后丢弃带来更多好处
- 3679 中窗口收缩时必须判断左端元素是否被丢弃,只有保留的物品才计入
cnt,否则会导致频率统计错误 - 3679 中
cnt用Map.merge(key, -1, Integer::sum)更新计数,返回 0 时及时remove,保证map.size()和getOrDefault的正确性
