CMD Guide
HomeDSA

Advanced Patterns

Step 22 in the DSA path · 17 concepts · 55 problems

0 / 72 complete

📘 Learn Advanced Patterns from zero

The Monotonic Stack/Queue pattern keeps elements in a stack in sorted (here, increasing) order so you can answer "for each element, what is the nearest smaller/greater neighbor?" in O(n) total. The trigger: you're iterating an array and for each element you need the previous-less-element (PLE) and next-less-element (NLE) boundaries — exactly what Sum of Subarray Minimums needs. For each value, count how many subarrays it dominates as the minimum: left * right * value, where left = distance to PLE and right = distance to NLE. To avoid double-counting subarrays when equal values appear, we break ties asymmetrically: the left boundary uses strictly-less (pop on >=) and the right boundary uses less-or-equal (pop on >), so each subarray is attributed to exactly one minimum.

✨ Added by the guide to build intuition — not from the source course.

🏗️ Visual walkthrough — trace it step by step

Step 1 — Setup & the goal
arr = [ 3 , 1 , 2 , 4 ]
idx     0   1   2   3

stack (indices, values increasing): []
ans = 0    MOD = 1e9+7

For each element we want:
  left[i]  = # of choices for subarray start (incl. i)
  right[i] = # of choices for subarray end   (incl. i)
contribution = arr[i] * left[i] * right[i]

Goal: sum of min over every contiguous subarray. Brute force is O(n^2). Instead, each element arr[i] is the minimum of exactly left[i]*right[i] subarrays — those bounded by its nearest smaller neighbors. Why a monotonic stack: it finds those nearest-smaller boundaries for all i in one pass.

Step 2 — Push index 0 (value 3)
arr = [ 3 , 1 , 2 , 4 ]
         ^i=0
stack top has nothing smaller below it.

While stack && arr[stack.top] >= 3:  (empty -> skip)
left[0] = i - (-1) = 1   (no PLE; sentinel -1)
push -> stack = [0]      (values: [3])

Stack is empty, so index 0 has no previous-less element; its left boundary is the array edge. left[0]=1 means only one subarray can start here with 3 as a candidate min: [3] itself. We pop on >= for the left side so the PLE is strictly-less, the convention that keeps equal values from being double-counted.

Step 3 — Process index 1 (value 1): pop 3
arr = [ 3 , 1 , 2 , 4 ]
             ^i=1
While stack && arr[stack.top]=3 >= 1:  POP index 0
  -> 3's right boundary is fixed: NLE of 3 is index 1.
stack now empty.
left[1] = i - (-1) = 2   (1 has no smaller element to its left)
push -> stack = [1]      (values: [1])

Value 1 is smaller than 3, so 3 can never be the min of any subarray extending past index 1 — we pop it, finalizing that 3's reach to the right ends here. left[1]=2: subarrays starting at index 0 or 1 can have 1 as min, since everything to the left (just 3) is larger.

Step 4 — Process index 2 (value 2)
arr = [ 3 , 1 , 2 , 4 ]
                 ^i=2
While stack && arr[stack.top]=1 >= 2?  1 >= 2 is FALSE -> stop.
PLE of 2 is index 1 (value 1).
left[2] = i - 1 = 1
push -> stack = [1, 2]   (values: [1, 2])

The stack top is 1, which is smaller than 2, so we stop popping — 1 is the previous-less element of 2. left[2]=1: only a subarray starting exactly at index 2 keeps 2 as min, because index 1 holds the smaller 1.

Step 5 — Process index 3 (value 4)
arr = [ 3 , 1 , 2 , 4 ]
                     ^i=3
While stack && arr[stack.top]=2 >= 4?  FALSE -> stop.
PLE of 4 is index 2 (value 2).
left[3] = i - 2 = 1
push -> stack = [1, 2, 3]   (values: [1, 2, 4])

Stack stays increasing [1,2,4]. 4 is the largest so far, its previous-less element is 2 at index 2, giving left[3]=1. The main loop ends; remaining stack elements never got popped, meaning their next-less-or-equal element is the right edge (sentinel n=4).

