Facebook Pixel

1172. Dinner Plate Stacks

Problem Description

You need to implement a system that manages dinner plates across multiple stacks. Imagine you have an infinite number of stacks arranged in a row, numbered from left to right starting at 0. Each stack can hold a maximum number of plates (the capacity).

The DinnerPlates class should support the following operations:

  1. DinnerPlates(int capacity): Initialize the system with a given maximum capacity for each stack.

  2. push(int val): Add a plate with value val to the leftmost stack that isn't full. This means you should find the first stack (with the smallest index) that has room for another plate and add the plate there.

  3. pop(): Remove and return the value from the top of the rightmost non-empty stack. If all stacks are empty, return -1.

  4. popAtStack(int index): Remove and return the value from the top of the stack at the given index. If the stack at that index is empty or doesn't exist, return -1.

The key challenge is efficiently tracking which stacks are not full (for push operations) and managing the removal of empty stacks from the right side (for pop operations). The solution uses a sorted set to maintain indices of non-full stacks, allowing for quick identification of where to push new plates. When popping from the rightmost stack, the implementation ensures that any trailing empty stacks are removed to maintain the correct "rightmost non-empty stack" for future pop operations.

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

Intuition

The main challenge in this problem is efficiently finding the leftmost non-full stack for push operations and managing the rightmost non-empty stack for pop operations. Let's think through what data structures we need.

First, we need to store the actual stacks. A simple array of stacks would work since we're numbering them sequentially from 0.

For the push operation, we need to quickly find the leftmost stack that isn't full. If we had to scan through all stacks from left to right each time, it would be inefficient. Instead, we can maintain a collection of indices of all non-full stacks. But we also need this collection to be ordered so we can always get the smallest index quickly. This suggests using an ordered set (like SortedSet in Python).

When we push to a stack, if it becomes full, we remove its index from our ordered set. When we pop from a stack, it becomes non-full, so we add its index back to the ordered set.

The tricky part comes with the pop operation. We need to pop from the rightmost non-empty stack. A naive approach would be to always keep track of the rightmost non-empty stack index, but this gets complicated when stacks become empty.

The key insight is that we can simply try to pop from the last stack in our array. If successful, great! But if that stack becomes empty after popping, we need to clean up. We should remove all trailing empty stacks from our array to maintain the invariant that the last stack in our array is the rightmost stack we care about. This cleanup step is crucial - it ensures that for the next pop operation, we can again just look at the last stack in our array.

This approach elegantly handles both the efficient lookup requirements (using the ordered set for push) and the rightmost stack management (by maintaining a clean array without trailing empty stacks).

Learn more about Stack and Heap (Priority Queue) patterns.

Solution Approach

Let's implement the solution using a stack array combined with an ordered set to efficiently manage our dinner plates system.

Data Structures

We maintain three key components:

  • capacity: The maximum capacity for each stack
  • stacks: An array of stacks to store all plates
  • not_full: A SortedSet to track indices of stacks that aren't at full capacity

Implementation Details

Initialization (__init__)

  • Store the capacity limit
  • Initialize an empty array for stacks
  • Create an empty SortedSet for tracking non-full stack indices

Push Operation (push)

When pushing a value:

  1. Check if not_full is empty:

    • If empty, no existing stacks have room, so create a new stack with the value
    • Add the new stack to stacks array
    • If capacity > 1, add this new stack's index to not_full (since it can hold more plates)
  2. If not_full has indices:

    • Get the smallest index from not_full (the leftmost non-full stack)
    • Push the value to stacks[index]
    • If the stack reaches capacity, remove its index from not_full

PopAtStack Operation (popAtStack)

When popping from a specific stack at index:

  1. Validate the index:

    • Return -1 if index is out of bounds or stacks[index] is empty
  2. Pop the value from stacks[index]

  3. Handle the aftermath:

    • If this was the last stack (index == len(stacks) - 1) and it's now empty:
      • Clean up all trailing empty stacks
      • Remove their indices from not_full
      • Keep removing empty stacks from the end until we hit a non-empty stack or run out of stacks
    • Otherwise, add index to not_full since the stack now has room
  4. Return the popped value

Pop Operation (pop)

Simply delegate to popAtStack(len(stacks) - 1) to pop from the rightmost stack.

Key Optimizations

The use of SortedSet for not_full allows:

  • O(log n) insertion and removal of indices
  • O(1) access to the minimum index (leftmost non-full stack)

