Facebook Pixel

1381. Design a Stack With Increment Operation

Problem Description

You need to design a custom stack data structure that supports standard stack operations plus an increment operation.

The CustomStack class should support:

  1. Constructor CustomStack(int maxSize): Creates a stack with a maximum capacity of maxSize elements. The stack cannot hold more than maxSize elements.

  2. Push operation push(int x): Adds element x to the top of the stack. If the stack has already reached its maximum capacity (maxSize), the element should not be added.

  3. Pop operation pop(): Removes and returns the element from the top of the stack. If the stack is empty, returns -1.

  4. Increment operation increment(int k, int val): Increases the value of the bottom k elements in the stack by val. If the stack contains fewer than k elements, all elements in the stack should be incremented by val.

For example, if the stack contains [1, 2, 3] (with 3 at the top) and you call increment(2, 100), the stack becomes [101, 102, 3] because the bottom 2 elements are incremented by 100.

The solution uses an optimization technique with a lazy propagation approach. Instead of directly modifying all elements during the increment operation, it uses an additional array add to track cumulative increments at each position. This allows the increment operation to run in O(1) time. When popping an element, the accumulated increment value is applied and propagated to the element below it if necessary.

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

Intuition

The straightforward approach would be to use a simple array to simulate the stack and directly modify elements during the increment operation. However, this would make the increment operation O(k) in time complexity, as we'd need to iterate through and update k elements.

The key insight is that we can defer the actual increments until we need to access the elements. Think about it this way: if we increment the bottom k elements by some value, and then increment them again before popping, we're doing redundant work by updating the same elements multiple times.

Instead of immediately applying the increment to all k elements, we can just "mark" that an increment needs to be applied. We use a separate array add where add[i] stores the cumulative increment that should be applied to the element at position i when it's accessed.

The clever part comes with how we handle these increments during the pop operation. When we pop an element at position i, we:

  1. Apply the increment stored at add[i] to get the actual value
  2. Pass this increment down to the element below it (add[i-1] += add[i])
  3. Clear the increment at position i since we've already used it

This "passing down" mechanism ensures that any increments meant for elements below the popped element are preserved. For example, if we increment the bottom 3 elements and then pop the 3rd element, the remaining 2 elements still need to remember they should be incremented.

By using this lazy propagation technique, we achieve O(1) time complexity for all operations: push and pop work with single elements, and increment only updates a single position in the add array rather than iterating through k elements.

Learn more about Stack patterns.

Solution Approach

The implementation uses two arrays and an index pointer to efficiently manage the custom stack with lazy increment propagation.

Data Structures:

  • stk: An array of size maxSize to store the actual stack elements
  • add: An array of size maxSize to store cumulative increment values at each position
  • i: An integer pointer indicating the next position to insert an element (also represents the current size of the stack)

Implementation Details:

  1. Initialization (__init__):

    • Create stk array with maxSize capacity, initialized with zeros
    • Create add array with maxSize capacity, initialized with zeros
    • Set i = 0 to indicate the stack is empty
  2. Push Operation (push):

    • Check if there's space: if self.i < len(self.stk)
    • If yes, place element x at position stk[i]
    • Increment i by 1 to update the stack size
    • Time complexity: O(1)
  3. Pop Operation (pop):

    • Check if stack is empty: if self.i <= 0, return -1
    • Decrement i by 1 to point to the top element
    • Calculate the actual value: ans = stk[i] + add[i]
    • Propagate the increment downward: if there's an element below (i > 0), add the current increment to it: add[i-1] += add[i]
    • Clear the current position's increment: add[i] = 0
    • Return the calculated answer
    • Time complexity: O(1)
  4. Increment Operation (increment):

    • Calculate the actual position to increment: i = min(k, self.i) - 1
    • This ensures we don't exceed the current stack size
    • If i >= 0 (stack is not empty), add val to add[i]
    • This marks that all elements from bottom to position i should be incremented by val
    • Time complexity: O(1)