Step 6 — Flush stack: assign right boundaries
n = 4. Survivors on the stack reach the right edge (NLE = n = 4).
right[i] = NLE[i] - i.
  popped value 3: NLE was set to index 1  -> right[0] = 1 - 0 = 1
  survivor 1 (idx 1): NLE = 4            -> right[1] = 4 - 1 = 3
  survivor 2 (idx 2): NLE = 4            -> right[2] = 4 - 2 = 2
  survivor 4 (idx 3): NLE = 4            -> right[3] = 4 - 3 = 1

idx:    0    1    2    3
val:    3    1    2    4
left:   1    2    1    1
right:  1    3    2    1

Survivors 1,2,4 never found an element <= themselves to their right, so they extend to the array's end (index 4). Popped value 3 got right=1 when index 1 popped it. Why this is safe: each element's right reach stops at the first element that is less-or-equal (or the edge), while its left reach stops at the first strictly-smaller element, so no subarray is counted under two different minima.

Step 7 — Compute contributions & total
Boundaries (PLE strictly-less on left, NLE <= on right):
idx:        0    1    2    3
val:        3    1    2    4
left:       1    2    1    1
right:      1    3    2    1
contrib = val*left*right:
  3*1*1 = 3
  1*2*3 = 6
  2*1*2 = 4
  4*1*1 = 4
--------------------------
sum = 3 + 6 + 4 + 4 = 17

Cross-check by listing every subarray min of [3,1,2,4]: 3, 1, 2, 4 (len1) + min(3,1)=1, min(1,2)=1, min(2,4)=2 (len2) + min(3,1,2)=1, min(1,2,4)=1 (len3) + min(3,1,2,4)=1 (len4) = (3+1+2+4) + (1+1+2) + (1+1) + 1 = 10 + 4 + 2 + 1 = 17. Matches. Each element was pushed and popped once, so total work is O(n).

🎯 Guided practice

  1. Easy — Count Elements With Maximum Frequency. Given nums = [1,2,2,3,1,4], return the total count of all elements that share the highest frequency.

    Step 1 — tally: walk once, building freq = {1:2, 2:2, 3:1, 4:1}.

    Step 2 — find the max frequency: scan the values → max is 2.

    Step 3 — sum the counts of every key that hits the max: keys 1 and 2 both have count 2, so total = 2 + 2 = 4. Answer: 4.

    Pattern: a single counting pass plus a one-line reduction. O(n) time, O(k) space. The trap is returning the number of keys (2) instead of the sum of their counts (4) — re-read what's asked.

  2. Medium — Least Number of Unique Integers after K Removals. Given arr = [4,3,1,1,3,3,2], k = 3, remove exactly k elements to minimize the remaining distinct count.

    Step 1 — tally: freq = {4:1, 3:3, 1:2, 2:1} → 4 distinct values.

    Step 2 — greedy insight: to eliminate a whole distinct value cheaply, spend removals on the values with the smallest counts first — each fully-cleared value drops the distinct count by one at least cost.

    Step 3 — sort counts ascending: [1, 1, 2, 3].

    Step 4 — spend k greedily: remove count-1 → k=2, distinct=3; remove count-1 → k=1, distinct=2; next is count-2 but only k=1 left, so it survives. Answer: 2.

    Pattern: counting + greedy on sorted frequencies. Tally O(n); sorting distinct counts O(k log k). Pitfall: partially removing a value wastes removals without lowering the distinct count, so only commit removals that fully clear a value.

  3. Medium — Longest Subarray with Absolute Diff ≤ Limit (monotonic queue). Given nums = [8,2,4,7], limit = 4, find the longest window where max − min ≤ 4.

    Step 1 — two deques of indices: maxDq (values decreasing front→back) and minDq (values increasing), plus a left pointer l = 0.

    Step 2 — extend right, maintaining monotonicity: at each r, pop the back of maxDq while it's smaller than nums[r] and the back of minDq while it's larger, then push r. The fronts now give the window's max and min in O(1).

    Step 3 — shrink when invalid: while nums[maxDq.front] − nums[minDq.front] > limit, advance l and pop any deque front whose index < l.

    Step 4 — trace: r=0 [8] ok len1; r=1 [8,2] diff 6>4 → shrink to [2] len1; r=2 [2,4] diff 2 ok len2; r=3 [2,4,7] diff 5>4 → shrink to [4,7] diff 3 ok len2. Answer: 2.

    Pattern: sliding window + monotonic deques storing indices. Each index enters and leaves each deque once → O(n) total, O(n) space. Pitfall: storing values instead of indices, which leaves you unable to evict elements that slid past l.

