Facebook Pixel

3679. Minimum Discards to Balance Inventory

MediumArrayHash TableCountingSliding WindowSimulation
LeetCode ↗

Problem Description

You are given two integers w and m, and an integer array arrivals, where arrivals[i] represents the type of item arriving on day i (days are 1-indexed).

Items follow these management rules:

  • When an item arrives, you must decide immediately whether to keep it or discard it. An item can only be discarded on the exact day it arrives — you cannot remove it later.
  • For every day i, look at the sliding window of the last w days: [max(1, i - w + 1), i].
    • Within any such window, each item type can appear at most m times among the kept items.
    • If keeping the item arriving on day i would push its type's count in the window above m, then that item must be discarded.

Your task is to return the minimum number of arrivals that need to be discarded so that every w-day window contains at most m kept items of each type.

In other words, as items arrive one by one, you maintain a sliding window of size w. Whenever a newly arriving item would cause its type to exceed the limit m within the current window, you have no choice but to throw it away. You need to count how many items end up being discarded.

Key points to understand:

  1. The decision is made online: each item is processed in order, and once kept, it stays in the window until it slides out naturally after w days.
  2. Discarded items do not count toward the window's type counts — only kept items matter.
  3. The greedy strategy of keeping an item whenever possible is optimal: keeping an earlier item never hurts, because it leaves the window sooner than any later item of the same type would, freeing up capacity earlier.

Example walkthrough:

Suppose arrivals = [1, 2, 1, 1], w = 3, m = 1:

  • Day 1: type 1 arrives. Window [1,1] has zero kept items of type 1, so keep it.
  • Day 2: type 2 arrives. Window [1,2] has no type 2, so keep it.
  • Day 3: type 1 arrives. Window [1,3] already contains one kept type 1 (from day 1), so this item must be discarded.
  • Day 4: type 1 arrives. Window [2,4] — the item from day 1 has slid out, and the day-3 item was discarded, so type 1 count is 0. Keep it.

Total discarded: 1.

The solution simulates this process with a counter cnt tracking kept items per type in the current window, and a marked array recording which items were kept, so counts can be correctly decremented when items slide out of the window.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Sliding Window?

This problem maps to Sliding Window through a short path in the full flowchart.

Subarray/substringproblem?yesMaintain avalidwindow?yesSliding Window

A fixed-size sliding window tracks kept item counts per type; each arrival is either kept or discarded greedily depending on whether the window already holds the per-type limit.

Open in Flowchart

Intuition

The first thing to notice is that the problem statement itself almost forces our hand: when an arriving item would make its type exceed m in the current window, it must be discarded — there's no decision to make in that moment. So the only real question is: should we ever voluntarily discard an item that could legally be kept, hoping it saves more items later?

The answer is no, and here's the key insight why.

Consider two items of the same type — one arriving earlier, one arriving later. The earlier item entered the window sooner, which means it will also leave the window sooner. If we're choosing which occurrences of a type to keep, an earlier item "occupies" a slot for less remaining time than a later one would. So keeping items as early as possible frees up capacity at the earliest possible moment, which can never be worse for future arrivals.

Put differently: suppose an optimal solution discards some item A that could have been kept, and later keeps an item B of the same type. We can always swap the choice — keep A and discard B instead — without ever violating the window constraint, because A exits every window no later than B does. The total number of discards stays the same. This exchange argument tells us the greedy strategykeep every item unless the rules force a discard — is optimal.

Once we trust the greedy approach, the implementation becomes a straightforward simulation with a sliding window:

  • We need to know, for each arriving item, how many kept items of the same type currently sit in the window [i - w + 1, i]. A hash map cnt tracks this.
  • As the window slides forward, the item from day i - w falls out. But we must be careful: only items that were actually kept should be subtracted from the counts. Discarded items never contributed to cnt in the first place. This is exactly why we maintain the marked array — marked[j] = 1 means the item on day j was kept, so when day j exits the window we decrement cnt[arrivals[j]] by marked[j] (subtracting 0 for discarded items, 1 for kept ones).
  • For the current item x: if cnt[x] >= m, keeping it would exceed the limit, so we discard it and increment the answer. Otherwise, we keep it, set marked[i] = 1, and bump cnt[x].