Key Pattern - Lazy Propagation: The solution employs lazy propagation to defer the actual increment operations. Instead of immediately updating all k elements, we only mark the topmost affected position with the increment value. When elements are popped, the increment values cascade down to the remaining elements, ensuring correctness while maintaining O(1) complexity for all operations.

For example, if we have stack [1, 2, 3] and call increment(2, 100), instead of immediately updating to [101, 102, 3], we just set add[1] = 100. When we pop elements, the actual values are computed on-the-fly by adding the base value and the accumulated increment.

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 sequence of operations on a CustomStack with maxSize = 3:

Initial State:

  • stk = [0, 0, 0]
  • add = [0, 0, 0]
  • i = 0 (stack is empty)

Operation 1: push(5)

  • Place 5 at position 0: stk = [5, 0, 0]
  • Increment i: i = 1
  • State: stk = [5, 0, 0], add = [0, 0, 0]

Operation 2: push(7)

  • Place 7 at position 1: stk = [5, 7, 0]
  • Increment i: i = 2
  • State: stk = [5, 7, 0], add = [0, 0, 0]

Operation 3: push(10)

  • Place 10 at position 2: stk = [5, 7, 10]
  • Increment i: i = 3
  • State: stk = [5, 7, 10], add = [0, 0, 0]

Operation 4: increment(2, 100)

  • Want to increment bottom 2 elements
  • Calculate position: min(2, 3) - 1 = 1
  • Set add[1] = 100 (marking that bottom 2 elements need +100)
  • State: stk = [5, 7, 10], add = [0, 100, 0]
  • Note: Actual values are [5, 107, 10] but we haven't modified stk yet

Operation 5: pop()

  • Decrement i: i = 2 (pointing to top element)
  • Calculate value: stk[2] + add[2] = 10 + 0 = 10
  • No increment to propagate since add[2] = 0
  • Clear: add[2] = 0 (already 0)
  • Return: 10
  • State: stk = [5, 7, 10], add = [0, 100, 0], i = 2

Operation 6: pop()

  • Decrement i: i = 1
  • Calculate value: stk[1] + add[1] = 7 + 100 = 107
  • Propagate increment down: add[0] = add[0] + add[1] = 0 + 100 = 100
  • Clear: add[1] = 0
  • Return: 107
  • State: stk = [5, 7, 10], add = [100, 0, 0], i = 1

Operation 7: increment(1, 50)

  • Want to increment bottom 1 element
  • Calculate position: min(1, 1) - 1 = 0
  • Add to existing increment: add[0] = 100 + 50 = 150
  • State: stk = [5, 7, 10], add = [150, 0, 0], i = 1

Operation 8: pop()

  • Decrement i: i = 0
  • Calculate value: stk[0] + add[0] = 5 + 150 = 155
  • No element below to propagate to (i = 0)
  • Clear: add[0] = 0
  • Return: 155
  • State: stk = [5, 7, 10], add = [0, 0, 0], i = 0 (empty stack)

This example demonstrates how the lazy propagation works: increments are stored in the add array and only applied when elements are popped, with the increment values cascading down to lower elements as needed.

Solution Implementation