The cleanup of trailing empty stacks ensures:

  • The last stack in our array is always the rightmost relevant stack
  • We don't waste memory on empty stacks at the end
  • The pop() operation remains simple

This design achieves efficient time complexity for all operations while maintaining clean state management throughout the lifetime of the data structure.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with capacity = 2 to see how the solution works:

Initial State:

  • capacity = 2
  • stacks = []
  • not_full = {}

Operation 1: push(1)

  • not_full is empty, so we create a new stack
  • stacks = [[1]]
  • Since this stack can hold one more plate (capacity is 2), add index 0 to not_full
  • not_full = {0}

Operation 2: push(2)

  • not_full = {0}, so we push to the leftmost non-full stack at index 0
  • stacks = [[1, 2]]
  • Stack 0 is now full (has 2 plates), remove it from not_full
  • not_full = {}

Operation 3: push(3)

  • not_full is empty, create a new stack at index 1
  • stacks = [[1, 2], [3]]
  • Stack 1 can hold one more plate, add it to not_full
  • not_full = {1}

Operation 4: push(4)

  • Push to stack at index 1 (the leftmost non-full)
  • stacks = [[1, 2], [3, 4]]
  • Stack 1 is now full, remove from not_full
  • not_full = {}

Operation 5: push(5)

  • Create new stack at index 2
  • stacks = [[1, 2], [3, 4], [5]]
  • not_full = {2}

Operation 6: popAtStack(0)

  • Pop from stack 0: returns 2
  • stacks = [[1], [3, 4], [5]]
  • Stack 0 now has room, add to not_full
  • not_full = {0, 2}

Operation 7: pop()

  • Calls popAtStack(2) (rightmost stack)
  • Pop from stack 2: returns 5
  • stacks = [[1], [3, 4], []]
  • Stack 2 is empty and it's the last stack, so clean up trailing empty stacks
  • Remove stack 2: stacks = [[1], [3, 4]]
  • Remove index 2 from not_full: not_full = {0}

Operation 8: push(6)

  • Push to stack 0 (leftmost non-full)
  • stacks = [[1, 6], [3, 4]]
  • Stack 0 is full, remove from not_full
  • not_full = {}

This example demonstrates:

  • How not_full tracks which stacks have room (enabling efficient push to leftmost)
  • How trailing empty stacks are cleaned up during pop operations
  • How the rightmost stack is always at len(stacks) - 1 due to cleanup

Solution Implementation

