DSA sheet · Stack · Stack-based design pattern

Design Stack With Increment

The last problem of the stack-design pattern. We build a stack with a size limit and one extra method: increment(k, val), which adds val to the bottom k items. The teacher first stores the stack in a fixed array with an index pointer and does the increment with a loop (O(k)). Then she makes increment O(1) with a lazy "pending increment" array: instead of touching every item now, she leaves a note at one spot and passes it down only when a pop actually reaches it. This "do the work later, only when it's needed" idea shows up in many advanced data structures.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run (operation-sequence table) → ⑨ complexity & remember

Part 0 · Before starting

What is a stack?

A pile of plates: add and remove only at the top. Last in, first out (LIFO). push(x) puts x on top, pop() removes and returns the top, peek reads it.

a Python list is a ready-made stack
st = []
st.append(1); st.append(2)   # push   -> [1, 2]   (right end = top)
st[-1]                       # peek   -> 2
st.pop()                     # pop    -> 2
if st: st.pop()              # ALWAYS check "not empty" before pop() or [-1]

We draw stacks left = bottom, right = top. The bottom k items are the first k from the left (the oldest k).

A stack inside a fixed-size array

If the stack has a maximum size, we can create an array of exactly that size once and keep an integer idx = "position of the top item". The stack is the part arr[0..idx].

questionanswer with idx
empty?idx == -1 (no item yet, so the top is "before position 0")
full?idx == maxSize - 1 (positions are 0-based, so the last one is maxSize − 1)
how many items?idx + 1
push xidx += 1; arr[idx] = x
popx = arr[idx]; idx -= 1

Unlike a plain stack, an array lets us reach any position by index, including the bottom. That's what makes increment possible.

Lazy work and "amortised" vs worst-case cost

Lazy means: don't do a job until someone actually needs its result. If you're told "add 100 to the bottom 1000 items", but nobody ever looks at 999 of them, updating all 1000 right away was wasted work. Instead, leave a small note and apply it later, item by item, as each one is popped.

Amortised O(1) means some calls are slow but the average is constant; worst-case O(1) means every single call is fast. Part B gets worst-case O(1) for every method. Each pop does at most one extra addition (passing the note down), which can't pile up.

Part A · Array + index, increment with a loop

LeetCode 1381

1The question in simple words

Design CustomStack(maxSize):

Example: maxSize 3: push 1, push 2, pop → 2, push 2, push 3, push 4 (ignored, full), increment(5, 100), increment(2, 100), pop → 103, pop → 202, pop → 201, pop → −1.

2What the constraints tell us

3Intuition

Since the stack has a fixed maximum size, store it in an array of that size with an index pointer to the top. push/pop just move the pointer. For increment, the bottom k items are simply positions 0 … k−1 of the array, so loop over them and add val. Clip the loop to the number of items present.

4Building the logic from the example

push with a size limit

push 1, push 2: stack [1, 2]. pop → 2. push 2, push 3: stack [1, 2, 3]. Now push 4: the stack already has 3 items and maxSize is 3. No room. So push must first check "is it full?" and if yes, just return without doing anything.

Doubt: why does the teacher use an idx variable instead of asking the stack its size each time?
→ Both push (full?) and pop (empty?) need the size on every call. Keeping idx updated as we go answers both questions instantly: full is idx == maxSize − 1, empty is idx == −1. Starting idx at −1 is what makes "empty" and "next push goes to position 0" both work.

increment(5, 100) with only 3 items

Fewer than 5 items → add to all of them. In general the number of items to update is limit = min(k, idx + 1). Here min(5, 3) = 3 → [101, 102, 103].

Doubt: why idx + 1 and not idx?
→ k is a count of items, and idx is a position (0-based). With items at positions 0, 1, 2, idx is 2 but the count is 3. To compare a count with a count, use idx + 1.

increment(2, 100)

min(2, 3) = 2 → positions 0 and 1: [201, 202, 103].

pop four times

103, then 202, then 201. Now idx = −1, so the 4th pop returns −1.

