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:
-
DinnerPlates(int capacity)
: Initialize the system with a given maximum capacity for each stack. -
push(int val)
: Add a plate with valueval
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. -
pop()
: Remove and return the value from the top of the rightmost non-empty stack. If all stacks are empty, return-1
. -
popAtStack(int index)
: Remove and return the value from the top of the stack at the givenindex
. 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.
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 stackstacks
: An array of stacks to store all platesnot_full
: ASortedSet
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:
-
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 tonot_full
(since it can hold more plates)
-
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
- Get the smallest index from
PopAtStack Operation (popAtStack
)
When popping from a specific stack at index
:
-
Validate the index:
- Return
-1
ifindex
is out of bounds orstacks[index]
is empty
- Return
-
Pop the value from
stacks[index]
-
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
tonot_full
since the stack now has room
- If this was the last stack (
-
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 EvaluatorExample 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 stackstacks = [[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 0stacks = [[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 1stacks = [[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 thenot_full
SortedSet. Adding to or accessing the first element of a SortedSet takesO(log m)
time wherem
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 isO(log n)
. -
pop()
: This callspopAtStack()
with the last index, so its complexity is the same aspopAtStack()
. -
popAtStack(index)
: The main operations involve:- Popping from a list:
O(1)
- Adding to or discarding from the SortedSet:
O(log m)
wherem
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. - Popping from a list:
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 isO(n)
. - The
not_full
SortedSet can contain at most one entry per stack, which is bounded byO(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
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!