1class CustomStack:
2    def __init__(self, maxSize: int):
3        """
4        Initialize the custom stack with a maximum size.
5      
6        Args:
7            maxSize: Maximum number of elements the stack can hold
8        """
9        # Pre-allocate arrays for stack elements and increment values
10        self.stack = [0] * maxSize
11        # Lazy propagation array to store pending increments
12        self.increment_values = [0] * maxSize
13        # Current size of the stack (points to next empty position)
14        self.current_size = 0
15
16    def push(self, x: int) -> None:
17        """
18        Push an element onto the stack if not full.
19      
20        Args:
21            x: Element to push onto the stack
22        """
23        # Only push if stack is not full
24        if self.current_size < len(self.stack):
25            self.stack[self.current_size] = x
26            self.current_size += 1
27
28    def pop(self) -> int:
29        """
30        Pop and return the top element from the stack.
31      
32        Returns:
33            The top element value (with accumulated increments), or -1 if stack is empty
34        """
35        # Return -1 if stack is empty
36        if self.current_size <= 0:
37            return -1
38      
39        # Move pointer to the top element
40        self.current_size -= 1
41      
42        # Calculate the actual value including any pending increments
43        result = self.stack[self.current_size] + self.increment_values[self.current_size]
44      
45        # Propagate the increment value to the element below (if exists)
46        if self.current_size > 0:
47            self.increment_values[self.current_size - 1] += self.increment_values[self.current_size]
48      
49        # Clear the increment value for this position
50        self.increment_values[self.current_size] = 0
51      
52        return result
53
54    def increment(self, k: int, val: int) -> None:
55        """
56        Increment the bottom k elements of the stack by val.
57        Uses lazy propagation for O(1) time complexity.
58      
59        Args:
60            k: Number of bottom elements to increment
61            val: Value to add to each element
62        """
63        # Find the index of the k-th element (or top if stack has fewer than k elements)
64        target_index = min(k, self.current_size) - 1
65      
66        # Apply increment only if there are elements to increment
67        if target_index >= 0:
68            # Add to the increment array at the topmost affected position
69            # This value will propagate down during pop operations
70            self.increment_values[target_index] += val
71
1class CustomStack {
2    private int[] stack;           // Array to store stack elements
3    private int[] incrementValues;  // Array to store lazy increment values for each position
4    private int top;                // Current top index of the stack (also represents size)
5
6    /**
7     * Initializes the custom stack with a maximum capacity.
8     * @param maxSize The maximum number of elements the stack can hold
9     */
10    public CustomStack(int maxSize) {
11        stack = new int[maxSize];
12        incrementValues = new int[maxSize];
13        top = 0;
14    }
15
16    /**
17     * Pushes an element onto the stack if it's not full.
18     * @param x The element to be pushed
19     */
20    public void push(int x) {
21        if (top < stack.length) {
22            stack[top] = x;
23            top++;
24        }
25    }
26
27    /**
28     * Pops and returns the top element from the stack.
29     * Returns -1 if the stack is empty.
30     * @return The popped element or -1 if stack is empty
31     */
32    public int pop() {
33        // Check if stack is empty
34        if (top <= 0) {
35            return -1;
36        }
37      
38        // Decrement top to point to the actual top element
39        top--;
40      
41        // Calculate the actual value including any pending increments
42        int result = stack[top] + incrementValues[top];
43      
44        // Propagate the increment value to the element below (if exists)
45        if (top > 0) {
46            incrementValues[top - 1] += incrementValues[top];
47        }
48      
49        // Clear the increment value for the popped position
50        incrementValues[top] = 0;
51      
52        return result;
53    }
54
55    /**
56     * Increments the bottom k elements of the stack by val.
57     * If there are fewer than k elements, increments all elements.
58     * @param k The number of bottom elements to increment
59     * @param val The value to increment by
60     */
61    public void increment(int k, int val) {
62        if (top > 0) {
63            // Apply increment to the minimum of k and current stack size
64            // Index is (min - 1) because we're using 0-based indexing
65            int targetIndex = Math.min(top, k) - 1;
66            incrementValues[targetIndex] += val;
67        }
68    }
69}
70
71/**
72 * Your CustomStack object will be instantiated and called as such:
73 * CustomStack obj = new CustomStack(maxSize);
74 * obj.push(x);
75 * int param_2 = obj.pop();
76 * obj.increment(k,val);
77 */
78
1class CustomStack {
2public:
3    /**
4     * Constructor: Initialize a custom stack with a maximum size
5     * @param maxSize: The maximum capacity of the stack
6     */
7    CustomStack(int maxSize) {
8        stack.resize(maxSize);
9        incrementValues.resize(maxSize);
10        topIndex = 0;
11    }
12  
13    /**
14     * Push an element onto the stack if not full
15     * @param x: The value to push onto the stack
16     */
17    void push(int x) {
18        if (topIndex < stack.size()) {
19            stack[topIndex++] = x;
20        }
21    }
22  
23    /**
24     * Pop and return the top element from the stack
25     * @return: The top element value (with accumulated increments), or -1 if stack is empty
26     */
27    int pop() {
28        // Check if stack is empty
29        if (topIndex <= 0) {
30            return -1;
31        }
32      
33        // Decrement top index and calculate the actual value with increments
34        int result = stack[--topIndex] + incrementValues[topIndex];
35      
36        // Propagate the increment value to the element below (if exists)
37        if (topIndex > 0) {
38            incrementValues[topIndex - 1] += incrementValues[topIndex];
39        }
40      
41        // Reset the increment value for the popped position
42        incrementValues[topIndex] = 0;
43      
44        return result;
45    }
46  
47    /**
48     * Increment the bottom k elements of the stack by val
49     * @param k: Number of bottom elements to increment
50     * @param val: The value to add to each of the bottom k elements
51     */
52    void increment(int k, int val) {
53        // Only increment if stack is not empty
54        if (topIndex > 0) {
55            // Apply increment to the min(k, topIndex)-th element
56            // This uses lazy propagation - the increment is stored and applied during pop
57            incrementValues[min(k, topIndex) - 1] += val;
58        }
59    }
60  
61private:
62    vector<int> stack;           // Array to store stack elements
63    vector<int> incrementValues;  // Array to store lazy increment values for each position
64    int topIndex;                // Current top index of the stack (number of elements)
65};
66
67/**
68 * Your CustomStack object will be instantiated and called as such:
69 * CustomStack* obj = new CustomStack(maxSize);
70 * obj->push(x);
71 * int param_2 = obj->pop();
72 * obj->increment(k,val);
73 */
74
1// Stack array to store elements
2let stack: number[] = [];
3// Array to store lazy increment values for each position
4let incrementArray: number[] = [];
5// Current index pointing to the next available position in stack
6let currentIndex: number = 0;
7// Maximum size of the stack
8let maxStackSize: number = 0;
9
10/**
11 * Initialize the custom stack with a maximum size
12 * @param maxSize - Maximum number of elements the stack can hold
13 */
14function initializeStack(maxSize: number): void {
15    maxStackSize = maxSize;
16    stack = new Array(maxSize).fill(0);
17    incrementArray = new Array(maxSize).fill(0);
18    currentIndex = 0;
19}
20
21/**
22 * Push an element onto the stack if not full
23 * @param x - The value to push onto the stack
24 */
25function push(x: number): void {
26    // Only push if stack hasn't reached maximum capacity
27    if (currentIndex < maxStackSize) {
28        stack[currentIndex] = x;
29        currentIndex++;
30    }
31}
32
33/**
34 * Pop and return the top element from the stack
35 * @returns The top element value (with accumulated increments) or -1 if stack is empty
36 */
37function pop(): number {
38    // Return -1 if stack is empty
39    if (currentIndex <= 0) {
40        return -1;
41    }
42  
43    // Decrement index to point to the top element
44    currentIndex--;
45  
46    // Calculate the final value by adding the stored increment value
47    const result = stack[currentIndex] + incrementArray[currentIndex];
48  
49    // Propagate the increment value to the element below (if exists)
50    if (currentIndex > 0) {
51        incrementArray[currentIndex - 1] += incrementArray[currentIndex];
52    }
53  
54    // Reset the increment value for this position
55    incrementArray[currentIndex] = 0;
56  
57    return result;
58}
59
60/**
61 * Increment the bottom k elements of the stack by val
62 * @param k - Number of bottom elements to increment
63 * @param val - Value to add to each of the bottom k elements
64 */
65function increment(k: number, val: number): void {
66    // Only increment if stack is not empty
67    if (currentIndex > 0) {
68        // Apply increment to the minimum of k and current stack size
69        // Using lazy propagation: store increment at the topmost affected position
70        const targetIndex = Math.min(currentIndex, k) - 1;
71        incrementArray[targetIndex] += val;
72    }
73}
74