5Approach steps

  1. Create st = [0] * maxSize and idx = −1.
  2. push(x): if idx == maxSize − 1 return; else idx += 1, st[idx] = x.
  3. pop(): if idx == −1 return −1; else read st[idx], idx −= 1, return it.
  4. increment(k, val): limit = min(k, idx + 1); add val to st[0 … limit−1].

6Code (Python)

Part A: increment with a loop
class CustomStack:
    def __init__(self, maxSize):
        self.maxSize = maxSize
        self.st = [0] * maxSize     # fixed-size array
        self.idx = -1               # position of the top item; -1 = empty

    def push(self, x):
        if self.idx == self.maxSize - 1:    # full: ignore
            return
        self.idx += 1
        self.st[self.idx] = x

    def pop(self):
        if self.idx == -1:                  # empty
            return -1
        val = self.st[self.idx]
        self.idx -= 1
        return val

    def increment(self, k, val):
        limit = min(k, self.idx + 1)        # can't touch more items than exist
        for i in range(limit):
            self.st[i] += val

7Code line by line

linewhat it means
self.st = [0] * maxSizeReserve all the slots once. (The teacher uses an int array in Java; a Python list of zeros plays the same role.)
self.idx = -1No top item yet. The first push will move idx to 0.
if self.idx == self.maxSize - 1: returnThe top is already at the last slot: the stack is full, so the push is ignored.
self.idx += 1 self.st[self.idx] = xMove the top up one slot first, then write x there.
if self.idx == -1: return -1Empty stack: the problem says return −1.
val = self.st[self.idx] self.idx -= 1Read the top, then move the top down. The old value stays in the array but is no longer part of the stack; a later push will overwrite it.
limit = min(k, self.idx + 1)Number of items to update: k, or all of them if there are fewer than k.
for i in range(limit): self.st[i] += valThe bottom items are positions 0, 1, 2, … so add val to each.

8Dry run: operation-sequence table

The array is drawn with only the live part st[0..idx] (left = bottom, right = top).

#operationwhat happensstack afteridxreturned
1CustomStack(3)3 slots, empty[]−1–
2push 1not full → idx 0[1]0–
3push 2idx 1[1, 2]1–
4popread st[1][1]02
5push 2idx 1[1, 2]1–
6push 3idx 2[1, 2, 3]2–
7push 4idx == 2 == maxSize − 1 → full, ignored[1, 2, 3]2ignored
8increment(5, 100)limit = min(5, 3) = 3 → positions 0, 1, 2[101, 102, 103]2–
9increment(2, 100)limit = min(2, 3) = 2 → positions 0, 1[201, 202, 103]2–
10popread st[2][201, 202]1103
11popread st[1][201]0202
12popread st[0][]−1201
13popidx == −1 → empty[]−1−1
after step 6
123
after step 8
101102103
after step 9
201202103

9Complexity & remember

Remember Part AArray + idx starting at −1. Full: idx == maxSize − 1. Empty: idx == −1 → pop returns −1. Increment loops over min(k, idx + 1) bottom slots.

Part B · Lazy increment array, everything O(1)

1The question again, with the new goal

Same three methods. Now make increment O(1) too, while keeping push and pop O(1).

2What the constraints tell us now

3Intuition: leave one note at the top of the range, pass it down on pop

The question asks us to add val to the bottom k items, but nobody can see those items until they're popped. If only one pop ever happens, updating all k items was wasted work. So instead of changing values now, write the increase once, as a note, on the highest item of the range. The note means "this item and every item below it still owe +val".

When that item is popped, we add its note to its value and return the sum. Then we pass the note down to the item just below, which now becomes the top. All the items further down will get the note too, one step at a time, as they come up to be popped.

4Building the logic from the example

Keep a second array inc of zeros, the same size as st. State before the increments: st = [1, 2, 3], idx = 2, inc = [0, 0, 0].

increment(5, 100)

The range is the bottom min(5, 3) = 3 items: positions 0 … 2. Its highest position is min(k − 1, idx) = min(4, 2) = 2. Write the note there: inc = [0, 0, 100]. One step, not three.