1from sortedcontainers import SortedSet
2from typing import List
3
4
5class DinnerPlates:
6    def __init__(self, capacity: int):
7        """
8        Initialize the DinnerPlates data structure.
9      
10        Args:
11            capacity: Maximum number of values each stack can hold
12        """
13        self.capacity = capacity
14        self.stacks: List[List[int]] = []  # List of stacks to store plates
15        self.not_full = SortedSet()  # Sorted set of indices of non-full stacks
16  
17    def push(self, val: int) -> None:
18        """
19        Push a value onto the leftmost stack with available capacity.
20      
21        Args:
22            val: The value to push onto a stack
23        """
24        if not self.not_full:
25            # No non-full stacks exist, create a new stack
26            self.stacks.append([val])
27          
28            # If the new stack still has room, add its index to not_full set
29            if self.capacity > 1:
30                self.not_full.add(len(self.stacks) - 1)
31        else:
32            # Push to the leftmost non-full stack
33            index = self.not_full[0]
34            self.stacks[index].append(val)
35          
36            # If stack is now full, remove it from not_full set
37            if len(self.stacks[index]) == self.capacity:
38                self.not_full.discard(index)
39  
40    def pop(self) -> int:
41        """
42        Pop a value from the rightmost non-empty stack.
43      
44        Returns:
45            The popped value, or -1 if all stacks are empty
46        """
47        return self.popAtStack(len(self.stacks) - 1)
48  
49    def popAtStack(self, index: int) -> int:
50        """
51        Pop a value from a specific stack at the given index.
52      
53        Args:
54            index: The index of the stack to pop from
55          
56        Returns:
57            The popped value, or -1 if the operation is invalid
58        """
59        # Check if index is valid and stack at index is not empty
60        if index < 0 or index >= len(self.stacks) or not self.stacks[index]:
61            return -1
62      
63        # Pop the value from the specified stack
64        val = self.stacks[index].pop()
65      
66        if index == len(self.stacks) - 1 and not self.stacks[-1]:
67            # If we popped from the last stack and it's now empty,
68            # remove all trailing empty stacks
69            while self.stacks and not self.stacks[-1]:
70                self.not_full.discard(len(self.stacks) - 1)
71                self.stacks.pop()
72        else:
73            # Stack at index now has space, add to not_full set
74            self.not_full.add(index)
75      
76        return val
77
78
79# Your DinnerPlates object will be instantiated and called as such:
80# obj = DinnerPlates(capacity)
81# obj.push(val)
82# param_2 = obj.pop()
83# param_3 = obj.popAtStack(index)
84
1class DinnerPlates {
2    // Maximum capacity for each stack
3    private int capacity;
4  
5    // List of stacks, each stack is a deque to support push/pop operations
6    private List<Deque<Integer>> stacks = new ArrayList<>();
7  
8    // TreeSet to track indices of stacks that are not at full capacity
9    // TreeSet maintains sorted order for efficient retrieval of leftmost non-full stack
10    private TreeSet<Integer> notFull = new TreeSet<>();
11
12    /**
13     * Initialize the DinnerPlates with a given capacity for each stack
14     * @param capacity Maximum number of plates each stack can hold
15     */
16    public DinnerPlates(int capacity) {
17        this.capacity = capacity;
18    }
19
20    /**
21     * Push a plate with value val to the leftmost stack that is not at full capacity
22     * If all stacks are full, create a new stack on the right
23     * @param val The value to push
24     */
25    public void push(int val) {
26        // If no non-full stacks exist, create a new stack
27        if (notFull.isEmpty()) {
28            // Add new stack and push the value
29            stacks.add(new ArrayDeque<>());
30            stacks.get(stacks.size() - 1).push(val);
31          
32            // If capacity > 1, the new stack still has room, so add to notFull set
33            if (capacity > 1) {
34                notFull.add(stacks.size() - 1);
35            }
36        } else {
37            // Push to the leftmost non-full stack
38            int index = notFull.first();
39            stacks.get(index).push(val);
40          
41            // If stack reaches capacity, remove it from notFull set
42            if (stacks.get(index).size() == capacity) {
43                notFull.pollFirst();
44            }
45        }
46    }
47
48    /**
49     * Pop a plate from the rightmost non-empty stack
50     * @return The value of the popped plate, or -1 if all stacks are empty
51     */
52    public int pop() {
53        // Delegate to popAtStack for the rightmost stack
54        return popAtStack(stacks.size() - 1);
55    }
56
57    /**
58     * Pop a plate from a specific stack at the given index
59     * @param index The index of the stack to pop from
60     * @return The value of the popped plate, or -1 if stack doesn't exist or is empty
61     */
62    public int popAtStack(int index) {
63        // Validate index and check if stack exists and is non-empty
64        if (index < 0 || index >= stacks.size() || stacks.get(index).isEmpty()) {
65            return -1;
66        }
67      
68        // Pop the value from the specified stack
69        int val = stacks.get(index).pop();
70      
71        // Handle cleanup for rightmost stacks
72        if (index == stacks.size() - 1 && stacks.get(stacks.size() - 1).isEmpty()) {
73            // Remove all empty stacks from the right
74            while (!stacks.isEmpty() && stacks.get(stacks.size() - 1).isEmpty()) {
75                notFull.remove(stacks.size() - 1);
76                stacks.remove(stacks.size() - 1);
77            }
78        } else {
79            // Mark this stack as not full since we just popped from it
80            notFull.add(index);
81        }
82      
83        return val;
84    }
85}
86
87/**
88 * Your DinnerPlates object will be instantiated and called as such:
89 * DinnerPlates obj = new DinnerPlates(capacity);
90 * obj.push(val);
91 * int param_2 = obj.pop();
92 * int param_3 = obj.popAtStack(index);
93 */
94
1class DinnerPlates {
2private:
3    int capacity;                      // Maximum capacity for each stack
4    vector<stack<int>> stacks;         // Vector of stacks to hold plates
5    set<int> notFull;                  // Set of indices of stacks that are not full (ordered)
6
7public:
8    // Constructor: Initialize with given capacity per stack
9    DinnerPlates(int capacity) {
10        this->capacity = capacity;
11    }
12
13    // Push a value onto the leftmost stack with available space
14    void push(int val) {
15        // If no non-full stacks exist, create a new stack
16        if (notFull.empty()) {
17            stacks.emplace_back(stack<int>());
18            stacks.back().push(val);
19          
20            // If the new stack still has room, add its index to notFull set
21            if (capacity > 1) {
22                notFull.insert(stacks.size() - 1);
23            }
24        } else {
25            // Push to the leftmost non-full stack
26            int index = *notFull.begin();
27            stacks[index].push(val);
28          
29            // If stack becomes full after push, remove it from notFull set
30            if (stacks[index].size() == capacity) {
31                notFull.erase(index);
32            }
33        }
34    }
35
36    // Pop from the rightmost non-empty stack
37    int pop() {
38        return popAtStack(stacks.size() - 1);
39    }
40
41    // Pop from a specific stack at given index
42    int popAtStack(int index) {
43        // Validate index and check if stack exists and is non-empty
44        if (index < 0 || index >= stacks.size() || stacks[index].empty()) {
45            return -1;
46        }
47      
48        // Pop the top element from the specified stack
49        int val = stacks[index].top();
50        stacks[index].pop();
51      
52        // Handle cleanup for rightmost stacks
53        if (index == stacks.size() - 1 && stacks[index].empty()) {
54            // Remove all trailing empty stacks from the right
55            while (!stacks.empty() && stacks.back().empty()) {
56                notFull.erase(stacks.size() - 1);
57                stacks.pop_back();
58            }
59        } else {
60            // Mark this stack as not full since we just popped from it
61            notFull.insert(index);
62        }
63      
64        return val;
65    }
66};
67
68/**
69 * Your DinnerPlates object will be instantiated and called as such:
70 * DinnerPlates* obj = new DinnerPlates(capacity);
71 * obj->push(val);
72 * int param_2 = obj->pop();
73 * int param_3 = obj->popAtStack(index);
74 */
75
1// Global variables for the DinnerPlates system
2let plateCapacity: number;
3let plateStacks: number[][];
4let nonFullStackIndices: Set<number>;
5
6// Initialize the dinner plates system with given capacity
7function DinnerPlates(capacity: number): void {
8    plateCapacity = capacity;
9    plateStacks = [];
10    nonFullStackIndices = new Set<number>();
11}
12
13// Push a value onto the leftmost non-full stack
14function push(val: number): void {
15    // If no non-full stacks exist, create a new stack
16    if (nonFullStackIndices.size === 0) {
17        plateStacks.push([val]);
18        // Mark the new stack as non-full if it has remaining capacity
19        if (plateCapacity > 1) {
20            nonFullStackIndices.add(plateStacks.length - 1);
21        }
22    } else {
23        // Find the leftmost non-full stack
24        const leftmostNonFullIndex = Math.min(...Array.from(nonFullStackIndices));
25        plateStacks[leftmostNonFullIndex].push(val);
26      
27        // Remove from non-full set if stack is now at capacity
28        if (plateStacks[leftmostNonFullIndex].length === plateCapacity) {
29            nonFullStackIndices.delete(leftmostNonFullIndex);
30        }
31    }
32}
33
34// Pop a value from the rightmost non-empty stack
35function pop(): number {
36    // Find the rightmost non-empty stack
37    for (let i = plateStacks.length - 1; i >= 0; i--) {
38        if (plateStacks[i].length > 0) {
39            return popAtStack(i);
40        }
41    }
42    return -1;
43}
44
45// Pop a value from a specific stack at the given index
46function popAtStack(index: number): number {
47    // Validate the index and check if stack exists and is non-empty
48    if (index < 0 || index >= plateStacks.length || plateStacks[index].length === 0) {
49        return -1;
50    }
51  
52    // Pop the value from the specified stack
53    const poppedValue = plateStacks[index].pop()!;
54  
55    // Clean up empty stacks from the right if we popped from the rightmost stack
56    if (index === plateStacks.length - 1 && plateStacks[index].length === 0) {
57        // Remove all trailing empty stacks
58        while (plateStacks.length > 0 && plateStacks[plateStacks.length - 1].length === 0) {
59            nonFullStackIndices.delete(plateStacks.length - 1);
60            plateStacks.pop();
61        }
62    } else {
63        // Mark the stack as non-full since we just removed an element
64        nonFullStackIndices.add(index);
65    }
66  
67    return poppedValue;
68}
69

