【算法】LeetCode 1052 & 3679:定长滑动窗口进阶——增益最大化与频率约束贪心

【算法】爱生气的书店老板 & 使库存平衡的最少丢弃次数

    • [1052. 爱生气的书店老板](#1052. 爱生气的书店老板)
    • [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.length
  • 1 <= minutes <= n <= 2 * 10^4
  • 0 <= customers[i] <= 1000
  • grumpy[i]01

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. 算法思路

本题的核心在于:老板的"冷静技巧"只能连续使用一次,需要找到最优的使用时机

首先理清两个事实:

  1. 老板原本就不生气的那些分钟grumpy[i] == 0),顾客天然满意,无论技巧是否覆盖
  2. 老板生气的那些分钟grumpy[i] == 1),只有被技巧覆盖时,顾客才会满意

因此,解题分为两步:

  • 第一步 :计算"基准满意人数"------所有 grumpy[i] == 0 的分钟对应的 customers[i] 之和,记为 base
  • 第二步 :使用一个长度为 minutes 的滑动窗口,计算窗口内 grumpy[i] == 1customers[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

给你两个整数 wm,以及一个整数数组 arrivals,其中 arrivals[i] 表示第 i 天到达的物品类型(天数从 1 开始编号)。

物品的管理遵循以下规则:

  • 每个到达的物品可以被 保留丢弃,物品只能在到达当天被丢弃
  • 对于每一天 i,考虑天数范围为 [max(1, i - w + 1), i](即截止到第 i 天为止最近的 w 天):
    • 对于 任何 这样的时间窗口,在被保留的到达物品中,每种类型最多只能出现 m
    • 如果在第 i 天保留该到达物品会导致其类型在该窗口中出现次数 超过 m 次,那么该物品 必须 被丢弃

返回为满足每个 w 天的窗口中每种类型最多出现 m 次,最少 需要丢弃的物品数量。

提示:

  • 1 <= arrivals.length <= 10^5
  • 1 <= arrivals[i] <= 10^5
  • 1 <= w <= arrivals.length
  • 1 <= 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)
解法二:滑动窗口 + 频率哈希表(推荐)

核心思路

  1. 维护一个长度为 w 的滑动窗口,用 lefti 分别表示窗口的左右边界
  2. 使用 Map<Integer, Integer> cnt 记录窗口内每种类型 保留物品 的数量
  3. 使用 boolean[] discarded 记录每一天的物品是否被丢弃
  4. 遍历每一天 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 = 3m = 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] == 1customers[i]
  • 这种"固定 + 可变"的拆分思路在滑动窗口问题中非常普遍

变体二:频率约束下的贪心决策(3679)

  • 窗口不仅是统计工具,更是决策的约束条件
  • 每个元素的去留必须在进入窗口的瞬间决定,无法反悔
  • 贪心策略"能留则留"的正确性依赖于同类型物品的等价性
  • 需要额外空间追踪丢弃状态,确保窗口收缩时正确维护频率表

核心要点

  1. 1052 的关键是 分离基准与增益 :先算 base(天生满意的),再用滑动窗口找最大 extra(技巧挽回的),两者互不干扰
  2. 1052 中 grumpy[i] == 0 的顾客无论技巧是否覆盖都满意,所以在滑动窗口内只需关注 grumpy[i] == 1 的分钟
  3. 3679 的 贪心正确性 源于:所有同类型物品等价,提前丢弃一个不会比延后丢弃带来更多好处
  4. 3679 中窗口收缩时必须判断左端元素是否被丢弃,只有保留的物品才计入 cnt,否则会导致频率统计错误
  5. 3679 中 cntMap.merge(key, -1, Integer::sum) 更新计数,返回 0 时及时 remove,保证 map.size()getOrDefault 的正确性
相关推荐
天若有情6731 小时前
从零搭建局域网手机遥控电脑网页项目,吃透工程化与架构设计思维
服务器·前端·数据库·算法·开源·node·工程化
凯瑟琳.奥古斯特1 小时前
力扣1367:二叉树中查找链表路径
数据结构·算法·leetcode·链表
tumu_C1 小时前
C++模板:Ret(Arg...)的相关
开发语言·c++·算法
Chase_______1 小时前
LeetCode 3 & 3090 题解:不定长滑动窗口——从“不重复“到“最多两次“,一个模板搞定频次约束问题
算法·leetcode
阿Y加油吧1 小时前
吃透 RAG 检索:纯向量短板、BM25 混合检索、RRF 融合与重排序
人工智能·leetcode
Overboom1 小时前
[BEV感知] --- IPM算法
数码相机·算法
qq_296553271 小时前
【LeetCode】最大子数组乘积:三种解法从暴力到最优
数据结构·算法·leetcode·职场和发展·动态规划·柔性数组
不知名的老吴1 小时前
关于C++中的placement new
数据结构·c++·算法
平行侠1 小时前
023Pollard-ρ 因子分解算法
数据结构·算法