Doubt: why min(k − 1, idx) here, when Part A used min(k, idx + 1)?
→ Part A needed a count (how many items to loop over). Now we need a position (where to put the note): the highest affected index. Both k − 1 and idx are 0-based positions, so we compare them directly. It's the same idea, shifted by one.

increment(2, 100)

Highest affected position = min(1, 2) = 1. inc[1] += 100 → inc = [0, 100, 100]. If a note is already there, we add to it; notes stack up.

pop → 103

Answer = st[2] + inc[2] = 3 + 100 = 103 ✓. The note at position 2 also applies to positions 0 and 1, so pass it down one step: inc[1] += 100 → 200. Then clear inc[2] = 0. idx = 1.

pop → 202

st[1] + inc[1] = 2 + 200 = 202 ✓. Pass down: inc[0] += 200 → 200. Clear inc[1]. idx = 0.

pop → 201

st[0] + inc[0] = 1 + 200 = 201 ✓. idx is 0, so there's no item below; nothing to pass. Clear inc[0]. idx = −1.

pop → −1

idx == −1, empty.

Doubt: why must we reset inc[idx] = 0 after popping?
→ The slot will be reused by a future push, and the old note would wrongly apply to the new item. Example with maxSize 3: push 1, push 3, increment(2, 100) → note 100 at position 1. pop → 3 + 100 = 103, pass 100 down to position 0. If we forget to clear position 1, then push 4 lands at position 1 and the next pop returns 4 + 100 = 104, but 4 was pushed after the increment and should come back as 4.
Doubt: why pass the note only to the one item below, not to all of them?
→ Because only the top can be popped next. The item below will pass the note on again when its own turn comes. Passing to all of them would bring back the O(k) loop we're trying to avoid.
Doubt: why check idx > 0 before passing down?
→ When we pop the very last item (position 0), there is no item below, so there's no position −1 to give the note to. (In Python, inc[-1] wouldn't crash: it would silently write to the last slot of the list, which is an even nastier bug.)

5Approach steps

  1. Arrays st and inc of size maxSize (inc all zeros), idx = −1.
  2. push(x): same as Part A (inc[idx] is already 0 for a fresh slot).
  3. increment(k, val): i = min(k − 1, idx); if i ≥ 0 → inc[i] += val.
  4. pop(): if empty return −1. Else res = st[idx] + inc[idx]; if idx > 0 → inc[idx − 1] += inc[idx]; inc[idx] = 0; idx −= 1; return res.

6Code (Python)

Part B: lazy increment, all O(1)
class CustomStack:
    def __init__(self, maxSize):
        self.maxSize = maxSize
        self.st = [0] * maxSize
        self.inc = [0] * maxSize    # inc[i] = amount still owed by items 0..i
        self.idx = -1

    def push(self, x):
        if self.idx == self.maxSize - 1:
            return
        self.idx += 1
        self.st[self.idx] = x

    def pop(self):
        if self.idx == -1:
            return -1
        i = self.idx
        res = self.st[i] + self.inc[i]      # apply the note now
        if i > 0:
            self.inc[i - 1] += self.inc[i]  # pass it to the item below
        self.inc[i] = 0                     # clear the slot for future pushes
        self.idx -= 1
        return res

    def increment(self, k, val):
        i = min(k - 1, self.idx)            # highest affected position
        if i >= 0:                          # stack might be empty
            self.inc[i] += val

7Code line by line

linewhat it means
self.inc = [0] * maxSizeOne note per slot. A note at position i means "items 0 … i each still owe this much".
res = self.st[i] + self.inc[i]The true value of the top = stored value + everything it still owes.
if i > 0: self.inc[i - 1] += self.inc[i]The items below owe the same amount. Give the note to the new top only.
self.inc[i] = 0This slot is now free. Clear it so a future item placed here doesn't inherit an old note.
i = min(k - 1, self.idx)The highest position in the bottom-k range, clipped to the current top.
if i >= 0: self.inc[i] += valIf the stack is empty (idx = −1), i is −1 and there's nothing to increment. Otherwise add to the note (notes add up).