Time and Space Complexity

Time Complexity: O(n × log n) where n is the number of operations.

  • push(val): The operation involves checking and manipulating the not_full SortedSet. Adding to or accessing the first element of a SortedSet takes O(log m) time where m is the size of the set. In the worst case, m can be proportional to the number of stacks, which grows with the number of operations. Therefore, each push operation is O(log n).

  • pop(): This calls popAtStack() with the last index, so its complexity is the same as popAtStack().

  • popAtStack(index): The main operations involve:

    • Popping from a list: O(1)
    • Adding to or discarding from the SortedSet: O(log m) where m is the size of the set
    • In the worst case when popping from the last stack, we might need to remove multiple empty stacks. Each removal involves a discard operation from the SortedSet (O(log n)) and a list pop (O(1)). While this cleanup can remove multiple stacks, amortized over all operations, each stack is removed at most once.

    The dominant operation is the SortedSet manipulation, giving O(log n) per operation.

Since we perform n operations total, and each operation has O(log n) complexity, the overall time complexity is O(n × log n).

Space Complexity: O(n) where n is the number of operations.

  • The stacks list can grow to store all pushed elements, which in the worst case is O(n).
  • The not_full SortedSet can contain at most one entry per stack, which is bounded by O(n).
  • Therefore, the total space complexity is O(n).

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