✨ Added by the guide — work these before the full problem set.

Lessons in this topic

🧠 Review & recall

Active recall is what moves a topic into long-term memory. Flip each card before revealing, then test yourself — your results are saved on this device.

Flashcard
What is a monotonic queue, what structure implements it, and what is its complexity over a full pass?
tap to reveal →
A monotonic queue keeps its elements in either non-decreasing or non-increasing order as they are processed, implemented with a deque so you can add/remove from both ends. Building it is O(n) time because each element is pushed and popped at most once, with O(n) space worst case.
💡 Deque that throws out anyone it dominates; min-window uses increasing, max-window uses decreasing.
Flashcard
When does Counting Sort beat comparison sorts, and what is its time complexity?
tap to reveal →
Counting Sort is a non-comparative, stable sort that counts occurrences of each value; it runs in O(n+k) where n is the element count and k is the value range, so it wins when k is not much larger than n. It builds a count array of size max+1, converts it to cumulative counts, then places elements (iterating input in reverse to stay stable).
💡 Count, cumulate, place in reverse for stability — O(n+k), great when range is small.
Flashcard
What is the Meet in the Middle pattern and when do you reach for it?
tap to reveal →
Meet in the Middle splits the input into two halves, enumerates all 2^(n/2) subset results per half, sorts one half, and combines them (often via binary search). It turns an infeasible O(2^n) brute force into roughly O(2^(n/2) * n), used for subset-sum style problems with n up to about 40.
💡 2^40 is too big, but 2^20 + binary search is fine — split, enumerate, meet.
Flashcard
How does a Segment Tree store its nodes in an array, and what is each node's index relationship?
tap to reveal →
A Segment Tree for an array of size n is stored in an array of size 4*n with the root at index 1; for any node at index i, the left child is at 2i and the right child at 2i+1. It answers range queries and applies point updates in O(log n) using divide and conquer.
💡 Root at 1, kids at 2i / 2i+1, allocate 4N — log n queries and updates.
Flashcard
Why pick a Binary Indexed Tree (Fenwick Tree) over a Segment Tree, and what is its key limitation?
tap to reveal →
A BIT gives O(log n) prefix-sum updates and queries using only O(n) space, and is simpler to implement than a Segment Tree, so it's preferred for prefix sums / inversion counts when space and simplicity matter. Its limitation: it can't handle arbitrary range queries like range min/max, only invertible aggregates like sums.
💡 Fenwick = leaner, simpler, sums-only; Segment Tree = heavier but does min/max too.
Flashcard
What does Tarjan's Algorithm find, and what is an articulation point?
tap to reveal →
An articulation point (cut vertex) is a node whose removal increases the number of connected components — a single point of failure. The naive check removes each vertex and recounts components in O(V*(V+E)); Tarjan's Algorithm finds all articulation points (and bridges) far more efficiently in a single DFS.
💡 Cut vertex = removing it shatters the graph; naive is O(V*(V+E)), Tarjan does it in one DFS.
Q1. To efficiently track the MAXIMUM in a sliding window, which monotonic queue do you maintain?
Q2. In Counting Sort, why is the input array traversed in REVERSE when building the output array?
Q3. For a subset-sum problem with n = 40 elements, why is Meet in the Middle used instead of brute force?
Q4. What array size does a Segment Tree allocate for an input array of size n, and where is the root stored?
Q5. In 'Sum of Subarray Minimums', the monotonic stack lets you compute each element's contribution by multiplying which two quantities?