Time and Space Complexity

Time Complexity: O(1) for all operations (push, pop, and increment)

  • push: Simply assigns a value to self.stk[self.i] and increments the index, which takes constant time.
  • pop: Performs a fixed number of operations - decrements the index, calculates the result by adding self.stk[self.i] + self.add[self.i], potentially updates self.add[self.i - 1], and resets self.add[self.i]. All these operations take constant time.
  • increment: Calculates min(k, self.i) - 1 and updates a single element in the self.add array. This is a constant time operation regardless of the value of k.

The key insight is that the increment operation uses lazy propagation. Instead of updating all k elements immediately, it only marks the increment at position i in the self.add array. The actual increment is applied when elements are popped, and the increment value is propagated down the stack one level at a time during each pop operation.

Space Complexity: O(n) where n is the maxSize parameter

  • The implementation uses two arrays: self.stk and self.add, each of size maxSize.
  • The space usage is 2 * maxSize = O(maxSize) = O(n).
  • Additionally, there's a constant amount of space for the index variable self.i.

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

Common Pitfalls

1. Forgetting to Propagate Increments During Pop

One of the most common mistakes is forgetting to propagate the increment value to the element below when popping. This breaks the lazy propagation mechanism.

Incorrect Implementation:

def pop(self) -> int:
    if self.current_size <= 0:
        return -1
  
    self.current_size -= 1
    result = self.stack[self.current_size] + self.increment_values[self.current_size]
  
    # MISTAKE: Forgetting to propagate increment to element below
    self.increment_values[self.current_size] = 0  # Only clearing without propagation
  
    return result