Common Pitfalls

1. Not Cleaning Up Empty Stacks After popAtStack

One of the most critical pitfalls is forgetting to remove trailing empty stacks when popping from the rightmost stack. This can lead to incorrect behavior in subsequent pop() operations.

Problem Example:

# Without proper cleanup:
plates = DinnerPlates(2)
plates.push(1)  # stacks: [[1]]
plates.push(2)  # stacks: [[1, 2]]
plates.push(3)  # stacks: [[1, 2], [3]]
plates.popAtStack(1)  # Returns 3, stacks: [[1, 2], []]
plates.pop()  # Should pop from [1, 2], but might try to pop from empty stack []

Solution: Always check if we're popping from the last stack and clean up any trailing empty stacks:

if index == len(self.stacks) - 1 and not self.stacks[-1]:
    while self.stacks and not self.stacks[-1]:
        self.not_full.discard(len(self.stacks) - 1)
        self.stacks.pop()

2. Incorrect Management of the not_full Set

Failing to properly maintain the not_full set can cause the push operation to behave incorrectly.

Common Mistakes:

  • Forgetting to add a stack's index to not_full after popping from it
  • Not removing indices when deleting trailing empty stacks
  • Adding index 0 to not_full when creating the first stack with capacity 1

Solution: Ensure proper synchronization:

# When creating a new stack:
if self.capacity > 1:  # Only add if stack can hold more than 1 plate
    self.not_full.add(len(self.stacks) - 1)

# When popping creates space:
if not (index == len(self.stacks) - 1 and not self.stacks[-1]):
    self.not_full.add(index)

# When removing trailing stacks:
self.not_full.discard(len(self.stacks) - 1)  # Remove before popping the stack

3. Edge Case: Capacity of 1

When capacity is 1, each stack can only hold one plate. A common mistake is not handling this special case properly when managing the not_full set.

Problem:

plates = DinnerPlates(1)
plates.push(1)  # Creates stack [1]
# If we incorrectly add index 0 to not_full, next push might try to add to full stack

Solution: Check capacity before adding to not_full:

if self.capacity > 1:  # Stack has room for more plates
    self.not_full.add(len(self.stacks) - 1)

4. Using Regular Set Instead of SortedSet

Using a regular set instead of SortedSet breaks the requirement to push to the leftmost non-full stack.

Problem:

# With regular set, iteration order is not guaranteed
not_full = {2, 0, 1}  # Regular set
index = min(not_full)  # O(n) operation each time

Solution: Use SortedSet for O(1) access to minimum:

from sortedcontainers import SortedSet
self.not_full = SortedSet()
index = self.not_full[0]  # O(1) access to minimum

5. Not Validating Index Bounds in popAtStack

Failing to check if the index is valid can cause runtime errors.

Problem:

plates.popAtStack(10)  # When only 3 stacks exist

Solution: Always validate before accessing:

if index < 0 or index >= len(self.stacks) or not self.stacks[index]:
    return -1
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More