Each item is processed exactly once with O(1) work, giving a clean O(n) time solution with O(n) space for the marked array.

Pattern Learn more about Sliding Window patterns.

Solution Approach

if i >= w: cnt[arrivals[i - w]] -= marked[i - w]


The subtlety here is the multiplication-by-`marked` trick: if the item on day `i - w` was **discarded**, `marked[i - w]` is `0` and the count stays unchanged — exactly right, since discarded items never counted in the first place. If it was **kept**, we subtract `1`.

**Step 2: Check the constraint for the current item.**

If the window already contains `m` kept items of type `x`, keeping one more would exceed the limit, so the item must be discarded:

```python
if cnt[x] >= m:
    ans += 1

Note that we do not set marked[i], and cnt[x] is not incremented — a discarded item leaves no trace in the window state.

Step 3: Otherwise, keep the item.

else:
    marked[i] = 1
    cnt[x] += 1

We record that the item was kept and increment its type count, so future days within the next w - 1 days will see it in their windows.

Step 4: Return the result.

After processing all arrivals, ans holds the minimum number of discards, which is guaranteed minimal by the greedy exchange argument established earlier.

Trace Example

With arrivals = [1, 2, 1, 1], w = 3, m = 1:

ixSlide outcnt[x] beforeActioncnt afterans
010keep{1: 1}0
120keep{1: 1, 2: 1}0
211 ≥ mdiscard{1: 1, 2: 1}1
31day 0 (kept) → cnt[1] -= 10keep{1: 1, 2: 1}1

The answer is 1.

Complete Code

class Solution:
    def minArrivalsToDiscard(self, arrivals: List[int], w: int, m: int) -> int:
        cnt = Counter()
        n = len(arrivals)
        marked = [0] * n
        ans = 0
        for i, x in enumerate(arrivals):
            if i >= w:
                cnt[arrivals[i - w]] -= marked[i - w]
            if cnt[x] >= m:
                ans += 1
            else:
                marked[i] = 1
                cnt[x] += 1
        return ans

Example Walkthrough

Let's trace the algorithm on arrivals = [1, 1, 1, 1, 1, 1], w = 3, m = 2 — six items of the same type, a window of size 3, and at most 2 kept items per type allowed in any window. This example is small but exercises every branch of the code, including the subtle marked trick.

We maintain:

  • cnt — the number of kept items of each type in the current window,
  • marked[i]1 if the item on day i was kept, 0 otherwise,
  • ans — the number of discards.
ixSlides out (i - w)Effect on cnt[1]cnt[1] before checkDecisionmarked[i]cnt[1] afterans
01— (window not full)00 < 2keep110
1111 < 2keep120
2122 ≥ 2discard021
31index 0, marked[0] = 12 - 1 = 111 < 2keep121
41index 1, marked[1] = 12 - 1 = 111 < 2keep121
51index 2, marked[2] = 02 - 0 = 2 (unchanged)22 ≥ 2discard022

Step-by-step commentary:

  • Days 1–2 (i = 0, 1): The window has fewer than m = 2 kept items of type 1, so both are kept greedily. Remember the exchange argument — keeping items as early as possible is never worse, since earlier items exit the window sooner.
  • Day 3 (i = 2): The window [1, 3] already holds 2 kept items of type 1. Keeping this one would exceed m, so it is forced out. Crucially, it leaves no trace: marked[2] stays 0 and cnt[1] is not incremented.
  • Days 4–5 (i = 3, 4): The window slides. The items from days 1 and 2 were kept (marked = 1), so each time cnt[1] correctly drops by 1, freeing a slot. Both new arrivals fit and are kept.
  • Day 6 (i = 5): Here is where the cnt[arrivals[i-w]] -= marked[i-w] trick pays off. The item sliding out is from day 3 — but it was discarded, so marked[2] = 0 and the subtraction does nothing. The window [4, 6] still genuinely contains 2 kept items (days 4 and 5), so the new arrival must also be discarded. Without the marked array, we would have wrongly decremented cnt[1] to 1 and kept this item, violating the constraint.

Final answer: 2 — the items arriving on days 3 and 6 are discarded.

Sanity check against the rules: the kept items land on days 1, 2, 4, 5. Every window of 3 consecutive days — [1,3], [2,4], [3,5], [4,6] — contains at most 2 kept items of type 1, and the greedy exchange argument guarantees no strategy can do better than 2 discards.

Solution Implementation

1from collections import Counter
2from typing import List
3
4
5class Solution:
6    def minArrivalsToDiscard(self, arrivals: List[int], w: int, m: int) -> int:
7        # Counter of values currently kept inside the sliding window of size w
8        window_count = Counter()
9        n = len(arrivals)
10        # kept[i] = 1 if arrivals[i] was kept (not discarded), else 0
11        kept = [0] * n
12        # Total number of discarded arrivals
13        discarded = 0
14
15        for i, value in enumerate(arrivals):
16            # Slide the window: remove the element leaving the window,
17            # but only if it was actually kept earlier
18            if i >= w:
19                window_count[arrivals[i - w]] -= kept[i - w]
20
21            if window_count[value] >= m:
22                # Keeping this arrival would exceed the limit m
23                # within the current window, so discard it
24                discarded += 1
25            else:
26                # Keep this arrival and record it in the window counter
27                kept[i] = 1
28                window_count[value] += 1
29
30        return discarded
31
1class Solution {
2    public int minArrivalsToDiscard(int[] arrivals, int w, int m) {
3        // Map to track the count of each value among the KEPT items
4        // within the current sliding window of size w
5        Map<Integer, Integer> countMap = new HashMap<>();
6        int n = arrivals.length;
7        // kept[i] = 1 if arrivals[i] was kept, 0 if it was discarded
8        int[] kept = new int[n];
9        // Total number of discarded arrivals
10        int discardCount = 0;
11
12        for (int i = 0; i < n; i++) {
13            int current = arrivals[i];
14
15            // If the window is full, remove the contribution of the element
16            // that slides out of the window (only if it was kept)
17            if (i >= w) {
18                int outgoing = arrivals[i - w];
19                countMap.merge(outgoing, -kept[i - w], Integer::sum);
20            }
21
22            // If the current value already appears m times in the window,
23            // it must be discarded
24            if (countMap.getOrDefault(current, 0) >= m) {
25                discardCount++;
26            } else {
27                // Otherwise, keep it and update its count in the window
28                kept[i] = 1;
29                countMap.merge(current, 1, Integer::sum);
30            }
31        }
32
33        return discardCount;
34    }
35}
36
1class Solution {
2public:
3    int minArrivalsToDiscard(vector<int>& arrivals, int w, int m) {
4        // Count of each value currently kept within the sliding window
5        unordered_map<int, int> keptCount;
6        int n = arrivals.size();
7
8        // kept[i] = 1 if arrivals[i] is kept (not discarded), 0 otherwise
9        vector<int> kept(n, 0);
10
11        // Total number of discarded arrivals
12        int discarded = 0;
13
14        for (int i = 0; i < n; i++) {
15            int value = arrivals[i];
16
17            // The element leaving the window is arrivals[i - w].
18            // Remove its contribution only if it was kept.
19            if (i >= w) {
20                keptCount[arrivals[i - w]] -= kept[i - w];
21            }
22
23            if (keptCount[value] >= m) {
24                // Keeping this arrival would exceed the limit m
25                // within the current window, so discard it.
26                discarded++;
27            } else {
28                // Keep this arrival and update its count.
29                kept[i] = 1;
30                keptCount[value] += 1;
31            }
32        }
33
34        return discarded;
35    }
36};
37
1/**
2 * Counts the minimum number of arrivals that must be discarded so that,
3 * within any sliding window of size `w`, each distinct value appears
4 * at most `m` times (kept arrivals only).
5 *
6 * Greedy strategy: scan from left to right; keep an arrival whenever the
7 * current window still has capacity for its value, otherwise discard it.
8 *
9 * @param arrivals - The sequence of arriving item values.
10 * @param w - The size of the sliding window.
11 * @param m - The maximum allowed occurrences of each value per window.
12 * @returns The number of discarded arrivals.
13 */
14function minArrivalsToDiscard(arrivals: number[], w: number, m: number): number {
15    // Frequency of each value among the KEPT arrivals inside the current window
16    const keptCount = new Map<number, number>();
17    const n = arrivals.length;
18
19    // kept[i] = 1 if arrivals[i] was kept, 0 if it was discarded
20    const kept: number[] = new Array<number>(n).fill(0);
21
22    // Total number of discarded arrivals
23    let discarded = 0;
24
25    for (let i = 0; i < n; i++) {
26        const value = arrivals[i];
27
28        // Slide the window: remove the element leaving the window (index i - w).
29        // Only subtract if that element was actually kept.
30        if (i >= w) {
31            const expired = arrivals[i - w];
32            keptCount.set(expired, (keptCount.get(expired) || 0) - kept[i - w]);
33        }
34
35        if ((keptCount.get(value) || 0) >= m) {
36            // The window already holds `m` kept copies of this value: discard it.
37            discarded++;
38        } else {
39            // There is still capacity: keep this arrival and update its count.
40            kept[i] = 1;
41            keptCount.set(value, (keptCount.get(value) || 0) + 1);
42        }
43    }
44
45    return discarded;
46}
47

Time and Space Complexity

  • Time complexity: O(n), where n is the length of the array arrivals. The code iterates through the array exactly once, and each operation inside the loop — updating the counter cnt, checking cnt[x] >= m, and marking marked[i] — takes O(1) time on average (hash table operations).

  • Space complexity: O(n), where n is the length of the array arrivals. The marked array requires O(n) space, and the counter cnt stores at most O(min(n, w)) distinct keys within the sliding window, which is bounded by O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Decrementing the count for discarded items when the window slides

The most frequent mistake is to unconditionally subtract 1 when an element leaves the window:

# WRONG
if i >= w:
    window_count[arrivals[i - w]] -= 1

This corrupts the window state. A discarded item was never added to window_count, so subtracting 1 when it exits the window makes the count for that type artificially negative. Later arrivals of the same type then pass the cnt[x] >= m check when they should fail, causing kept items to exceed the per-type limit and producing an answer that is too small.

Concrete failure: arrivals = [1, 1, 1, 1], w = 2, m = 1.

  • Day 1: keep 1cnt[1] = 1
  • Day 2: cnt[1] = 1 ≥ m → discard (correct so far)
  • Day 3: window slides, wrong code does cnt[1] -= 1 for the kept day-1 item — fine here, cnt[1] = 0, keep it.
  • Day 4: window slides, wrong code does cnt[1] -= 1 for the discarded day-2 item → cnt[1] = 0 instead of 1. The day-4 item is kept, but now days 3 and 4 both hold type 1 inside the same window of size 2 — a constraint violation. The wrong answer is 1; the correct answer is 2.

Fix: track which items were actually kept, and only decrement for those:

# CORRECT
if i >= w:
    window_count[arrivals[i - w]] -= kept[i - w]   # subtracts 0 for discarded items

Pitfall 2: Off-by-one errors in the window boundary

The window covering day i (0-indexed) is [i - w + 1, i], so the element that exits when processing index i is arrivals[i - w]. Writing i > w instead of i >= w, or removing arrivals[i - w + 1], shifts the window by one day. This either keeps an expired item in the count one day too long (over-discarding) or evicts it one day too early (under-discarding, violating the constraint). Always verify with the smallest case: when w = 1, processing index i = 1 must evict index 0.

Pitfall 3: Counting discarded items toward the window

Symmetric to Pitfall 1 — incrementing cnt[x] (or setting kept[i] = 1) even on the discard branch:

# WRONG
if window_count[value] >= m:
    discarded += 1
    window_count[value] += 1   # discarded items must leave no trace!

Discarded items do not occupy capacity. Inflating the count makes future same-type arrivals get discarded unnecessarily, yielding an answer that is too large.

Pitfall 4: Using > m instead of >= m in the check

The condition asks whether keeping one more item would push the count above m. If the window already holds m items of type x, adding another gives m + 1 > m, so the test must be cnt[x] >= m. Writing cnt[x] > m allows m + 1 kept items per window.

Pitfall 5: Trying a "smarter" non-greedy strategy

Some attempt to discard an earlier kept item retroactively, or to skip keeping an item "to save room" for future arrivals. Both violate the problem rules (decisions are immediate and irrevocable) and are also unnecessary: keeping an item as early as possible is provably optimal, since an earlier item exits the window no later than any subsequent item of the same type, so it never blocks more future arrivals than the alternative. The straightforward greedy simulation is both required and optimal.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More