8Dry run: operation-sequence table

#operationwhat happensst[0..idx]inc[0..2]idxreturned
1CustomStack(3)–[][0, 0, 0]−1–
2push 1–[1][0, 0, 0]0–
3push 2–[1, 2][0, 0, 0]1–
4pop2 + 0; pass 0 down; clear[1][0, 0, 0]02
5push 2–[1, 2][0, 0, 0]1–
6push 3–[1, 2, 3][0, 0, 0]2–
7push 4full → ignored[1, 2, 3][0, 0, 0]2ignored
8increment(5, 100)i = min(4, 2) = 2[1, 2, 3][0, 0, 100]2–
9increment(2, 100)i = min(1, 2) = 1[1, 2, 3][0, 100, 100]2–
10pop3 + 100; inc[1] += 100; clear inc[2][1, 2][0, 200, 0]1103
11pop2 + 200; inc[0] += 200; clear inc[1][1][200, 0, 0]0202
12pop1 + 200; idx is 0 → no pass; clear inc[0][][0, 0, 0]−1201
13popempty[][0, 0, 0]−1−1

The stored values in st never changed (1, 2, 3). Only the notes moved, and each pop applied exactly what its item was owed.

after 9st:123inc:0100100
after 10st:12inc:02000
after 11st:1inc:20000

Yellow = the current top and its note. Read st left = bottom, right = top.

9Complexity & remember (with a counted example)

Counting the saving

maxSize = 1000. Push 1000 items, call increment(1000, 1) 1000 times, then pop all 1000.

Part A (loop)Part B (lazy)
1000 increments1000 additions each = 1,000,0001 addition each = 1,000
1000 pops1,000about 3 small steps each (add note, pass down, clear) = 3,000
total work for increments + pops≈ 1,001,000≈ 4,000

The teacher's point: an increment might be followed by only one pop, so updating every item up front can be pure waste. In an interview, the lazy version is the expected answer.

Remember Part Bincrement: inc[min(k−1, idx)] += val. pop: return st[idx] + inc[idx], pass inc[idx] to idx − 1 (if idx > 0), then reset inc[idx] = 0.

Part C · Revision page

Part A: loopPart B: lazy notes
storagest array + idxst + inc arrays + idx
pushif idx == maxSize − 1 ignore; else idx += 1, st[idx] = x · O(1)
popst[idx], idx −= 1 · O(1)st[idx] + inc[idx], pass note down, clear, idx −= 1 · O(1)
incrementadd val to st[0 … min(k, idx+1) − 1] · O(k)inc[min(k−1, idx)] += val · O(1)
spaceO(n)O(2n)
If you remember only 5 lines 1. Fixed max size → use an array and an idx starting at −1 (empty = −1, full = maxSize − 1).
2. "Bottom k" = positions 0 … k−1, clipped to the current number of items.
3. Lazy idea: put the increase as a note on the highest affected position only.
4. On pop: value + note, hand the note to the item just below, reset the slot's note to 0.
5. Result: push, pop and increment are all O(1), using one extra array.
Mistakes to avoid ✗ comparing k with idx instead of idx + 1 (Part A) or k − 1 with idx + 1 (Part B): off-by-one
✗ forgetting to reset inc[idx] = 0 after a pop (a later push inherits an old note)
✗ passing the note down when idx is 0 (in Python inc[-1] silently hits the last slot)
✗ incrementing when the stack is empty (i = −1 again)
✗ forgetting to return −1 when popping an empty stack

With this problem the stack-design pattern is done. The teacher notes that with these patterns you've covered most of the common stack questions.

test it yourself (paste under either version)
s = CustomStack(3)
s.push(1); s.push(2)
print(s.pop())            # 2
s.push(2); s.push(3); s.push(4)   # 4 is ignored
s.increment(5, 100)
s.increment(2, 100)
print(s.pop(), s.pop(), s.pop(), s.pop())   # 103 202 201 -1

Based on this video: Design a Stack With Increment Operation