Correct Implementation:

def pop(self) -> int:
    if self.current_size <= 0:
        return -1
  
    self.current_size -= 1
    result = self.stack[self.current_size] + self.increment_values[self.current_size]
  
    # Propagate increment to the element below before clearing
    if self.current_size > 0:
        self.increment_values[self.current_size - 1] += self.increment_values[self.current_size]
  
    self.increment_values[self.current_size] = 0
  
    return result

2. Off-by-One Error in Increment Operation

Another frequent mistake is incorrectly calculating the index for the increment operation, especially when dealing with 1-based k versus 0-based indexing.

Incorrect Implementation:

def increment(self, k: int, val: int) -> None:
    # MISTAKE: Using k directly as index without adjustment
    target_index = min(k, self.current_size)
  
    if target_index >= 0:
        self.increment_values[target_index] += val  # This would increment k+1 elements!

Correct Implementation:

def increment(self, k: int, val: int) -> None:
    # Correctly subtract 1 to convert from count to index
    target_index = min(k, self.current_size) - 1
  
    if target_index >= 0:
        self.increment_values[target_index] += val

3. Not Clearing Increment Values After Pop

Failing to reset the increment value at a popped position can cause incorrect values if that position is reused later.

Incorrect Implementation:

def pop(self) -> int:
    if self.current_size <= 0:
        return -1
  
    self.current_size -= 1
    result = self.stack[self.current_size] + self.increment_values[self.current_size]
  
    if self.current_size > 0:
        self.increment_values[self.current_size - 1] += self.increment_values[self.current_size]
  
    # MISTAKE: Not clearing the increment value
    # self.increment_values[self.current_size] = 0  # Missing this line!
  
    return result

This would cause issues when a new element is pushed to the same position later, as it would inherit the old increment value.

4. Incorrect Boundary Check in Increment

Not properly handling the case when the stack is empty or when k is 0.

Incorrect Implementation:

def increment(self, k: int, val: int) -> None:
    # MISTAKE: Not checking if the result is negative
    target_index = min(k, self.current_size) - 1
    self.increment_values[target_index] += val  # Could access index -1!

Correct Implementation:

def increment(self, k: int, val: int) -> None:
    target_index = min(k, self.current_size) - 1
  
    # Proper boundary check
    if target_index >= 0:
        self.increment_values[target_index] += val

Prevention Tips:

  1. Always trace through the lazy propagation logic with small examples
  2. Verify that increment values are properly propagated and cleared
  3. Test edge cases: empty stack, single element, k larger than stack size
  4. Use consistent naming and clear comments to track the propagation flow
  5. Remember that the increment array represents cumulative increments that apply to all elements from bottom up to that index
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