All tracks

DSA patterns

A compact, pattern-first problem sheet in Python. 285 problems, each chosen because it teaches something the previous ones didn't.

Learn the pattern, solve a few canonical problems, solve meaningful variations, then recognise the pattern on your own.

The curriculum map

Seven stages, followed in order. Inside each pattern, problems go from canonical to variation to combination, and every problem says what it adds.

Stage 0: Prerequisites

Concepts and Python tools, with short examples: Big-O, arrays and strings, hash maps and sets, linked lists, stacks and queues, recursion, trees, heaps, graph representations, sorting, binary search, and the idea behind DP.

Stage 1: Linear patterns

Arrays, strings, and lists, scanned in one or two passes. Everything later builds on these.

PatternProblemsBuilds on
Hashing and counting (including the array as its own hash map)~12Prerequisites
Two pointers~12Hashing, sorting
Sliding window (fixed and variable)~12Two pointers, hashing
Prefix sums and difference arrays~10Hashing
Binary search (on indexes, rotated arrays, and on the answer)~14Two pointers
Linked lists, including fast and slow pointers~12Two pointers
Stacks, monotonic stacks, and monotonic deques~14Sliding window
Intervals and sweep line~9Sorting, prefix sums

Stage 2: Recursion and trees

Thinking in subproblems. Backtracking comes first because tree DFS is the same idea with a fixed shape.

PatternProblemsBuilds on
Recursion and backtracking~15Prerequisites (recursion)
Trees: DFS, BFS, and binary search trees~24Backtracking, queues
Heaps: top-K, two heaps, and k-way merge~12Hashing, linked lists

Stage 3: Graphs

Trees without the rules: cycles, many components, and weights.

PatternProblemsBuilds on
Graph traversal: DFS, BFS, grids, multi-source BFS~14Trees
Topological sort~6Graph traversal
Union-find~7Graph traversal
Shortest paths and minimum spanning trees~8Heaps, graph traversal

Stage 4: Greedy and dynamic programming

Making optimal choices: when a local choice is safe (greedy), and when you must try them all and remember results (DP).

PatternProblemsBuilds on
Greedy~12Sorting, heaps, intervals
DP: one-dimensional~12Backtracking
DP: knapsack and subset sums~81-D DP
DP: grids and two strings~10Knapsack
DP: advanced (LIS, state machines, intervals, bitmasks, trees)~10All earlier DP, trees

Stage 5: Specialised tools

Smaller patterns that come up less often but are hard to invent under pressure.

PatternProblemsBuilds on
Tries~6Trees, backtracking
Bit manipulation~7Hashing
Math, matrices, and simulation~6Stage 1
Data structure design (LRU cache and similar)~5Hashing, linked lists, heaps
Optional: range queries (Fenwick and segment trees) and string algorithms~8Prefix sums, divide and conquer

Stage 6: Mixed practice

About 20 problems with no pattern label, spread across every stage. Name the pattern before you write any code.

Stage 0

Prerequisites

The tools every pattern builds on. Skim what you know, open what you don't. Everything here is Python, in the style LeetCode expects.

Python toolkit at a glance

StructureIn PythonFastSlow
Dynamic arraylistIndex, append, pop(), length: O(1)insert(0, x), pop(0), x in list, remove: O(n)
Hash mapdict, Counter, defaultdictGet, set, delete, in: O(1) averageIterating: O(n)
Hash setsetAdd, remove, in: O(1) averageIterating: O(n)
Double-ended queuecollections.dequeAppend and pop at either end: O(1)Indexing the middle: O(n)
Min-heapheapq on a listPush, pop: O(log n); peek: O(1); heapify: O(n)Finding an arbitrary item: O(n)
Sorted listbisectSearch: O(log n)insort: O(n), because it shifts elements
Stringstr (immutable)Index, length: O(1)Slicing: O(k); += in a loop: can be O(n²)
Sortsorted, list.sortO(n log n), stable

Read the constraints first

The input size usually tells you the intended complexity before you've thought about the problem.

Input size nTargetTypical approaches
Up to about 10O(n!) or O(2ⁿ · n)Permutations, backtracking
Up to about 20O(2ⁿ)Subsets, bitmask DP
Up to about 500O(n³)Interval DP, Floyd–Warshall
Up to about 5,000O(n²)2-D DP, checking all pairs
10⁵ to 10⁶O(n log n) or O(n)Sorting, heaps, hashing, two pointers, sliding window
Values up to 10⁹ or moreO(log n) or O(1)Binary search on the answer, math

Rules of thumb, not guarantees. Python is slower than C++ or Java, so when a limit is borderline, aim one step lower.

Concepts

Big-O and hidden costsEstimate the cost before you code, and know which Python operations are secretly linear.

Big-O describes how work grows with input size, ignoring constants. Count the loops, then add the cost of what happens inside them. Space counts the extra memory you allocate, not the input.

The usual trade is memory for speed: a set turns an O(n) search into an O(1) lookup.

# O(n^2) time, O(1) space: compare every pair
def has_duplicate_slow(nums):
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] == nums[j]:
                return True
    return False

# O(n) time, O(n) space: remember what you've seen
def has_duplicate(nums):
    seen = set()
    for x in nums:
        if x in seen:
            return True
        seen.add(x)
    return False

Watch out: Hidden linear costs inside a loop turn O(n) into O(n²): x in some_list, list.pop(0), list.insert(0, x), slicing, and s += c on strings.

Arrays and stringsPython lists are dynamic arrays; strings are immutable, so build them with join.

Indexing and appending are O(1). Inserting or deleting anywhere but the end shifts elements, so it's O(n). Strings can't change in place: convert to a list, edit, and join back.

For lowercase letters, a 26-slot count array is often faster and simpler than a dictionary.

grid = [[0] * 3 for _ in range(2)]   # 2 rows, 3 columns: correct
bad = [[0] * 3] * 2                    # both rows are the SAME list
bad[0][0] = 9                          # bad is now [[9, 0, 0], [9, 0, 0]]

s = "hello"
chars = list(s)
chars[0] = "j"
s = "".join(chars)                     # "jello"
rev = s[::-1]                          # "ollej", O(n) copy

freq = [0] * 26
for ch in "abca":
    freq[ord(ch) - ord("a")] += 1      # freq[0] == 2

Watch out: [[0] * cols] * rows creates one row shared by every index. Always build grids with a comprehension.

Hash maps and setsAverage O(1) insert, delete, and lookup. The tool you'll reach for most often.

dict maps keys to values and set stores unique keys. Counter counts things, and defaultdict removes the "is this key new?" check. Keys must be hashable: numbers, strings, and tuples work; lists and sets don't.

from collections import Counter, defaultdict

counts = Counter("banana")       # Counter({'a': 3, 'n': 2, 'b': 1})
missing = counts["z"]            # 0: missing keys count as zero

groups = defaultdict(list)
for word in ["eat", "tea", "tan"]:
    groups[len(word)].append(word)

ages = {"asha": 31}
ages.get("ravi", 0)              # 0 instead of a KeyError

visited = set()
visited.add((2, 3))              # a tuple can be a key; a list can't
a, b = {1, 2, 3}, {2, 3, 4}
both, either, only_a = a & b, a | b, a - b

Watch out: To use a list as a key, convert it to a tuple first, for example tuple(sorted(word)).

Linked listsNodes pointing to the next node. Most bugs are a missing None check.

No indexing: reaching the k-th node is O(k). In exchange, inserting or removing a node you already hold is O(1). A dummy node placed before the head removes the special case of changing the head itself.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def build(values):
    dummy = ListNode()               # placeholder before the real head
    tail = dummy
    for v in values:
        tail.next = ListNode(v)
        tail = tail.next
    return dummy.next

def to_list(head):
    out = []
    while head:                      # stops cleanly at None
        out.append(head.val)
        head = head.next
    return out

print(to_list(build([1, 2, 3])))     # [1, 2, 3]

Watch out: Check that a node exists before touching node.next. Draw the pointers on paper before rewiring them.

Stacks and queuesLast in, first out and first in, first out. Use a list for a stack and a deque for a queue.

A stack serves the most recent item first: matching brackets, undo, depth-first search. A queue serves the oldest item first: breadth-first search, processing in arrival order. deque gives O(1) operations at both ends.

from collections import deque

stack = []
stack.append(1)
stack.append(2)
top = stack[-1]          # peek: 2
stack.pop()              # 2

queue = deque([1, 2])
queue.append(3)
first = queue.popleft()  # 1, in O(1)

Watch out: list.pop(0) works as a queue but costs O(n) each time, which quietly makes BFS O(n²).

RecursionA function that solves a smaller copy of the same problem, with a base case to stop.

Write the base case first, then trust the recursive call to solve the smaller problem, and combine. Every call waits on the call stack, so recursion uses O(depth) memory.

import sys

def power(base, exp):
    if exp == 0:                          # base case
        return 1
    half = power(base, exp // 2)          # smaller subproblem
    return half * half * (base if exp % 2 else 1)

print(power(2, 10))                       # 1024, in O(log exp) calls
print(sys.getrecursionlimit())            # 1000 by default

Watch out: Python's default recursion limit is 1,000 frames. A skewed tree or a long linked list with 10⁵ nodes raises RecursionError; switch to an explicit stack, or raise the limit with sys.setrecursionlimit as a last resort.

TreesRecursive structures: most tree problems are a DFS that combines answers from the two children.

Depth-first search visits a whole subtree before moving on; the three orders differ only in when you process the node. Breadth-first search visits level by level with a queue. In a binary search tree, an in-order traversal returns the values sorted.

from collections import deque

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def inorder(root, out):
    if not root:
        return
    inorder(root.left, out)
    out.append(root.val)      # preorder: before both calls; postorder: after both
    inorder(root.right, out)

def height(root):
    if not root:
        return 0
    return 1 + max(height(root.left), height(root.right))

def bfs(root):
    order = []
    queue = deque([root] if root else [])
    while queue:
        node = queue.popleft()
        order.append(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
    return order

root = TreeNode(2, TreeNode(1), TreeNode(3))
vals = []
inorder(root, vals)
print(vals, height(root), bfs(root))   # [1, 2, 3] 2 [2, 1, 3]

Watch out: Decide what each call returns before writing it: a height, a boolean, a best-so-far value. Most tree bugs come from mixing "return the answer" with "update a global answer".

Heaps and priority queuesAlways get the smallest item in O(log n). Python's heapq is a min-heap over a plain list.

Use a heap when you repeatedly need the minimum or maximum of a changing collection: top-K, scheduling, merging sorted lists, Dijkstra. heap[0] is the smallest item; the rest of the list is not sorted.

For a max-heap, push negated numbers. Python 3.14 added dedicated functions such as heappush_max, but coding platforms may run older versions, so negation is the portable choice.

import heapq

h = []
for x in [5, 1, 3]:
    heapq.heappush(h, x)
smallest = h[0]                  # 1, peek in O(1)
heapq.heappop(h)                 # 1, O(log n)

nums = [4, 1, 7]
heapq.heapify(nums)              # O(n), in place

max_heap = []
for x in [5, 1, 7]:
    heapq.heappush(max_heap, -x)
largest = -max_heap[0]           # 7

tasks = []
heapq.heappush(tasks, (2, 0, "write"))   # (priority, tie-breaker, item)
heapq.heappush(tasks, (1, 1, "read"))
print(heapq.heappop(tasks)[2])           # read
print(heapq.nlargest(2, [5, 1, 9, 3]))   # [9, 5]

Watch out: Tuples compare item by item. If two priorities tie and the next item can't be compared (a ListNode, say), you get a TypeError; put a counter between them as a tie-breaker.

Graph basicsNodes and edges. Build an adjacency list, then traverse with BFS or DFS and a visited set.

Most problems give you an edge list or a grid. Convert edges into an adjacency list; treat a grid as a graph where each cell connects to its neighbours. Undirected edges go in both directions.

from collections import defaultdict, deque

edges = [[0, 1], [1, 2], [2, 0], [3, 4]]
graph = defaultdict(list)
for a, b in edges:
    graph[a].append(b)
    graph[b].append(a)           # undirected: add both directions

def reachable(start):
    seen = {start}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for nxt in graph[node]:
            if nxt not in seen:
                seen.add(nxt)    # mark when queued, not when popped
                queue.append(nxt)
    return seen

DIRS = [(1, 0), (-1, 0), (0, 1), (0, -1)]
def neighbours(r, c, rows, cols):
    for dr, dc in DIRS:
        nr, nc = r + dr, c + dc
        if 0 <= nr < rows and 0 <= nc < cols:
            yield nr, nc

print(sorted(reachable(0)), list(neighbours(0, 0, 2, 2)))

Watch out: Mark a node visited when you add it to the queue. Marking it when you pop lets the same node be queued many times.

SortingO(n log n) and stable in Python. Often the first step that makes a problem easy.

sorted() returns a new list; list.sort() sorts in place. Sort by several fields with a tuple key, and descending on a number by negating it. Python's sort is stable: equal items keep their original order.

Know merge sort well enough to write it. Its merge step reappears in several problems later.

people = [("asha", 31), ("ravi", 25), ("meera", 31)]
by_age = sorted(people, key=lambda p: p[1])
# [('ravi', 25), ('asha', 31), ('meera', 31)]: asha stays before meera
oldest_first = sorted(people, key=lambda p: (-p[1], p[0]))

def merge_sort(a):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    left, right = merge_sort(a[:mid]), merge_sort(a[mid:])
    out, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            out.append(left[i])
            i += 1
        else:
            out.append(right[j])
            j += 1
    return out + left[i:] + right[j:]

print(merge_sort([5, 2, 4, 1]))   # [1, 2, 4, 5]

Watch out: When an order can't be expressed as a key, functools.cmp_to_key turns a two-argument comparison function into one.

The idea behind dynamic programmingSolve each subproblem once and reuse the answer.

DP applies when a problem breaks into smaller versions of itself that overlap, and the best answer is built from the best answers to those pieces. Define three things: the state (what a subproblem is), the transition (how states connect), and the base cases.

Top-down is recursion plus a cache, and is usually easiest to write first. Bottom-up fills a table in order, and often lets you keep only the last few values.

from functools import cache

@cache
def fib(n):                  # top-down: recursion + memo, O(n)
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

def fib_table(n):            # bottom-up, O(1) space
    if n < 2:
        return n
    prev, cur = 0, 1
    for _ in range(n - 1):
        prev, cur = cur, prev + cur
    return cur

print(fib(40), fib_table(40))   # 102334155 102334155

Watch out: @cache needs hashable arguments, so pass indexes rather than lists. Define cached helpers inside the solution method so results don't leak between test cases.

Python habits for problem solvingSmall language details that cause wrong answers or save a lot of code.

Python integers never overflow, which removes a whole class of bugs, but problems that ask for 32-bit behaviour need explicit checks. The itertools and collections modules cover most helpers you'd otherwise hand-write.

from itertools import accumulate, pairwise, combinations

-7 // 2                          # -4: floor division rounds down
int(-7 / 2)                      # -3: truncates toward zero
best = float("inf")              # starting value for a minimum

path, result = [1, 2], []
result.append(path[:])           # save a COPY; path will keep changing

def count_paths(n):
    total = 0
    def dfs(i):
        nonlocal total           # assign to the outer variable
        if i == n:
            total += 1
            return
        dfs(i + 1)
    dfs(0)
    return total

list(accumulate([1, 2, 3]))      # [1, 3, 6], running prefix sums
list(pairwise([1, 2, 3]))        # [(1, 2), (2, 3)], Python 3.10+
list(combinations([1, 2, 3], 2)) # [(1, 2), (1, 3), (2, 3)]

Watch out: Never use a mutable default argument such as def f(seen=set()); the same set is shared across calls. Use None and create it inside.

Stage 1

Linear patterns

Arrays, strings, and lists scanned in one or two passes. Every later stage leans on these, so don't rush them.

Pattern 1

Hashing and counting

Trade memory for time: store what you've seen in a hash map or set so each later check is O(1) instead of a scan. It turns many O(n²) brute-force solutions into one O(n) pass.

Recognise it when

  • You ask "have I seen this, or its partner, before?"
  • The problem is about frequencies, duplicates, or grouping
  • Brute force compares every pair, and the input isn't sorted
  • Values fall in a known range, so the array itself can act as the map

Variations

  • Lookup: store items, check for a complement or match
  • Counting: frequency maps with Counter
  • Grouping: map a canonical key to a list of items
  • Index as hash: mark positions in the input array itself for O(1) extra space

Core intuition: Decide what to store (values, counts, or indexes) and what the key is. Check before you insert, so an item never pairs with itself.

Template

from collections import Counter, defaultdict

# 1. Lookup: have I seen what this item needs?
seen = {}                          # key -> index (or count, or item)
for i, x in enumerate(items):
    need = partner_of(x)
    if need in seen:
        pass                       # found: pair (seen[need], i)
    seen[key_of(x)] = i            # insert AFTER checking

# 2. Counting
counts = Counter(items)

# 3. Grouping by a canonical key
groups = defaultdict(list)
for x in items:
    groups[canonical(x)].append(x)

Pseudocode: partner_of, key_of and canonical stand for whatever the problem needs.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Two SumEasyLeetCode 1

    The problem the pattern is named after. Brute force is O(n²); a map makes it one pass.

    Hashing
    Key insight

    Store each value's index, and before storing, look up target - x. Checking first stops a number pairing with itself.

  2. Valid AnagramEasyLeetCode 242

    The simplest frequency-counting problem.

    HashingCounting
    Key insight

    Two strings are anagrams exactly when their character counts match. A 26-slot array or a Counter beats sorting: O(n) instead of O(n log n).

  3. Group AnagramsMediumLeetCode 49

    Introduces grouping by a canonical key, one of the most reused ideas in the sheet.

    HashingGrouping

    Solve first: Valid Anagram

    Key insight

    Every anagram of a word shares one key: its sorted letters, or a tuple of 26 counts. Map that key to a list of words.

Variations 6

The same idea with a twist that changes the details.

  1. Isomorphic StringsEasyLeetCode 205

    A mapping must hold in both directions, which a single map misses.

    Hashing
    Key insight

    Keep two maps, s-to-t and t-to-s, and fail on any conflict. One map alone accepts "ab" and "aa".

  2. Contains Duplicate IIEasyLeetCode 219

    Stores an index instead of a flag, so the map answers "how far back?"

    Hashing
    Key insight

    Keep each value's most recent index; a duplicate counts if i - last[x] <= k. This is a first taste of a sliding window.

  3. Majority ElementEasyLeetCode 169

    Counting solves it in O(n) space; the follow-up asks for O(1).

    Counting
    Key insight

    Boyer-Moore voting: keep a candidate and a count, add one for a match and subtract one otherwise. The majority element survives because it outnumbers everything else combined.

  4. Top K Frequent ElementsMediumLeetCode 347

    Counting is the easy half; choosing the top k without a full sort is the lesson.

    CountingBucket sort

    Solve first: Valid Anagram

    Key insight

    A count can't exceed n, so put values into buckets indexed by count, then read buckets from the highest. That's O(n). A heap also works; it returns in the Heaps pattern.

  5. Valid SudokuMediumLeetCode 36

    Several sets checked at once, with tuple keys.

    Hashing
    Key insight

    One set per row, column and 3×3 box. The box index is (r // 3, c // 3). Alternatively, one set of tuples such as ("row", r, val).

  6. Longest Consecutive SequenceMediumLeetCode 128

    Sorting gives O(n log n); a set gives O(n) if you avoid recounting.

    Hashing
    Key insight

    Only start counting from a number whose predecessor isn't in the set. Each number is then visited at most twice, so the total is O(n).

Combinations and stretch 3

The pattern mixed with another, or pushed harder.

  1. Find All Numbers Disappeared in an ArrayEasyLeetCode 448

    Introduces using the array itself as the map, for O(1) extra space.

    Index as hash
    Key insight

    Values are 1 to n, so value v can mark index v − 1, by making it negative. Indexes still positive at the end are the missing numbers.

  2. 4Sum IIMediumLeetCode 454

    Splits a four-way search into two halves that meet in a map.

    HashingMeet in the middle

    Solve first: Two Sum

    Key insight

    Count every sum a + b in a map, then for every c + d look up −(c + d). O(n²) instead of O(n⁴).

  3. First Missing PositiveHardLeetCode 41

    A classic hard problem that needs O(n) time and O(1) space together.

    Index as hashCyclic sort

    Solve first: Find All Numbers Disappeared in an Array

    Key insight

    The answer is in 1 to n + 1. Swap each value v in that range into index v − 1 until nothing moves (cyclic sort), then the first index i holding the wrong value gives i + 1.

Pattern 2

Two pointers

Walk two indexes through the data so that each step rules out part of the search. It usually replaces a nested loop with one pass and O(1) extra space.

Recognise it when

  • The input is sorted, or sorting it won't hurt
  • You're looking for a pair or triplet that meets a target
  • You must modify an array in place with O(1) extra space
  • You compare from both ends, as with palindromes
  • You're merging two sorted sequences

Variations

  • Converging: one pointer at each end, moving inward
  • Read and write: a fast reader and a slow writer compact an array in place
  • Two sequences: one pointer per array, as in a merge
  • Three pointers: partitioning into three regions (Dutch national flag)

Core intuition: Every move must be justified: moving a pointer should throw away only candidates that can't be the answer. If you can't say why a move is safe, the approach is wrong.

Template

# Converging: sorted input, find a pair with a target sum
lo, hi = 0, len(a) - 1
while lo < hi:
    s = a[lo] + a[hi]
    if s == target:
        break                      # found (or record it and move both)
    elif s < target:
        lo += 1                    # need a bigger sum
    else:
        hi -= 1                    # need a smaller sum

# Read and write: keep wanted items, in order, in place
write = 0
for read in range(len(a)):
    if keep(a[read]):
        a[write] = a[read]
        write += 1
# a[:write] now holds the result

Two Sum II is the converging template almost exactly; solve it without looking back here.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Valid PalindromeEasyLeetCode 125

    Converging pointers with a filter: skip characters that don't count.

    Two pointers
    Key insight

    Move each pointer past non-alphanumeric characters before comparing, and compare lowercase. No need to build a cleaned copy.

  2. Two Sum II: Input Array Is SortedMediumLeetCode 167

    The converging template itself. Compare it with the hash map version.

    Two pointers

    Solve first: Two Sum

    Key insight

    Sorted order tells you which pointer to move: too small means move lo right, too big means move hi left. O(1) space, unlike the map.

  3. Remove Duplicates from Sorted ArrayEasyLeetCode 26

    The read-and-write template in its plainest form.

    Two pointers
    Key insight

    Write a value only when it differs from the last written one. Because the input is sorted, duplicates are always adjacent.

Variations 7

The same idea with a twist that changes the details.

  1. Move ZeroesEasyLeetCode 283

    Read-and-write where the discarded items must end up somewhere.

    Two pointers
    Key insight

    Compact the non-zeros to the front, then fill the rest with zeros. Or swap as you go, which keeps it a single pass.

  2. Squares of a Sorted ArrayEasyLeetCode 977

    Converging pointers that build the output from the largest end.

    Two pointers
    Key insight

    The largest square is at one of the two ends. Compare the ends and write into the result from the back.

  3. Merge Sorted ArrayEasyLeetCode 88

    Two sequences, merged in place. Going forward overwrites data; going backward doesn't.

    Two pointersMerge
    Key insight

    Fill nums1 from the end with the larger of the two current tails. Only leftover nums2 elements need copying at the end.

  4. Valid Palindrome IIEasyLeetCode 680

    Adds one branching decision to the converging pattern.

    Two pointers
    Key insight

    On the first mismatch, try skipping the left character or the right one and check the rest. Only one branch point is ever needed, so it's still O(n).

  5. 3SumMediumLeetCode 15

    The most asked two-pointer problem: fix one value, then run Two Sum II on the rest.

    Two pointersSorting

    Solve first: Two Sum II: Input Array Is Sorted

    Key insight

    Sort, then for each i run converging pointers on the right side. Skip equal neighbours for both i and the pointers to avoid duplicate triplets.

  6. Container With Most WaterMediumLeetCode 11

    Converging pointers on unsorted input, justified by an argument rather than order.

    Two pointersGreedy
    Key insight

    Area is limited by the shorter line, so moving the taller one can never help. Always move the shorter side.

  7. Sort ColorsMediumLeetCode 75

    Three regions instead of two: the Dutch national flag partition.

    Three pointersPartitioning
    Key insight

    Keep low, mid and high. A 0 swaps to low, a 2 swaps to high, a 1 stays. Don't advance mid after swapping with high; the incoming value is unchecked.

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Boats to Save PeopleMediumLeetCode 881

    Sorting plus converging pointers plus a greedy choice.

    Two pointersGreedySorting

    Solve first: Two Sum II: Input Array Is Sorted

    Key insight

    Sort, then pair the heaviest person with the lightest if they fit; otherwise the heaviest goes alone. The heaviest person must leave on some boat, so pairing them with the lightest is never worse.

  2. Trapping Rain WaterHardLeetCode 42

    A famous hard problem with prefix-max, stack, and two-pointer solutions. The two-pointer one is the most elegant.

    Two pointersPrefix max

    Solve first: Container With Most Water

    Key insight

    Water above a bar is min(max_left, max_right) − height. Move the side with the smaller running max: its water level is already decided by that max.

Pattern 3

Sliding window

Keep a contiguous window over the input and update its state as the edges move, instead of recomputing every subarray. Most window problems drop from O(n²) or O(n·k) to O(n).

Recognise it when

  • The answer is a contiguous subarray or substring
  • You want the longest, shortest, or count of windows meeting a condition
  • The window's state (sum, counts, distinct items) can be updated as an item enters or leaves
  • The window size is fixed, or validity only breaks as the window grows

Variations

  • Fixed size: add the new item, drop the one that fell out
  • Variable, longest valid: grow, shrink only while invalid
  • Variable, shortest valid: grow until valid, then shrink while still valid
  • Exactly K: count(at most K) minus count(at most K − 1)

Core intuition: The window only works when validity is monotonic: if a window is invalid, every larger window containing it is too (or the reverse). Negative numbers usually break this, which is when you reach for prefix sums instead.

Template

# Variable window: longest window that stays valid
left = 0
best = 0
for right, x in enumerate(a):
    add(x)                          # grow: include a[right]
    while not valid():              # shrink until valid again
        remove(a[left])
        left += 1
    best = max(best, right - left + 1)

# Fixed window of size k
window = sum(a[:k])
best = window
for right in range(k, len(a)):
    window += a[right] - a[right - k]   # add the new item, drop the old one
    best = max(best, window)

Pseudocode: add, remove and valid maintain whatever the window tracks, such as a sum or a Counter.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Best Time to Buy and Sell StockEasyLeetCode 121

    The simplest "track the best so far while scanning" problem.

    Sliding windowRunning minimum
    Key insight

    Keep the lowest price seen so far; each day's best profit is today's price minus that minimum. One pass, O(1) space.

  2. Maximum Average Subarray IEasyLeetCode 643

    The fixed-size window in its plainest form.

    Fixed window
    Key insight

    Keep a running sum: add the entering element and subtract the leaving one. Compare sums, not averages, since k is constant.

  3. Longest Substring Without Repeating CharactersMediumLeetCode 3

    The canonical variable window, and one of the most asked problems anywhere.

    Variable windowHashing

    Solve first: Two Sum

    Key insight

    Grow the right edge; when the new character is already in the window, move the left edge past its previous position. Storing each character's last index lets the left edge jump directly.

Variations 6

The same idea with a twist that changes the details.

  1. Minimum Size Subarray SumMediumLeetCode 209

    Flips the goal to the shortest valid window.

    Variable window
    Key insight

    Grow until the sum reaches the target, then shrink from the left while it still does, recording the length each time. This relies on all numbers being positive.

  2. Max Consecutive Ones IIIMediumLeetCode 1004

    Validity defined by a budget: at most k zeros in the window.

    Variable window
    Key insight

    Count zeros in the window and shrink while the count exceeds k. The longest window seen is the answer; you never actually flip anything.

  3. Fruit Into BasketsMediumLeetCode 904

    "Longest window with at most two distinct values", disguised as a story.

    Variable windowHashing
    Key insight

    Keep a count map of the window. When it holds more than two keys, shrink from the left and delete keys whose count reaches zero.

  4. Longest Repeating Character ReplacementMediumLeetCode 424

    A validity condition that needs a small insight to state.

    Variable windowCounting

    Solve first: Max Consecutive Ones III

    Key insight

    A window is fixable when length − count of its most frequent letter ≤ k. You don't need to decrease the stored maximum when shrinking: a stale maximum can only keep the window from growing, never produce a wrong longer answer.

  5. Permutation in StringMediumLeetCode 567

    Fixed window plus frequency matching.

    Fixed windowCounting

    Solve first: Valid Anagram

    Key insight

    Slide a window of length len(s1) over s2 and compare letter counts. Tracking how many of the 26 counts currently match makes each step O(1).

  6. Maximum Points You Can Obtain from CardsMediumLeetCode 1423

    The window is hidden: you pick from the ends, not the middle.

    Fixed window
    Key insight

    Taking k cards from the ends leaves a contiguous block of n − k in the middle. Maximise your points by minimising that middle window's sum.

Combinations and stretch 3

The pattern mixed with another, or pushed harder.

  1. Frequency of the Most Frequent ElementMediumLeetCode 1838

    Sorting first makes a window condition possible.

    Sliding windowSorting
    Key insight

    After sorting, try making every element in a window equal to its largest. The cost is largest × length − window sum; shrink while it exceeds k.

  2. Subarrays with K Different IntegersHardLeetCode 992

    Teaches the "exactly K" trick, which applies well beyond this problem.

    Sliding windowCounting

    Solve first: Fruit Into Baskets

    Key insight

    "Exactly K distinct" isn't monotonic, but "at most K" is. Count windows with at most K and subtract those with at most K − 1. At most K adds right − left + 1 per step.

  3. Minimum Window SubstringHardLeetCode 76

    The hardest standard window problem; it combines everything above.

    Variable windowCounting

    Solve first: Minimum Size Subarray Sum

    Key insight

    Track how many required characters are fully satisfied (have versus need). When all are, shrink from the left to minimise, updating the count as characters leave.

Pattern 4

Prefix sums and difference arrays

Precompute running totals so any range sum is one subtraction. Combined with a hash map, prefix sums count subarrays with a given sum even when numbers are negative, where a sliding window fails.

Recognise it when

  • You need many range sums, or sums of every subarray
  • The question is about subarrays with sum k, or divisible by k
  • The input can contain negative numbers, so a window's validity isn't monotonic
  • Many updates add a value to a range, and you only read the result at the end

Variations

  • Plain prefix array for O(1) range queries
  • Prefix plus hash map: count or find earlier prefixes
  • Prefix of a transformed array (0 becomes −1, values mod k)
  • Prefix and suffix products or maxima
  • 2-D prefix sums; difference arrays for range updates

Core intuition: The sum of a[i..j] is prefix[j + 1] − prefix[i]. Any question about subarray sums becomes a question about pairs of prefixes, which a hash map answers in one pass.

Template

from itertools import accumulate
from collections import defaultdict

prefix = [0] + list(accumulate(a))       # prefix[i] = sum of a[:i]
range_sum = prefix[j + 1] - prefix[i]    # sum of a[i..j] in O(1)

# Count subarrays summing to k: pairs of prefixes that differ by k
count = running = 0
seen = defaultdict(int)
seen[0] = 1                              # the empty prefix
for x in a:
    running += x
    count += seen[running - k]
    seen[running] += 1

# Difference array: add v to a[l..r] for many updates, rebuild once
diff = [0] * (n + 1)
diff[l] += v
diff[r + 1] -= v
final = list(accumulate(diff[:n]))

Subarray Sum Equals K is the middle loop; try it without looking back here.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Range Sum Query: ImmutableEasyLeetCode 303

    The reason prefix sums exist: many range queries on unchanging data.

    Prefix sums
    Key insight

    Build the prefix array once in O(n); each query is prefix[right + 1] − prefix[left]. The extra leading zero removes an edge case.

  2. Find Pivot IndexEasyLeetCode 724

    Left and right sums from one total, without a second array.

    Prefix sums
    Key insight

    Keep a running left sum; the right sum is total − left − nums[i]. The pivot is where they're equal.

  3. Product of Array Except SelfMediumLeetCode 238

    Prefix and suffix passes, and a staple of every major list.

    Prefix and suffix
    Key insight

    The answer at i is (product of everything left of i) × (product of everything right of i). Fill left products forward, then multiply in right products backward, with no division.

Variations 6

The same idea with a twist that changes the details.

  1. Maximum SubarrayMediumLeetCode 53

    Links prefix sums to Kadane's algorithm, which reappears in DP.

    Prefix sumsKadane

    Solve first: Best Time to Buy and Sell Stock

    Key insight

    The best subarray ending at j is prefix[j] − smallest earlier prefix, the same shape as Best Time to Buy and Sell Stock. Kadane's version: keep the best sum ending here, restarting when it goes negative.

  2. Subarray Sum Equals KMediumLeetCode 560

    The key prefix-plus-map problem. A sliding window fails because of negative numbers.

    Prefix sumsHashing

    Solve first: Two Sum

    Key insight

    Count how many earlier prefixes equal running − k. Start the map with {0: 1} so subarrays that begin at index 0 count.

  3. Contiguous ArrayMediumLeetCode 525

    Transform the input first, then apply the pattern.

    Prefix sumsHashing

    Solve first: Subarray Sum Equals K

    Key insight

    Treat 0 as −1, so an equal number of 0s and 1s means a zero sum. Store the first index of each prefix sum; the longest span between equal prefixes is the answer.

  4. Subarray Sums Divisible by KMediumLeetCode 974

    Prefix sums compared by remainder instead of value.

    Prefix sumsHashing

    Solve first: Subarray Sum Equals K

    Key insight

    Two prefixes with the same remainder mod k bound a subarray divisible by k. Count remainders. Python's % already returns a non-negative result for negative numbers; many other languages need a fix.

  5. Range Sum Query 2D: ImmutableMediumLeetCode 304

    Extends prefix sums to grids with inclusion-exclusion.

    2-D prefix sums

    Solve first: Range Sum Query: Immutable

    Key insight

    P[r][c] holds the sum of the rectangle above-left. A query adds the big rectangle, subtracts the two strips, and adds back the overlapping corner.

  6. Corporate Flight BookingsMediumLeetCode 1109

    The canonical difference array: many range updates, one read.

    Difference array
    Key insight

    For each booking add seats at first − 1 and subtract at last; one prefix sum at the end gives every flight's total. O(n + bookings) instead of O(n × bookings).

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. Number of Submatrices That Sum to TargetHardLeetCode 1074

    Combines 2-D prefix sums with Subarray Sum Equals K.

    2-D prefix sumsHashing

    Solve first: Subarray Sum Equals K, Range Sum Query 2D: Immutable

    Key insight

    Fix a pair of rows; the column sums between them form a 1-D array. Count its subarrays summing to the target with the prefix-map trick. O(rows² × cols).

Pattern 6

Linked lists, including fast and slow pointers

Rewire pointers carefully, and use two pointers moving at different speeds to find middles, cycles, and positions from the end without knowing the length.

Recognise it when

  • The input is a linked list
  • You need the middle, or the k-th node from the end, in one pass
  • You must detect a cycle or find where it starts
  • An array's values point to indexes, so it behaves like a linked list

Variations

  • Dummy head to avoid special-casing the first node
  • In-place reversal of all or part of the list
  • Fast and slow pointers: middle, cycle detection, cycle start
  • Gap pointers: move one pointer k steps ahead first
  • Combining steps: find middle, reverse half, merge

Core intuition: Pointer bugs come from losing a reference. Before changing node.next, save whatever you still need. Drawing three boxes on paper is faster than debugging.

Template

# Dummy head: every node, including the first, has a predecessor
dummy = ListNode(0, head)
prev = dummy

# Fast and slow: slow ends at the middle; they meet if there's a cycle
slow = fast = head
while fast and fast.next:
    slow = slow.next
    fast = fast.next.next
    if slow is fast:
        break                        # cycle detected

# Gap pointers: when lead reaches the end, trail is n nodes behind
lead = trail = dummy
for _ in range(n):
    lead = lead.next
while lead.next:
    lead, trail = lead.next, trail.next

Reversal isn't in the template on purpose: Reverse Linked List is the first problem, and doing it by hand is the point.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Reverse Linked ListEasyLeetCode 206

    The most reused linked-list operation. Write it until it's automatic.

    In-place reversal
    Key insight

    Walk the list with prev and cur. Save cur.next, point cur.next at prev, then advance both. Also write the recursive version once.

  2. Merge Two Sorted ListsEasyLeetCode 21

    Merging with a dummy head, the linked-list twin of Merge Sorted Array.

    MergeDummy head
    Key insight

    Build from a dummy node, always attaching the smaller head. When one list runs out, attach the rest of the other in one step.

  3. Linked List CycleEasyLeetCode 141

    Floyd's cycle detection: O(1) space instead of a visited set.

    Fast and slow
    Key insight

    Fast moves two steps, slow one. With a cycle, fast gains one node per step and must land on slow; without one, fast hits None.

Variations 5

The same idea with a twist that changes the details.

  1. Middle of the Linked ListEasyLeetCode 876

    Fast and slow used for position rather than cycles.

    Fast and slow
    Key insight

    When fast reaches the end, slow is at the middle. With an even length, the loop condition decides which of the two middles you get; know which yours returns.

  2. Remove Nth Node From End of ListMediumLeetCode 19

    Gap pointers, plus the dummy head that handles removing the first node.

    Gap pointersDummy head
    Key insight

    Move the lead pointer n steps ahead of the trailing one, then move both until lead reaches the last node. Trail now sits just before the node to delete.

  3. Intersection of Two Linked ListsEasyLeetCode 160

    A neat trick for lists of unknown, different lengths.

    Two pointers
    Key insight

    Walk both pointers; when one reaches the end, restart it at the other list's head. Both then travel a + b steps and meet at the intersection, or both reach None together.

  4. Linked List Cycle IIMediumLeetCode 142

    Finds where the cycle starts, not just whether it exists.

    Fast and slow

    Solve first: Linked List Cycle

    Key insight

    After slow and fast meet, move one pointer back to the head and advance both one step at a time. They meet at the cycle's start; the distances work out exactly.

  5. Add Two NumbersMediumLeetCode 2

    Careful simulation with a carry across two lists of different lengths.

    Dummy headSimulation
    Key insight

    Loop while either list or the carry remains, treating a missing node as 0. Build the result from a dummy node.

Combinations and stretch 4

The pattern mixed with another, or pushed harder.

  1. Reorder ListMediumLeetCode 143

    Three linked-list skills chained together.

    Fast and slowReversalMerge

    Solve first: Reverse Linked List, Middle of the Linked List

    Key insight

    Find the middle with fast and slow, reverse the second half, then interleave the two halves. Cut the list at the middle, or the result loops.

  2. Find the Duplicate NumberMediumLeetCode 287

    Floyd's algorithm on an array: the most surprising reuse in this pattern.

    Fast and slowArray as linked list

    Solve first: Linked List Cycle II

    Key insight

    Treat each index i as a node pointing to nums[i]. A duplicate value means two nodes point to the same place, which forms a cycle whose start is the duplicate.

  3. Copy List with Random PointerMediumLeetCode 138

    Deep-copying a structure whose pointers reach anywhere.

    HashingLinked list
    Key insight

    First pass: map each old node to a new copy. Second pass: set each copy's next and random through the map. An O(1)-space version interleaves copies into the original list.

  4. Reverse Nodes in k-GroupHardLeetCode 25

    Reversal applied repeatedly to segments, with careful reconnection.

    In-place reversalDummy head

    Solve first: Reverse Linked List

    Key insight

    For each group, check k nodes remain, reverse exactly those, then connect the previous group's tail to the new head. A dummy head makes the first group behave like the rest.

Pattern 7

Stacks, monotonic stacks, and monotonic deques

A stack holds unfinished work, where the most recent item is resolved first. Keep it monotonic (always increasing or decreasing) and it finds the next or previous greater or smaller element for every item in O(n) total.

Recognise it when

  • Matching or nesting: brackets, tags, nested encodings
  • Evaluating expressions, or undoing the most recent step
  • For each item, find the next or previous greater or smaller item
  • The maximum or minimum of every sliding window
  • A brute force that looks back or forward from each item is O(n²)

Variations

  • Plain stack for matching and evaluation
  • Stack of pairs or states, as in decoding nested strings
  • Monotonic stack for next or previous greater and smaller
  • Monotonic stack used greedily to build the smallest result
  • Monotonic deque for sliding-window extremes

Core intuition: In a monotonic stack, an item is popped the moment its answer arrives, and every item is pushed and popped at most once. That's why the nested-looking while loop is still O(n).

Template

# Monotonic stack: the next greater element for every index
res = [-1] * len(a)
stack = []                           # indexes still waiting for an answer
for i, x in enumerate(a):
    while stack and a[stack[-1]] < x:
        res[stack.pop()] = x         # x is the next greater for that index
    stack.append(i)
# Indexes left on the stack have no greater element to their right

A monotonic deque works the same way but also drops items from the front when they leave the window. Sliding Window Maximum introduces it.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Valid ParenthesesEasyLeetCode 20

    The canonical stack problem.

    Stack
    Key insight

    Push opening brackets; on a closing bracket, the stack top must be its partner. The string is valid only if the stack is empty at the end.

  2. Min StackMediumLeetCode 155

    Keeping extra state alongside each stack entry.

    StackDesign
    Key insight

    Store the minimum so far with every push, as a pair, or in a second stack. getMin is then the top's stored minimum.

  3. Evaluate Reverse Polish NotationMediumLeetCode 150

    Using a stack to evaluate expressions.

    Stack
    Key insight

    Push numbers; for an operator, pop two, apply it, and push the result. Pop order matters for − and ÷, and division must truncate toward zero: use int(a / b), not //.

Variations 8

The same idea with a twist that changes the details.

  1. Implement Queue using StacksEasyLeetCode 232

    A short lesson in amortised O(1).

    StackAmortised analysis
    Key insight

    Push into an in-stack; pop from an out-stack, refilling it from the in-stack only when it's empty. Each item moves at most twice, so operations are O(1) on average.

  2. Daily TemperaturesMediumLeetCode 739

    The canonical monotonic stack problem.

    Monotonic stack
    Key insight

    Keep a stack of indexes with decreasing temperatures. A warmer day pops every colder index and records the distance i − popped.

  3. Next Greater Element IIMediumLeetCode 503

    The same idea on a circular array.

    Monotonic stack

    Solve first: Daily Temperatures

    Key insight

    Loop over indexes 0 to 2n − 1 using i % n, but only push during the first pass. The second pass gives earlier items a chance to see values that wrap around.

  4. Online Stock SpanMediumLeetCode 901

    A monotonic stack for the previous greater element, processed as a stream.

    Monotonic stackDesign
    Key insight

    Store (price, span) pairs. A new price pops all smaller-or-equal prices and absorbs their spans.

  5. Remove K DigitsMediumLeetCode 402

    A monotonic stack used greedily to build the smallest number.

    Monotonic stackGreedy
    Key insight

    While k > 0 and the stack top is larger than the next digit, pop it: a smaller digit earlier always wins. Remove any remaining k from the end, then strip leading zeros.

  6. Decode StringMediumLeetCode 394

    Nesting handled with a stack of saved states.

    Stack
    Key insight

    On [, push the current string and repeat count, then start fresh. On ], pop and set current = saved + count × current. Multi-digit counts need accumulating.

  7. Asteroid CollisionMediumLeetCode 735

    A stack as a simulation of what's still standing.

    StackSimulation
    Key insight

    Only a left-moving asteroid meeting a right-moving stack top collides. Pop smaller ones, destroy both on a tie, and push the new one only if it survives.

  8. Car FleetMediumLeetCode 853

    Sort first; then the stack question becomes simple.

    Monotonic stackSorting
    Key insight

    Sort cars by position, closest to the target first, and compute each arrival time. A car catches the fleet ahead if its time is ≤ that fleet's time; otherwise it starts a new fleet.

Combinations and stretch 3

The pattern mixed with another, or pushed harder.

  1. Sliding Window MaximumHardLeetCode 239

    Introduces the monotonic deque, joining this pattern with sliding windows.

    Monotonic dequeSliding window

    Solve first: Daily Temperatures, Maximum Average Subarray I

    Key insight

    Keep indexes in a deque with decreasing values. Pop smaller values from the back before pushing; pop the front when it leaves the window. The front is always the window maximum.

  2. Largest Rectangle in HistogramHardLeetCode 84

    The classic hard monotonic stack problem.

    Monotonic stack

    Solve first: Daily Temperatures

    Key insight

    Keep an increasing stack of indexes. When a shorter bar arrives, pop taller bars: each popped bar's rectangle extends from the new stack top + 1 to the current index − 1. A final height of 0 flushes the stack.

  3. Shortest Subarray with Sum at Least KHardLeetCode 862

    What to do when Minimum Size Subarray Sum meets negative numbers.

    Monotonic dequePrefix sums

    Solve first: Minimum Size Subarray Sum, Sliding Window Maximum

    Key insight

    Keep a deque of prefix indexes with increasing prefix sums. Pop from the front while the current prefix minus the front's is ≥ k (recording lengths), and from the back while the back's prefix is ≥ the current one.

Pattern 8

Intervals and sweep line

Sort intervals so overlaps become neighbours, then make one pass. For "how many at once" questions, turn intervals into start and end events and sweep through them in order.

Recognise it when

  • The input is ranges, meetings, bookings, or time slots
  • You're asked to merge, insert, or remove overlapping ranges
  • You need the most overlapping at any moment, or the rooms or groups required
  • Two sorted lists of ranges must be intersected

Variations

  • Sort by start, then merge with the last kept interval
  • Sort by end to keep the most non-overlapping ones (greedy)
  • Sweep line: +1 at a start, −1 at an end
  • Two pointers across two interval lists

Core intuition: Two intervals [a, b] and [c, d] overlap exactly when a ≤ d and c ≤ b. Once sorted by start, you only ever compare an interval with the last one you kept.

Template

# Sweep line: the most intervals overlapping at any moment
events = []
for start, end in intervals:
    events.append((start, 1))
    events.append((end, -1))     # half-open [start, end): at a tie, ends sort first
events.sort()
active = peak = 0
for _, delta in events:
    active += delta
    peak = max(peak, active)

For closed intervals, where touching counts as overlapping, record the end event at end + 1 instead.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Merge IntervalsMediumLeetCode 56

    The canonical interval problem.

    IntervalsSorting
    Key insight

    Sort by start. If an interval starts at or before the last merged interval's end, extend that end with max; otherwise start a new merged interval.

  2. Insert IntervalMediumLeetCode 57

    Merging without sorting, since the input is already in order.

    Intervals

    Solve first: Merge Intervals

    Key insight

    Copy intervals that end before the new one starts, merge every interval that overlaps it into one, then copy the rest.

  3. Non-overlapping IntervalsMediumLeetCode 435

    Sorting by end instead of start, and why that's the greedy choice.

    IntervalsGreedy
    Key insight

    Sort by end and keep an interval whenever it starts at or after the last kept end. Finishing earliest leaves the most room; the answer is the number you didn't keep.

Variations 5

The same idea with a twist that changes the details.

  1. Interval List IntersectionsMediumLeetCode 986

    Two pointers over two sorted interval lists.

    IntervalsTwo pointers

    Solve first: Merge Sorted Array

    Key insight

    The overlap of the current pair is [max(starts), min(ends)] if non-empty. Then advance whichever interval ends first.

  2. Remove Covered IntervalsMediumLeetCode 1288

    A two-key sort that makes one pass enough.

    IntervalsSorting
    Key insight

    Sort by start ascending and end descending. Then an interval is covered exactly when its end is ≤ the largest end seen so far.

  3. My Calendar IMediumLeetCode 729

    Overlap checks inside a data structure.

    IntervalsDesign
    Key insight

    A booking conflicts with an existing one when start < other_end and other_start < end. A list check is O(n) per booking; a sorted list with bisect checks only the neighbours.

  4. Car PoolingMediumLeetCode 1094

    A sweep line with a capacity check.

    Sweep lineDifference array

    Solve first: Corporate Flight Bookings

    Key insight

    Add passengers at each pickup and remove them at drop-off. Process events in order (drop-offs first at the same stop) and fail if the load ever exceeds capacity. Locations are small, so a difference array also works.

  5. Divide Intervals Into Minimum Number of GroupsMediumLeetCode 2406

    "Meeting Rooms II" as a free problem: the minimum groups equals the maximum overlap.

    Sweep lineHeap
    Key insight

    The answer is the most intervals active at once. These intervals are closed, so a start equal to an end still overlaps; sweep with the end event at end + 1, or sort starts and use a min-heap of ends.

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. Minimum Interval to Include Each QueryHardLeetCode 1851

    Sorting both intervals and queries, then a heap. Come back to it after the Heaps pattern.

    IntervalsHeapSorting

    Solve first: Merge Intervals, and the Heaps pattern

    Key insight

    Sort intervals by start and queries by value. For each query, push intervals that have started (keyed by size), pop those that have ended, and the heap top is the smallest interval containing it.

Stage 2

Recursion and trees

Thinking in subproblems. Backtracking comes first because a tree DFS is the same idea on a fixed shape; heaps close the stage because they're trees you use rather than traverse.

Pattern 9

Recursion and backtracking

Build a solution one choice at a time; when a partial solution can't succeed, undo the last choice and try the next. It's a DFS over a tree of decisions, and it's how you generate subsets, permutations, combinations, and constrained arrangements.

Recognise it when

  • "Generate all", "list every", or "count all" arrangements
  • The input is small, often n ≤ 15 to 20, so exponential time is expected
  • Each step is a choice among options, and some partial choices are clearly invalid
  • A problem splits into smaller copies of itself (plain recursion or divide and conquer)

Variations

  • Subsets: include or exclude each item
  • Combinations: choose from a start index onward, with or without reuse
  • Permutations: any unused item in each position
  • Partitioning: choose where the next piece ends
  • Constraint search: grids and boards with pruning

Core intuition: Every backtracking solution is choose, explore, un-choose. Duplicates in the input are handled the same way everywhere: sort, then skip an item equal to the previous one at the same level of the recursion.

Template

def backtrack(start, path):
    if is_complete(path):
        results.append(path[:])      # copy: path keeps changing
        return
    for i in range(start, len(choices)):
        if i > start and choices[i] == choices[i - 1]:
            continue                 # skip duplicate siblings (input sorted)
        if not allowed(i, path):
            continue                 # prune as early as possible
        path.append(choices[i])      # choose
        backtrack(i + 1, path)       # explore (pass i instead to allow reuse)
        path.pop()                   # un-choose

results = []
backtrack(0, [])

Pseudocode: is_complete and allowed are the problem's rules. Drop the duplicate check when the input has no repeats.

Canonical 4

The pattern in its plainest form. Solve these first.

  1. Pow(x, n)MediumLeetCode 50

    Plain recursion that halves the problem, before any backtracking.

    RecursionDivide and conquer
    Key insight

    xn = (xn/2)², times x when n is odd, which makes it O(log n). For negative n, compute 1 / x−n.

  2. SubsetsMediumLeetCode 78

    The decision tree in its simplest form: every element is in or out.

    Backtracking
    Key insight

    There are 2ⁿ subsets. Either branch on include or exclude for each element, or loop from start and record the path at every call; every node of that recursion tree is a subset.

  3. PermutationsMediumLeetCode 46

    Order matters, so any unused element can go in each position.

    Backtracking
    Key insight

    Track used elements with a boolean list (or swap elements in place). There are n! results, and each costs O(n) to copy.

  4. Combination SumMediumLeetCode 39

    Combinations where a number can be reused, with pruning on the running sum.

    Backtracking
    Key insight

    Recurse with the same index i rather than i + 1 so a number can repeat. Sort first, and break out of the loop once a number exceeds the remaining target.

Variations 7

The same idea with a twist that changes the details.

  1. Subsets IIMediumLeetCode 90

    Introduces the standard way to handle duplicate input.

    BacktrackingDuplicates

    Solve first: Subsets

    Key insight

    Sort, then skip a value equal to the previous one at the same level: i > start and nums[i] == nums[i − 1]. Equal values may appear down one path, but never as two siblings.

  2. Combination Sum IIMediumLeetCode 40

    Combines two earlier ideas: no reuse, and no duplicate combinations.

    BacktrackingDuplicates

    Solve first: Combination Sum, Subsets II

    Key insight

    Move to i + 1 so each number is used once, and skip equal siblings as in Subsets II.

  3. Permutations IIMediumLeetCode 47

    Duplicates in permutations need a slightly different rule.

    BacktrackingDuplicates

    Solve first: Permutations, Subsets II

    Key insight

    Sort, then skip nums[i] when it equals nums[i − 1] and that previous copy isn't currently used. This forces equal values into a fixed order, so each arrangement appears once.

  4. Letter Combinations of a Phone NumberMediumLeetCode 17

    A fixed number of levels, with a different set of choices at each.

    Backtracking
    Key insight

    One recursion level per digit and one branch per letter it maps to. An empty input returns an empty list, not a list holding an empty string.

  5. Generate ParenthesesMediumLeetCode 22

    Pruning so strong that no invalid string is ever built.

    BacktrackingPruning
    Key insight

    Add ( while fewer than n are open, and ) only while fewer closes than opens have been placed. Every complete path is then valid; nothing needs filtering.

  6. Palindrome PartitioningMediumLeetCode 131

    Partitioning: the choice is where the next piece ends.

    BacktrackingPartitioning

    Solve first: Subsets

    Key insight

    From position start, try each end index and recurse only when that piece is a palindrome. A precomputed palindrome table avoids re-checking the same substrings.

  7. Word SearchMediumLeetCode 79

    Backtracking on a grid, where "un-choose" means restoring a cell.

    BacktrackingGrid

    Solve first: Permutations

    Key insight

    DFS from every cell, marking the current cell as used (overwrite it temporarily) and restoring it on the way back. Prune by checking the board has enough of each letter before searching.

Combinations and stretch 4

The pattern mixed with another, or pushed harder.

  1. Partition to K Equal Sum SubsetsMediumLeetCode 698

    Pruning decides whether this finishes in time.

    BacktrackingPruning

    Solve first: Combination Sum

    Key insight

    Each bucket must reach sum / k. Place numbers largest first so failures appear early, and skip a bucket whose current sum equals one already tried for this number; it would lead to the same result.

  2. Different Ways to Add ParenthesesMediumLeetCode 241

    Divide and conquer that returns lists of results, a bridge towards DP.

    Divide and conquerMemoisation

    Solve first: Pow(x, n)

    Key insight

    Split at every operator, solve the left and right sides recursively, and combine every pair of results. Memoise on the substring, because the same pieces recur.

  3. N-QueensHardLeetCode 51

    The classic constraint-search problem.

    BacktrackingConstraint sets

    Solve first: Permutations

    Key insight

    Place one queen per row. Keep sets of used columns and both diagonals; a cell's diagonals are r − c and r + c, so every check is O(1).

  4. Sudoku SolverHardLeetCode 37

    Constraint search where the order of choices matters for speed.

    BacktrackingConstraint sets

    Solve first: N-Queens, Valid Sudoku

    Key insight

    Keep row, column and box sets; try digits in an empty cell and undo on failure. Filling the empty cell with the fewest options first cuts the search dramatically.

Pattern 10

Trees: DFS, BFS, and binary search trees

Most tree problems are a DFS where each node combines answers from its two children, or a BFS that processes one level at a time. In a binary search tree, the ordering lets you skip half the tree at each step.

Recognise it when

  • The input is a tree, or can be turned into one
  • The answer depends on paths, heights, or subtrees
  • Anything "per level": views, averages, widths
  • A BST, where in-order traversal gives sorted values

Variations

  • Bottom-up DFS: children return values, the parent combines them
  • Top-down DFS: pass state such as a running maximum or path downward
  • Global answer updated at each node while returning something else
  • BFS level by level
  • BST: use the ordering to search, validate, or build

Core intuition: Before writing a recursive tree function, decide exactly what it returns for one subtree. When the answer can pass through a node without going to its parent (a diameter, a path sum), return one thing and update a separate global answer.

Template

# Bottom-up DFS: each call reports a value about its subtree
def dfs(node):
    if not node:
        return 0                     # value for an empty subtree
    left, right = dfs(node.left), dfs(node.right)
    # update a global answer here if it can pass through this node
    return 1 + max(left, right)      # what this subtree tells its parent

# BFS, one level at a time
from collections import deque
def by_level(root):
    out, queue = [], deque([root] if root else [])
    while queue:
        level = []
        for _ in range(len(queue)):  # exactly this level's nodes
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        out.append(level)
    return out

Level Order Traversal is the BFS half of this template; solve it once without looking.

Canonical 4

The pattern in its plainest form. Solve these first.

  1. Maximum Depth of Binary TreeEasyLeetCode 104

    The simplest bottom-up DFS.

    DFSBFS
    Key insight

    Depth is 1 + the deeper child's depth, and 0 for an empty tree. Solve it with BFS too, counting levels.

  2. Invert Binary TreeEasyLeetCode 226

    A structural change applied at every node.

    DFS
    Key insight

    Swap each node's children, then invert both subtrees. Any traversal order works as long as every node is visited once.

  3. Same TreeEasyLeetCode 100

    Recursion over two trees at once.

    DFS
    Key insight

    Two trees match when both are empty, or both roots are equal and both pairs of subtrees match. Handle the "one is empty" case before reading values.

  4. Binary Tree Level Order TraversalMediumLeetCode 102

    The BFS template, and the base for every level-based question.

    BFS
    Key insight

    Record len(queue) before the inner loop and pop exactly that many nodes. That boundary is what separates one level from the next.

Variations 15

The same idea with a twist that changes the details.

  1. Diameter of Binary TreeEasyLeetCode 543

    The first "return one thing, update another" problem.

    DFSGlobal answer

    Solve first: Maximum Depth of Binary Tree

    Key insight

    The longest path through a node is left height + right height. Return the height to the parent, but update a global diameter at every node; the answer may not pass through the root.

  2. Balanced Binary TreeEasyLeetCode 110

    Returning a sentinel to avoid repeated work.

    DFS

    Solve first: Maximum Depth of Binary Tree

    Key insight

    Return the height, or −1 once any subtree is unbalanced, and propagate the −1 upward. One O(n) pass instead of recomputing heights at every node in O(n²).

  3. Symmetric TreeEasyLeetCode 101

    Same Tree, but comparing mirrored positions.

    DFS

    Solve first: Same Tree

    Key insight

    Compare the two subtrees as mirrors: left.left against right.right, and left.right against right.left.

  4. Subtree of Another TreeEasyLeetCode 572

    Reusing a helper at every node.

    DFS

    Solve first: Same Tree

    Key insight

    At each node of the main tree, run Same Tree against the smaller tree. O(m × n) is accepted; serialising both trees and matching strings brings it down to O(m + n).

  5. Count Good Nodes in Binary TreeMediumLeetCode 1448

    Top-down DFS: state flows from parent to child.

    Top-down DFS
    Key insight

    Pass down the largest value seen on the path from the root. A node is good when its value is ≥ that maximum.

  6. Path Sum IIMediumLeetCode 113

    Backtracking on a tree: build a path, record it, undo.

    DFSBacktracking

    Solve first: Combination Sum

    Key insight

    Carry the path and the remaining sum down. At a leaf with remaining 0, save a copy of the path. Pop the node after both recursive calls.

  7. Lowest Common Ancestor of a Binary TreeMediumLeetCode 236

    A bottom-up search where the return value carries meaning.

    DFS
    Key insight

    If the node is p or q, return it. Otherwise search both sides: if both return a node, this is the LCA; if only one does, pass that result up.

  8. Binary Tree Right Side ViewMediumLeetCode 199

    A per-level question answered with the BFS template.

    BFS

    Solve first: Binary Tree Level Order Traversal

    Key insight

    Take the last node of each level. Alternatively, a DFS visiting right before left can record the first node seen at each new depth.

  9. Maximum Width of Binary TreeMediumLeetCode 662

    Positions, not just values, matter within a level.

    BFSIndexing

    Solve first: Binary Tree Level Order Traversal

    Key insight

    Number nodes as in a heap: the children of i are 2i and 2i + 1. A level's width is the last index − the first + 1. Python's integers don't overflow, but re-basing indexes each level keeps them small.

  10. Validate Binary Search TreeMediumLeetCode 98

    The BST property is about whole subtrees, not just children.

    BSTDFS
    Key insight

    Pass down the allowed range (low, high). Comparing a node only with its children misses violations further down. Equivalently, an in-order traversal must be strictly increasing.

  11. Kth Smallest Element in a BSTMediumLeetCode 230

    In-order traversal is sorted order.

    BSTIn-order

    Solve first: Validate Binary Search Tree

    Key insight

    Traverse in order and stop at the k-th node. An iterative in-order with an explicit stack makes stopping early natural.

  12. Lowest Common Ancestor of a Binary Search TreeMediumLeetCode 235

    The same question as the general tree, answered faster with ordering.

    BST

    Solve first: Lowest Common Ancestor of a Binary Tree

    Key insight

    If both values are smaller, go left; if both are larger, go right. Otherwise this node separates them and is the LCA. O(h), with no need to search both sides.

  13. Convert Sorted Array to Binary Search TreeEasyLeetCode 108

    Building a balanced tree from sorted data.

    BSTDivide and conquer
    Key insight

    Make the middle element the root so both sides are the same size, and build each half recursively.

  14. Delete Node in a BSTMediumLeetCode 450

    Changing a BST's structure while keeping it valid.

    BST

    Solve first: Lowest Common Ancestor of a Binary Search Tree

    Key insight

    With zero or one child, replace the node with that child. With two, copy in the smallest value from the right subtree, then delete that value from the right subtree.

  15. Binary Search Tree IteratorMediumLeetCode 173

    In-order traversal done lazily, one step at a time.

    BSTStackDesign

    Solve first: Kth Smallest Element in a BST

    Key insight

    Push the left spine. On next(), pop a node and push the left spine of its right child. Every node is pushed once, so each call is O(1) on average.

Combinations and stretch 5

The pattern mixed with another, or pushed harder.

  1. Construct Binary Tree from Preorder and Inorder TraversalMediumLeetCode 105

    Rebuilding a tree from two traversals.

    DFSHashing

    Solve first: Convert Sorted Array to Binary Search Tree

    Key insight

    The next preorder value is the root; its position in the inorder list splits the left and right subtrees. Map values to inorder indexes first so each split is O(1).

  2. Path Sum IIIMediumLeetCode 437

    Prefix sums from Stage 1 applied along tree paths.

    DFSPrefix sums

    Solve first: Subarray Sum Equals K, Path Sum II

    Key insight

    It's Subarray Sum Equals K on every root-to-node path. Keep a count map of prefix sums during the DFS, and decrement the current sum when leaving a node.

  3. Binary Tree Maximum Path SumHardLeetCode 124

    The hardest version of "return one thing, update another".

    DFSGlobal answer

    Solve first: Diameter of Binary Tree

    Key insight

    Each node returns its value plus the better of its children's downward paths, ignoring negative ones with max(0, …). The global answer considers the node plus both children.

  4. All Nodes Distance K in Binary TreeMediumLeetCode 863

    Turns a tree into a graph, a bridge to Stage 3.

    Tree to graphBFS

    Solve first: Binary Tree Level Order Traversal

    Key insight

    Record each node's parent with one DFS, so you can move up as well as down. Then BFS k steps from the target, with a visited set.

  5. Serialize and Deserialize Binary TreeHardLeetCode 297

    Designing a format that captures structure, not just values.

    DFSDesign

    Solve first: Binary Tree Level Order Traversal

    Key insight

    A preorder traversal with a marker for missing children describes the tree completely. Deserialise by reading tokens in the same order from an iterator.

Pattern 11

Heaps: top-K, two heaps, and k-way merge

A heap gives the smallest (or largest) item of a changing collection in O(log n). Use it to keep the best k items, to merge several sorted sources, or to split data into two halves for running medians.

Recognise it when

  • "k-th largest", "top k", "k closest", or "k most frequent"
  • You repeatedly need the next smallest or largest as items arrive
  • Several sorted lists or streams must be combined
  • A running median, or a scheduling choice of "best available next"

Variations

  • Size-k heap: keep the k best, discard the rest
  • Greedy with a heap: always take the best available option
  • K-way merge: heap of the current head of each list
  • Two heaps: lower half in a max-heap, upper half in a min-heap

Core intuition: To keep the k largest items, use a min-heap of size k: its top is the weakest of the k, the one to evict. It sounds backwards the first time, and it's the most common heap idea.

Template

import heapq

# Top k largest: a min-heap of size k; its root is the k-th largest
heap = []
for x in items:
    heapq.heappush(heap, x)
    if len(heap) > k:
        heapq.heappop(heap)          # evict the smallest

# K-way merge: always take the smallest current head
heap = [(lst[0], i, 0) for i, lst in enumerate(lists) if lst]
heapq.heapify(heap)
merged = []
while heap:
    val, i, j = heapq.heappop(heap)  # i breaks ties between equal values
    merged.append(val)
    if j + 1 < len(lists[i]):
        heapq.heappush(heap, (lists[i][j + 1], i, j + 1))

Python's heapq is a min-heap. For a max-heap, push negated values (see the Heaps card in Prerequisites).

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Kth Largest Element in a StreamEasyLeetCode 703

    The size-k heap in its plainest form.

    HeapTop-K
    Key insight

    Keep a min-heap of exactly k elements; its top is the k-th largest. Each add is O(log k).

  2. Last Stone WeightEasyLeetCode 1046

    A max-heap in Python, and a simple heap simulation.

    Heap
    Key insight

    Push negated weights. Pop the two heaviest and push back their difference if it isn't zero.

  3. K Closest Points to OriginMediumLeetCode 973

    Top-K where you keep the smallest, so the heap flips to a max-heap.

    HeapTop-K

    Solve first: Kth Largest Element in a Stream

    Key insight

    Keep a max-heap of size k by pushing negated distances. Compare squared distances; the square root never changes the order.

Variations 6

The same idea with a twist that changes the details.

  1. Kth Largest Element in an ArrayMediumLeetCode 215

    The classic place to compare a heap with quickselect.

    HeapQuickselect

    Solve first: Kth Largest Element in a Stream

    Key insight

    A size-k heap is O(n log k). Quickselect partitions like quicksort but recurses into one side only: O(n) on average, O(n²) in the worst case unless the pivot is random.

  2. Top K Frequent WordsMediumLeetCode 692

    Top-K with a tie-break rule that runs in the opposite direction.

    HeapCounting

    Solve first: Top K Frequent Elements

    Key insight

    Order by (−count, word). A size-k heap would need both keys inverted, which is awkward in Python; heapq.nsmallest(k, words, key=lambda w: (−count[w], w)) expresses it directly.

  3. Task SchedulerMediumLeetCode 621

    A greedy heap simulation that also has a closed-form answer.

    HeapGreedy
    Key insight

    The most frequent task sets the frame: (max_count − 1) × (n + 1) + tasks_with_max_count. If that's less than the number of tasks, the answer is the number of tasks. Simulating with a heap gives the same result.

  4. Reorganize StringMediumLeetCode 767

    Greedy with a heap, plus holding an item back for one turn.

    HeapGreedy

    Solve first: Task Scheduler

    Key insight

    Always place the most frequent character that isn't the one just placed: pop it, and push the previous character back only after this step. It's impossible exactly when some count exceeds (len + 1) // 2.

  5. Merge k Sorted ListsHardLeetCode 23

    The canonical k-way merge.

    HeapK-way merge

    Solve first: Merge Two Sorted Lists

    Key insight

    Push each list's head as (value, list index, node); the index breaks ties because nodes can't be compared. Pop the smallest, attach it, push its next. O(N log k).

  6. Find Median from Data StreamHardLeetCode 295

    The canonical two-heap problem.

    Two heaps

    Solve first: Kth Largest Element in a Stream

    Key insight

    Keep the lower half in a max-heap and the upper half in a min-heap, with sizes differing by at most one. The median is the larger heap's top, or the average of both tops.

Combinations and stretch 3

The pattern mixed with another, or pushed harder.

  1. Find K Pairs with Smallest SumsMediumLeetCode 373

    Expanding a frontier instead of generating every pair.

    HeapFrontier

    Solve first: Merge k Sorted Lists

    Key insight

    Start with pairs (i, 0) for the first k values of i. Popping (i, j) pushes (i, j + 1). You never look at more than about k pairs.

  2. IPOHardLeetCode 502

    Sorting plus a heap to pick the best option you can currently afford.

    HeapGreedySorting

    Solve first: Last Stone Weight

    Key insight

    Sort projects by capital. Each round, push the profits of every project now affordable into a max-heap, then take the most profitable. Taking the best affordable project is always safe.

  3. Smallest Range Covering Elements from K ListsHardLeetCode 632

    K-way merge while tracking a moving range.

    HeapK-way merge

    Solve first: Merge k Sorted Lists

    Key insight

    Keep one element from each list in a min-heap and track the current maximum. [heap top, maximum] covers every list; advance the list holding the minimum, and stop when any list runs out.

Stage 3

Graphs

Trees without the rules: cycles, many components, directions, and weights. Traversal first, then the three specialised tools that answer ordering, grouping, and cost questions.

Pattern 12

Graph traversal: DFS, BFS, and grids

Explore everything reachable from a starting point, marking nodes as visited. DFS is simplest for connectivity and regions; BFS explores in rings, so it finds shortest paths when every edge has the same cost.

Recognise it when

  • A grid of cells, islands, regions, or mazes
  • "Connected", "reachable", "how many groups"
  • Fewest steps or moves when every move costs the same
  • The graph is implicit: states (a lock combination, a word) and the moves between them

Variations

  • Count components: start a new search from each unvisited node
  • Multi-source BFS: start from every source at once
  • Reverse the question: search from the border or the target instead
  • Two-colouring for bipartite checks
  • BFS over (node, extra state), such as keys collected

Core intuition: The only difference between DFS and BFS is the container: a stack or a queue. Mark nodes when you add them, and BFS's first visit to a node is its shortest distance.

Template

from collections import deque

# BFS from one or many sources: shortest distances when every edge costs 1
def bfs(sources, neighbours):
    dist = {s: 0 for s in sources}
    queue = deque(sources)
    while queue:
        node = queue.popleft()
        for nxt in neighbours(node):
            if nxt not in dist:           # first visit is the shortest
                dist[nxt] = dist[node] + 1
                queue.append(nxt)
    return dist

# Grid neighbours as a function, so the same BFS works on grids
def grid_neighbours(grid):
    rows, cols = len(grid), len(grid[0])
    def neighbours(cell):
        r, c = cell
        for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != "#":
                yield nr, nc
    return neighbours

For DFS, swap the deque for a list and popleft() for pop(). Iterative DFS avoids Python's recursion limit on large grids.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Find if Path Exists in GraphEasyLeetCode 1971

    Building an adjacency list from an edge list, then a plain search.

    BFSDFS
    Key insight

    Add each edge in both directions, then search from the source until you reach the destination. With up to 2 × 10⁵ nodes, use BFS or an iterative DFS; recursion would hit Python's limit.

  2. Number of IslandsMediumLeetCode 200

    The canonical grid problem: counting connected components.

    DFSBFSGrid
    Key insight

    Scan every cell; each unvisited land cell starts a new island, and a search marks all the land connected to it. The answer is the number of searches started.

  3. Clone GraphMediumLeetCode 133

    Traversal that builds a copy as it goes.

    BFSHashing
    Key insight

    Map each original node to its copy. Create the copy the first time you see a node, and connect neighbours through the map; the map also works as the visited set.

Variations 8

The same idea with a twist that changes the details.

  1. Max Area of IslandMediumLeetCode 695

    Each search returns a value instead of just marking.

    DFSGrid

    Solve first: Number of Islands

    Key insight

    The same traversal as Number of Islands, but each search returns how many cells it covered. Take the maximum.

  2. Rotting OrangesMediumLeetCode 994

    Introduces multi-source BFS, where time is the number of levels.

    Multi-source BFS

    Solve first: Number of Islands

    Key insight

    Put every rotten orange in the queue at the start. Each BFS level is one minute; if fresh oranges remain when the queue empties, return −1.

  3. 01 MatrixMediumLeetCode 542

    Reversing the direction of the search to save work.

    Multi-source BFS

    Solve first: Rotting Oranges

    Key insight

    Rather than a BFS from every 1 to its nearest 0, run one BFS from all 0s together. A cell's first visit gives its distance.

  4. Pacific Atlantic Water FlowMediumLeetCode 417

    Searching backwards from the goal instead of from every start.

    DFSReverse search

    Solve first: Number of Islands

    Key insight

    Start from each ocean's border and move to neighbours of equal or greater height. Cells reached from both oceans are the answer.

  5. Surrounded RegionsMediumLeetCode 130

    Mark what survives, then flip everything else.

    DFSReverse search

    Solve first: Pacific Atlantic Water Flow

    Key insight

    Only regions touching the border survive. Mark every O connected to the border first, then turn the remaining Os into X and restore the marked ones.

  6. Is Graph Bipartite?MediumLeetCode 785

    Traversal that assigns a label and checks a rule.

    BFSTwo-colouring
    Key insight

    Colour each node and give its neighbours the opposite colour; a neighbour with the same colour means it isn't bipartite. Start from every uncoloured node, since the graph may be disconnected.

  7. Shortest Path in Binary MatrixMediumLeetCode 1091

    BFS for the shortest path, with eight directions.

    BFSShortest path

    Solve first: Rotting Oranges

    Key insight

    BFS gives shortest paths when every step costs the same. Neighbours include diagonals, and check that the start and end cells are open before searching.

  8. Open the LockMediumLeetCode 752

    A graph that's never given to you: states and moves.

    BFSImplicit graph
    Key insight

    Each combination has eight neighbours, one wheel turned up or down. BFS from "0000", treating deadends as already visited.

Combinations and stretch 3

The pattern mixed with another, or pushed harder.

  1. Shortest BridgeMediumLeetCode 934

    DFS and BFS in sequence, each doing what it's best at.

    DFSMulti-source BFS

    Solve first: Number of Islands, Rotting Oranges

    Key insight

    Find one island with DFS and put all its cells in the queue. Then BFS outward over water; the first time you touch the other island, the level is the answer.

  2. Word LadderHardLeetCode 127

    BFS on an implicit graph where finding neighbours is the hard part.

    BFSImplicit graphHashing

    Solve first: Open the Lock, Group Anagrams

    Key insight

    Neighbours differ by one letter. Bucket words by wildcard patterns such as h*t, so finding neighbours means looking up L patterns rather than comparing with every word.

  3. Shortest Path to Get All KeysHardLeetCode 864

    BFS where a node is a cell plus what you're carrying.

    BFSBitmask state

    Solve first: Shortest Path in Binary Matrix

    Key insight

    The state is (row, column, keys held), with keys as a bitmask. The same cell with different keys is a different node; you're done when the mask has every key.

Pattern 13

Topological sort

Order the nodes of a directed graph so every edge points forward: each task after its prerequisites. It exists exactly when the graph has no cycle, so the same algorithm doubles as cycle detection.

Recognise it when

  • Prerequisites, dependencies, or build order
  • "Can all tasks be finished?", which is cycle detection in a directed graph
  • Longest or earliest-finish time when tasks depend on each other
  • A grid or graph where moves only go "upward", which makes it acyclic

Variations

  • Kahn's algorithm: repeatedly remove nodes with no remaining prerequisites
  • DFS with three colours: unvisited, in progress, done
  • DP along a topological order, such as longest path in a DAG
  • Peeling leaves from an undirected tree

Core intuition: A node with in-degree zero has nothing blocking it, so it can go next. Removing it may free up others. If you get stuck before placing every node, the rest form a cycle.

Template

from collections import deque, defaultdict

def topo_order(n, edges):              # edge (a, b): a must come before b
    graph = defaultdict(list)
    indegree = [0] * n
    for a, b in edges:
        graph[a].append(b)
        indegree[b] += 1
    queue = deque(i for i in range(n) if indegree[i] == 0)
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for nxt in graph[node]:
            indegree[nxt] -= 1
            if indegree[nxt] == 0:
                queue.append(nxt)
    return order if len(order) == n else []   # shorter means a cycle

Course Schedule II is this function almost exactly; check which way its pairs point before building edges.

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Course ScheduleMediumLeetCode 207

    The canonical topological sort question.

    Topological sortCycle detection
    Key insight

    You can finish every course exactly when the prerequisite graph has no cycle. Run Kahn's algorithm and check whether it places every course. A three-colour DFS works too.

  2. Course Schedule IIMediumLeetCode 210

    Return the order itself.

    Topological sort

    Solve first: Course Schedule

    Key insight

    Kahn's order is a valid answer. Watch the direction: [a, b] means b comes before a, so the edge is b → a.

Variations 2

The same idea with a twist that changes the details.

  1. Find Eventual Safe StatesMediumLeetCode 802

    Kahn's algorithm on the reversed graph.

    Topological sortReverse graph

    Solve first: Course Schedule

    Key insight

    A node is safe when every path from it ends at a terminal node. Reverse the edges and run Kahn's from the terminal nodes; every node it removes is safe. A three-colour DFS also works: anything that reaches a cycle is unsafe.

  2. Minimum Height TreesMediumLeetCode 310

    Topological-style peeling on an undirected tree.

    Leaf peelingBFS

    Solve first: Course Schedule

    Key insight

    The best roots are the tree's centre, one or two nodes. Remove all current leaves layer by layer until at most two nodes remain.

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Parallel Courses IIIHardLeetCode 2050

    DP along a topological order: the longest path in a DAG.

    Topological sortDP

    Solve first: Course Schedule II

    Key insight

    Process courses in topological order. A course finishes at its own time plus the latest finish among its prerequisites; the answer is the largest finish time.

  2. Longest Increasing Path in a MatrixHardLeetCode 329

    Spotting a DAG hidden in a grid.

    DAGDFS with memo

    Solve first: Course Schedule, Number of Islands

    Key insight

    Moving to a strictly larger neighbour can never loop, so the grid is a DAG. DFS with memoisation gives each cell's longest path in O(rows × cols) overall; peeling cells in topological order also works.

Pattern 14

Union-find

A disjoint-set structure that tracks which items belong to the same group. Joining two groups and asking "same group?" are both nearly O(1), which makes it ideal when connections arrive one at a time.

Recognise it when

  • Connected components, especially as edges are added
  • "Would this edge create a cycle?" in an undirected graph
  • Grouping items that are linked by equality or shared attributes
  • Kruskal's minimum spanning tree

Variations

  • Count components: each successful union removes one
  • Detect the edge that closes a cycle
  • Union first, then check constraints
  • Union indexes, then work within each group
  • Weighted union-find that stores ratios to the root

Core intuition: Each group has a representative, its root. find follows parents to the root; union points one root at the other. Path compression and union by size keep the trees almost flat.

Template

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

    def find(self, x):
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]   # path halving
            x = self.parent[x]
        return x

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False                   # already in the same group
        if self.size[ra] < self.size[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra               # attach the smaller tree
        self.size[ra] += self.size[rb]
        return True

The iterative find avoids Python's recursion limit. Returning False from union is how you detect a cycle.

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Number of ProvincesMediumLeetCode 547

    Counting components, solvable by both union-find and DFS.

    Union-findDFS
    Key insight

    Union every connected pair; the number of distinct roots is the answer. Solve it with DFS too and compare: union-find wins when edges arrive over time.

  2. Redundant ConnectionMediumLeetCode 684

    Cycle detection in an undirected graph as edges are added.

    Union-findCycle detection

    Solve first: Number of Provinces

    Key insight

    Add edges in order. The first edge whose endpoints already share a root would close a cycle, so it's the answer.

Variations 4

The same idea with a twist that changes the details.

  1. Number of Operations to Make Network ConnectedMediumLeetCode 1319

    Turning a component count into an answer.

    Union-find

    Solve first: Number of Provinces

    Key insight

    You need at least n − 1 cables. If you have enough, the answer is the number of components minus one, since each spare cable joins two components.

  2. Satisfiability of Equality EquationsMediumLeetCode 990

    Union first, then check constraints.

    Union-find
    Key insight

    Union every == pair first, then check each != pair: it fails if both sides share a root. Processing both kinds together in input order gives wrong answers.

  3. Accounts MergeMediumLeetCode 721

    Union-find over strings instead of numbered nodes.

    Union-findHashing

    Solve first: Number of Provinces

    Key insight

    Union every email in an account with that account's first email. Then group emails by root, sort each group, and put the owner's name in front.

  4. Smallest String With SwapsMediumLeetCode 1202

    Grouping positions, then working inside each group.

    Union-findSorting

    Solve first: Number of Provinces

    Key insight

    Indexes linked by swaps, directly or through others, form a group whose characters can be arranged in any order. Sort each group's characters and place them into its sorted indexes.

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. Evaluate DivisionMediumLeetCode 399

    A graph with multiplicative weights, solvable two ways.

    Weighted graphUnion-find

    Solve first: Clone Graph, Redundant Connection

    Key insight

    Each equation a / b = k is a weighted edge in both directions (k and 1/k). Answer a query by searching for a path and multiplying weights, or use a weighted union-find that stores each node's ratio to its root.

Pattern 15

Shortest paths and minimum spanning trees

When edges have different costs, BFS no longer gives shortest paths. Dijkstra's algorithm does, for non-negative weights; Bellman-Ford handles limits on the number of edges; minimum spanning trees connect everything as cheaply as possible.

Recognise it when

  • Weighted edges and a "cheapest", "fastest", or "minimum cost" path
  • A path's cost is its worst step rather than its total
  • A limit on the number of edges, stops, or hops
  • "Connect all points at minimum total cost"

Variations

  • Dijkstra: shortest paths from one source, non-negative weights
  • Minimax Dijkstra: the cost is the maximum edge on the path
  • Bellman-Ford: shortest paths using at most k edges
  • 0-1 BFS: weights of only 0 and 1, with a deque
  • Floyd–Warshall: all pairs on small graphs
  • Prim and Kruskal: minimum spanning trees

Core intuition: Dijkstra always expands the closest unfinished node; with non-negative weights, nothing found later can make it closer. A heap entry older than the node's best known distance is stale and gets skipped.

Template

import heapq
from collections import defaultdict

def dijkstra(n, edges, src):              # edges: (u, v, w) with w >= 0
    graph = defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))
    dist = [float("inf")] * n
    dist[src] = 0
    heap = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                       # stale entry
        for v, w in graph[u]:
            if d + w < dist[v]:
                dist[v] = d + w
                heapq.heappush(heap, (dist[v], v))
    return dist

Network Delay Time is this function plus one line. For a minimax cost, replace d + w with max(d, w).

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Network Delay TimeMediumLeetCode 743

    Dijkstra in its standard form.

    Dijkstra
    Key insight

    Run Dijkstra from k. The answer is the largest shortest distance, or −1 if any node is unreachable.

  2. Min Cost to Connect All PointsMediumLeetCode 1584

    The canonical minimum spanning tree problem.

    Minimum spanning tree

    Solve first: Redundant Connection

    Key insight

    Every pair of points is an edge, so the graph is dense. Prim's with a simple O(n²) scan suits dense graphs; Kruskal's sorts all edges and unions with a DSU. Know one well.

Variations 4

The same idea with a twist that changes the details.

  1. Path With Minimum EffortMediumLeetCode 1631

    Dijkstra where the cost is the worst step, not the total.

    DijkstraMinimax

    Solve first: Network Delay Time

    Key insight

    Relax with max(current effort, step) instead of adding. It still works because a path's cost never decreases as it grows.

  2. Cheapest Flights Within K StopsMediumLeetCode 787

    A limit on edges breaks plain Dijkstra.

    Bellman-Ford

    Solve first: Network Delay Time

    Key insight

    A cheaper path with too many stops can block a pricier valid one. Run Bellman-Ford for k + 1 rounds, relaxing from a copy of the previous round's distances so each round adds at most one edge.

  3. Find the City With the Smallest Number of Neighbors at a Threshold DistanceMediumLeetCode 1334

    All-pairs shortest paths on a small graph.

    Floyd–Warshall

    Solve first: Network Delay Time

    Key insight

    With n ≤ 100, Floyd–Warshall is simplest: three nested loops with the intermediate node outermost. Then count cities within the threshold, breaking ties by the larger index.

  4. Minimum Obstacle Removal to Reach CornerHardLeetCode 2290

    0-1 BFS: Dijkstra's result without a heap.

    0-1 BFS

    Solve first: Rotting Oranges, Network Delay Time

    Key insight

    Moving into an empty cell costs 0 and into an obstacle costs 1. Use a deque: push 0-cost moves to the front and 1-cost moves to the back. O(rows × cols).

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Swim in Rising WaterHardLeetCode 778

    One problem, three valid techniques.

    Minimax DijkstraUnion-findBinary search

    Solve first: Path With Minimum Effort

    Key insight

    A path costs its highest cell, so minimax Dijkstra solves it. So does binary searching the time with a BFS check, or adding cells in height order with union-find until the corners connect.

  2. Number of Ways to Arrive at DestinationMediumLeetCode 1976

    Dijkstra that counts shortest paths as well as measuring them.

    DijkstraCounting

    Solve first: Network Delay Time

    Key insight

    Keep a count per node. A strictly shorter path replaces the count; an equally short one adds to it. Take counts modulo 10⁹ + 7.

Stage 4

Greedy and dynamic programming

Optimal choices. Greedy when one local choice is always safe; DP when you must try every choice and reuse results. DP is split by the shape of its state, from one index to bitmasks and trees.

Pattern 16

Greedy

Make the locally best choice at each step and never revisit it. When it works, it's usually a sort plus one pass; the hard part is knowing it's safe.

Recognise it when

  • Sorting by one key seems to make the choice obvious
  • You pick the earliest, cheapest, largest, or farthest option each time
  • A DP would work, but each state only ever uses one best option
  • Reachability or coverage problems: how far can you get?

Variations

  • Sort, then match or assign in order
  • Track the farthest reach or a running balance
  • Sort by a derived key, such as a difference or concatenation
  • Greedy with a heap: take the best option seen so far, even retroactively
  • Two passes, one from each side

Core intuition: Before trusting a greedy rule, try to break it with a small counterexample. If you can't, sketch an exchange argument: take any optimal answer and show you can swap in the greedy choice without making it worse.

Template

# Most greedy solutions: sort by the right key, then take what's safe
items.sort(key=sort_key)            # choosing the key IS the insight
chosen = []
for item in items:
    if still_compatible(item, chosen):
        chosen.append(item)          # a choice that can't hurt later ones

Pseudocode. Non-overlapping Intervals in Stage 1 was a greedy problem in this exact shape.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Assign CookiesEasyLeetCode 455

    The simplest sort-and-match greedy, with an easy exchange argument.

    GreedySorting
    Key insight

    Sort both lists and give each child the smallest cookie that satisfies them. Using a bigger cookie than needed can only hurt a later child.

  2. Jump GameMediumLeetCode 55

    Tracking the farthest reach instead of exploring every path.

    GreedyReach
    Key insight

    Keep the farthest index reachable so far. If you ever stand beyond it, you're stuck; if it reaches the last index, you're done. O(n), where DP would be O(n²).

  3. Best Time to Buy and Sell Stock IIMediumLeetCode 122

    Greedy that works because the choices don't interact.

    Greedy

    Solve first: Best Time to Buy and Sell Stock

    Key insight

    With unlimited transactions, the answer is the sum of every positive day-to-day increase. Each rise can be captured by buying before it and selling after.

Variations 7

The same idea with a twist that changes the details.

  1. Jump Game IIMediumLeetCode 45

    The minimum jumps, found with BFS-style levels in O(1) space.

    GreedyBFS levels

    Solve first: Jump Game

    Key insight

    Indexes reachable with j jumps form a range. Scan the current range while tracking the farthest next reach; each time you finish a range, count a jump.

  2. Gas StationMediumLeetCode 134

    A running balance that tells you where not to start.

    GreedyRunning balance
    Key insight

    A solution exists if total gas ≥ total cost. Scan with a running tank; when it drops below zero, no start from the current candidate up to here can work, so the next start is the station after.

  3. Partition LabelsMediumLeetCode 763

    Greedy extension of a boundary, using precomputed last positions.

    GreedyHashing
    Key insight

    Record each letter's last index. Extend the current part's end to the last index of every letter you pass; when you reach that end, close the part.

  4. Hand of StraightsMediumLeetCode 846

    The smallest remaining item forces your hand.

    GreedyCounting
    Key insight

    The smallest remaining card can only start a group, never sit inside one. Repeatedly start a group there and consume the next group-size values from the counts.

  5. Valid Parenthesis StringMediumLeetCode 678

    Tracking a range of possibilities instead of branching.

    GreedyRange tracking

    Solve first: Valid Parentheses

    Key insight

    Keep the range [low, high] of possible open counts. ( raises both, ) lowers both, * lowers low and raises high. Fail if high goes negative, keep low at least 0, and succeed if low is 0 at the end.

  6. Two City SchedulingMediumLeetCode 1029

    Sorting by a derived key, justified by an exchange argument.

    GreedySorting
    Key insight

    Imagine sending everyone to A, then moving n people to B. Move the ones with the smallest costB − costA. Sorting by that difference is the whole solution.

  7. Largest NumberMediumLeetCode 179

    A custom comparator where no simple key works.

    GreedyCustom sort
    Key insight

    Put a before b when a + b > b + a as strings; use functools.cmp_to_key. If the result starts with 0, the answer is "0".

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. CandyHardLeetCode 135

    Two one-directional passes combined.

    GreedyTwo passes

    Solve first: Product of Array Except Self

    Key insight

    Left to right: give one more than the left neighbour when rated higher. Right to left: do the same against the right neighbour, keeping the larger of the two values. Sum the result.

  2. Minimum Number of Refueling StopsHardLeetCode 871

    Greedy with a heap: decide retroactively which past option to use.

    GreedyHeap

    Solve first: Last Stone Weight

    Key insight

    Drive as far as fuel allows, pushing every station you pass into a max-heap. When you can't reach the next point, refuel from the biggest station passed so far. Each refuel is one stop.

Pattern 17

Dynamic programming: one dimension

Define an answer for each position (or amount), express it through answers you've already got, and solve each one once. The state is a single index.

Recognise it when

  • Count ways, or find the minimum or maximum, over a sequence
  • Each step is a small choice, such as take or skip, and choices affect what's allowed next
  • Recursion would repeat the same subproblems
  • The answer at i depends only on a few earlier (or later) positions

Variations

  • Take or skip: House Robber shape
  • Ways to reach: sum over the last step
  • Prefix feasibility: can the first i items be split validly?
  • Track two values, such as a max and a min
  • Solve from the back when choices jump forward

Core intuition: Write the state in words first, for example "f(i) = the most money from houses i onward". Then the recurrence is just the choices at i. Top-down with @cache is easiest to get right; convert to a loop with a few variables once it works.

Template

from functools import cache

def top_down(a):
    @cache
    def f(i):                           # best answer using a[i:]
        if i >= len(a):
            return 0                    # base case
        take = a[i] + f(i + 2)          # the choices at i ...
        skip = f(i + 1)
        return max(take, skip)          # ... combined
    return f(0)

def bottom_up(a):
    nxt1 = nxt2 = 0                     # f(i + 1) and f(i + 2)
    for i in range(len(a) - 1, -1, -1):
        nxt1, nxt2 = max(a[i] + nxt2, nxt1), nxt1
    return nxt1

The example is House Robber. Try Climbing Stairs and Min Cost Climbing Stairs first, then House Robber without looking back here.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Climbing StairsEasyLeetCode 70

    The first DP: counting ways by the last step.

    DPCounting
    Key insight

    Ways to reach step n = ways to reach n − 1 + ways to reach n − 2. It's Fibonacci, so two variables are enough.

  2. Min Cost Climbing StairsEasyLeetCode 746

    Minimisation instead of counting, with a careful definition of "the top".

    DP

    Solve first: Climbing Stairs

    Key insight

    The cost to reach step i is the cheaper of arriving from i − 1 or i − 2, plus that step's cost. The top is one position past the last index.

  3. House RobberMediumLeetCode 198

    The take-or-skip recurrence that half of 1-D DP reuses.

    DPTake or skip

    Solve first: Climbing Stairs

    Key insight

    f(i) = max(nums[i] + f(i + 2), f(i + 1)): rob this house and skip the next, or skip this one. Two rolling variables give O(1) space.

Variations 7

The same idea with a twist that changes the details.

  1. House Robber IIMediumLeetCode 213

    Reducing a circular problem to two linear ones.

    DP

    Solve first: House Robber

    Key insight

    The first and last houses are neighbours, so they can't both be robbed. Run House Robber without the first house and without the last, and take the better. With one house, return it.

  2. Delete and EarnMediumLeetCode 740

    Recognising a known DP after transforming the input.

    DPReduction

    Solve first: House Robber

    Key insight

    Taking value x earns x × count(x) and removes x − 1 and x + 1. Build points[x] and run House Robber over the values.

  3. Decode WaysMediumLeetCode 91

    Counting with validity checks, where edge cases are the lesson.

    DPCounting

    Solve first: Climbing Stairs

    Key insight

    f(i) gets f(i + 1) if s[i] isn't '0', plus f(i + 2) if s[i:i+2] is between 10 and 26. A '0' can never start a code.

  4. Word BreakMediumLeetCode 139

    Prefix feasibility: can the string from i onward be split?

    DPHashing

    Solve first: Decode Ways

    Key insight

    can(i) is true if some dictionary word matches at i and can(i + len(word)) is true. Memoise on i, and use a set of words.

  5. Maximum Product SubarrayMediumLeetCode 152

    Kadane's idea with two tracked values, because of negative numbers.

    DPKadane

    Solve first: Maximum Subarray

    Key insight

    A negative number turns the largest product into the smallest. Track both the maximum and minimum product ending here; the new maximum is the largest of x, x × old max, and x × old min.

  6. Minimum Cost For TicketsMediumLeetCode 983

    Choices that cover several future positions at once.

    DP
    Key insight

    cost(i) for the i-th travel day is the cheapest pass price plus the cost from the first travel day that pass doesn't cover. Iterate travel days rather than all 365.

  7. Solving Questions With BrainpowerMediumLeetCode 2140

    Why some DPs are easier solved from the back.

    DPBackward

    Solve first: House Robber

    Key insight

    f(i) = max(points[i] + f(i + brainpower[i] + 1), f(i + 1)). Going backward means every value you need is already computed.

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Maximum Sum Circular SubarrayMediumLeetCode 918

    Two Kadane runs combined.

    DPKadane

    Solve first: Maximum Subarray, House Robber II

    Key insight

    The best subarray either doesn't wrap (normal Kadane) or wraps, which equals total − the minimum subarray. If every number is negative, the wrap case would be empty, so return the normal maximum.

  2. Word Break IIHardLeetCode 140

    DP returning lists instead of booleans: memoised backtracking.

    DPBacktracking

    Solve first: Word Break

    Key insight

    Memoise on the start index and return every sentence for the rest of the string. The output can be exponential, so this is the best possible.

Pattern 18

Dynamic programming: knapsack and subset sums

Choose items to hit or stay within a capacity: a sum, an amount, or a budget. The state is "capacity used", and the loop order decides whether items can repeat and whether order matters.

Recognise it when

  • Pick a subset that sums to a target, or split items into two groups
  • Make an amount from coins or numbers
  • Each item has a cost and you have a budget
  • Signs, partitions, or "closest to half" in disguise

Variations

  • 0/1: each item at most once, capacity loop runs downward
  • Unbounded: items reusable, capacity loop runs upward
  • Counting combinations: items in the outer loop
  • Counting ordered sequences: capacity in the outer loop
  • Two capacities at once

Core intuition: In a one-row table, the direction of the capacity loop is the whole difference between "once" and "reusable". Going downward reads values from before this item was considered; going upward lets the item be used again.

Template

# 0/1 knapsack (each item once): capacity loop goes DOWN
best = [0] * (cap + 1)                 # best[c] = best value within capacity c
for weight, value in items:
    for c in range(cap, weight - 1, -1):
        best[c] = max(best[c], best[c - weight] + value)

# Unbounded (items reusable): capacity loop goes UP
best = [0] * (cap + 1)
for weight, value in items:
    for c in range(weight, cap + 1):
        best[c] = max(best[c], best[c - weight] + value)

For counting, combinations put items in the outer loop; ordered sequences put the capacity in the outer loop. Coin Change II and Combination Sum IV show the difference.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Partition Equal Subset SumMediumLeetCode 416

    The canonical 0/1 knapsack, with booleans.

    0/1 knapsack
    Key insight

    Half the total must be reachable as a subset sum. Iterate the capacity downward so each number is used once. A Python integer used as a bitset (bits |= bits << x) is a neat shortcut.

  2. Coin ChangeMediumLeetCode 322

    The canonical unbounded knapsack, minimising a count.

    Unbounded knapsack
    Key insight

    dp[a] = min(dp[a − coin] + 1) over coins, with dp[0] = 0 and infinity for unreachable amounts. For a minimum, loop order doesn't change the answer.

  3. Coin Change IIMediumLeetCode 518

    Counting combinations, where loop order starts to matter.

    Unbounded knapsackCounting

    Solve first: Coin Change

    Key insight

    Put coins in the outer loop and amounts in the inner loop. Each combination is then built in one fixed coin order, so it's counted once.

Variations 4

The same idea with a twist that changes the details.

  1. Target SumMediumLeetCode 494

    A sign problem that reduces to subset counting.

    0/1 knapsackReduction

    Solve first: Partition Equal Subset Sum

    Key insight

    The + numbers P and − numbers N satisfy P − N = target and P + N = total, so P = (target + total) / 2. Count subsets summing to P. Check parity and bounds first.

  2. Combination Sum IVMediumLeetCode 377

    The mirror image of Coin Change II: here order matters.

    KnapsackCounting

    Solve first: Coin Change II

    Key insight

    1 + 2 and 2 + 1 count separately, so put the target in the outer loop and numbers in the inner loop, the reverse of Coin Change II.

  3. Perfect SquaresMediumLeetCode 279

    Recognising coins in disguise.

    Unbounded knapsack

    Solve first: Coin Change

    Key insight

    The squares are the coins: dp[n] = 1 + min(dp[n − k²]). A number theory result bounds the answer at 4, but the DP is what's being taught.

  4. Last Stone Weight IIMediumLeetCode 1049

    A simulation that is secretly a partition problem.

    0/1 knapsackReduction

    Solve first: Partition Equal Subset Sum

    Key insight

    Every smashing order ends up splitting stones into two piles and taking the difference. Find the subset sum closest to half the total.

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. Ones and ZeroesMediumLeetCode 474

    A knapsack with two budgets at once.

    0/1 knapsackTwo capacities

    Solve first: Partition Equal Subset Sum

    Key insight

    Each string costs some zeros and some ones. dp[z][o] is the most strings within those budgets; loop both capacities downward.

Pattern 19

Dynamic programming: grids and two strings

The state is two indexes: a cell in a grid, or a position in each of two strings. Fill a table where each cell depends on its neighbours above, to the left, and diagonally.

Recognise it when

  • Paths through a grid that move only right and down
  • Comparing, aligning, or transforming two strings
  • Palindromes within one string (the two indexes are its ends)
  • "How many ways can s produce t" or "does s match pattern p"

Variations

  • Grid paths: count or minimise
  • Two strings: match extends the diagonal, mismatch chooses a neighbour
  • Palindromes: expand around centres, or fill by length
  • Squares and shapes: combine three neighbours
  • Pattern matching with wildcards

Core intuition: Use an extra row and column for empty prefixes, so dp[i][j] is about a[:i] and b[:j]. Every cell then reads only the row above, which is also why one row of memory is enough.

Template

# dp[i][j] answers the question for prefixes a[:i] and b[:j]
m, n = len(a), len(b)
dp = [[0] * (n + 1) for _ in range(m + 1)]   # row 0 / column 0: empty prefix
for i in range(1, m + 1):
    for j in range(1, n + 1):
        if a[i - 1] == b[j - 1]:
            dp[i][j] = dp[i - 1][j - 1] + 1
        else:
            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
answer = dp[m][n]

This example is Longest Common Subsequence; most problems below change only the two lines inside the loop.

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Unique PathsMediumLeetCode 62

    The simplest grid DP.

    Grid DP
    Key insight

    paths(r, c) = paths(r − 1, c) + paths(r, c − 1), with the first row and column all 1. One row of storage is enough.

  2. Longest Common SubsequenceMediumLeetCode 1143

    The two-string table that the rest of this pattern reuses.

    Two-string DP
    Key insight

    If the characters match, extend the diagonal by one; otherwise take the better of dropping a character from either string.

Variations 6

The same idea with a twist that changes the details.

  1. Minimum Path SumMediumLeetCode 64

    The same table, minimising instead of counting.

    Grid DP

    Solve first: Unique Paths

    Key insight

    Each cell adds its value to the smaller of the cell above and the cell to the left. One row, or the grid itself if you may modify it, is enough.

  2. Maximal SquareMediumLeetCode 221

    A cell that combines three neighbours.

    Grid DP

    Solve first: Unique Paths

    Key insight

    For a '1' cell, the largest square ending there has side 1 + min(top, left, top-left). The answer is the largest side, squared.

  3. Edit DistanceMediumLeetCode 72

    The most important two-string DP after LCS.

    Two-string DP

    Solve first: Longest Common Subsequence

    Key insight

    If the characters match, copy the diagonal. Otherwise take 1 + the minimum of insert (left), delete (up), and replace (diagonal). Converting to or from an empty prefix costs its length.

  4. Longest Palindromic SubstringMediumLeetCode 5

    Two indexes inside one string, and a simpler alternative to the table.

    PalindromesExpand around centre
    Key insight

    Expand around each of the 2n − 1 centres (characters and gaps) while the ends match. O(n²) time with O(1) memory; the DP table is also O(n²) but uses O(n²) memory.

  5. Palindromic SubstringsMediumLeetCode 647

    Counting with the same centre expansion.

    PalindromesExpand around centre

    Solve first: Longest Palindromic Substring

    Key insight

    Expand from every centre and count each successful step. Every palindrome is found exactly once, from its own centre.

  6. Interleaving StringMediumLeetCode 97

    A boolean table where each cell has two possible sources.

    Two-string DP

    Solve first: Longest Common Subsequence

    Key insight

    dp[i][j] says whether s3[:i + j] can come from s1[:i] and s2[:j]. It's true if the last character matches s1 and dp[i − 1][j] holds, or matches s2 and dp[i][j − 1] holds. Check lengths first.

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Distinct SubsequencesHardLeetCode 115

    Counting over two strings.

    Two-string DPCounting

    Solve first: Longest Common Subsequence

    Key insight

    dp[i][j] counts ways to form t[:j] from s[:i]. Always inherit dp[i − 1][j] (skip this character of s); when the characters match, also add dp[i − 1][j − 1]. An empty t can be formed exactly once.

  2. Regular Expression MatchingHardLeetCode 10

    The classic hard string DP.

    Two-string DPPattern matching

    Solve first: Edit Distance

    Key insight

    dp[i][j] says whether s[:i] matches p[:j]. A * either uses zero of the previous element (dp[i][j − 2]) or one more of it, if that element matches s[i − 1] (dp[i − 1][j]). Top-down memoisation on (i, j) is often easier to get right.

Pattern 20

Dynamic programming: advanced shapes

The same idea, with less obvious states: the best subsequence ending at each item, a small set of states per day, answers over every range, a set of used items as a bitmask, or a value returned from each tree node.

Recognise it when

  • Longest increasing or chained subsequence
  • Buy and sell rules, cooldowns, or transaction limits
  • The cost of a range depends on how you split it, or two players take turns
  • n is at most about 20 and the state is "which items are used"
  • Choices on a tree, where each node's answer depends on its children

Variations

  • Longest increasing subsequence, O(n²) and O(n log n)
  • State machines: a few states updated each step
  • Interval DP: fill by range length
  • Bitmask DP over subsets
  • DP on trees: return a tuple of states from each node

Core intuition: When a DP feels impossible, the state is usually missing information. Ask what you'd need to know to continue from here (holding stock or not, which items are used, which balloon goes last) and add exactly that.

Template

from bisect import bisect_left

# Longest strictly increasing subsequence in O(n log n)
tails = []          # tails[k] = smallest tail of an increasing run of length k + 1
for x in nums:
    k = bisect_left(tails, x)
    if k == len(tails):
        tails.append(x)
    else:
        tails[k] = x
lis_length = len(tails)

# Interval DP: shortest ranges first, so every sub-range is ready
for length in range(2, n + 1):
    for i in range(n - length + 1):
        j = i + length - 1
        dp[i][j] = max(dp[i][k] + dp[k + 1][j] + cost(i, k, j) for k in range(i, j))

tails is not itself a valid subsequence; only its length is meaningful. The interval part is pseudocode for the shape.

Canonical 3

The pattern in its plainest form. Solve these first.

  1. Longest Increasing SubsequenceMediumLeetCode 300

    The best-known DP with a faster, non-obvious improvement.

    LISBinary search

    Solve first: Search Insert Position

    Key insight

    O(n²): lis[i] = 1 + max(lis[j]) over earlier smaller values. O(n log n): keep the smallest tail for each length and update it with bisect_left.

  2. Best Time to Buy and Sell Stock with CooldownMediumLeetCode 309

    The state machine DP in its clearest form.

    State machine

    Solve first: Best Time to Buy and Sell Stock II

    Key insight

    Keep three states per day: holding, just sold, and resting without stock. Update each from yesterday's states (you can only buy from resting); the answer is the best state without stock.

  3. House Robber IIIMediumLeetCode 337

    DP on a tree, returning a pair from each node.

    Tree DP

    Solve first: House Robber, Diameter of Binary Tree

    Key insight

    Each node returns (best if robbed, best if not). Robbed = value + both children's not-robbed; not robbed = each child's better option, summed. One post-order pass.

Variations 5

The same idea with a twist that changes the details.

  1. Russian Doll EnvelopesHardLeetCode 354

    Reducing a 2-D problem to LIS with one sorting trick.

    LISSorting

    Solve first: Longest Increasing Subsequence

    Key insight

    Sort by width ascending and height descending, then take the LIS of heights. The descending heights stop two envelopes of equal width from nesting.

  2. Longest String ChainMediumLeetCode 1048

    LIS where "previous" is defined by a rule, not an index.

    LIS-style DPHashing

    Solve first: Longest Increasing Subsequence

    Key insight

    Sort words by length. A word's chain is 1 + the best chain of any word formed by deleting one of its letters; a dictionary keyed by word makes each lookup O(1).

  3. Best Time to Buy and Sell Stock IVHardLeetCode 188

    A state machine with a transaction counter.

    State machine

    Solve first: Best Time to Buy and Sell Stock with Cooldown

    Key insight

    Keep buy[j] and sell[j] for j = 1 to k, where buy[j] builds on sell[j − 1]. If k ≥ n / 2, the limit never binds and it's the unlimited case.

  4. Predict the WinnerMediumLeetCode 486

    Interval DP for a two-player game.

    Interval DPGame
    Key insight

    Let diff(i, j) be the best score margin the player to move can force on nums[i..j]: max(nums[i] − diff(i + 1, j), nums[j] − diff(i, j − 1)). Player 1 wins if diff(0, n − 1) ≥ 0.

  5. Burst BalloonsHardLeetCode 312

    The classic interval DP, where the trick is choosing which balloon goes last.

    Interval DP

    Solve first: Predict the Winner

    Key insight

    Pad with 1s at both ends. If k is the last balloon burst strictly between i and j, its neighbours are then i and j: dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] × nums[k] × nums[j]).

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Binary Tree CamerasHardLeetCode 968

    Tree DP with three states, which also has a greedy reading.

    Tree DPGreedy

    Solve first: House Robber III

    Key insight

    Each node reports whether it needs covering, has a camera, or is covered without one. Working up from the leaves, a parent of an uncovered child must hold a camera; handle an uncovered root at the end. Store the root's state first: count + (dfs(root) == NEEDS_COVER) reads count before the DFS has updated it.

  2. Minimum Number of Work Sessions to Finish the TasksMediumLeetCode 1986

    Bitmask DP: the state is which tasks are done.

    Bitmask DP

    Solve first: Partition to K Equal Sum Subsets

    Key insight

    With at most 14 tasks, store for each mask of finished tasks the best (sessions used, time used in the current session), compared as a pair. Adding task t either fits in the current session or opens a new one.

Stage 5

Specialised tools

Smaller patterns that come up less often but are hard to invent under pressure. The last pattern is optional: skip it if time is short.

Pattern 21

Tries

A tree of characters where each path from the root spells a prefix. It answers "does any word start with this?" in time proportional to the prefix, however many words are stored.

Recognise it when

  • Many words, and questions about prefixes
  • Autocomplete, spell-check, or replacing words by their roots
  • Searching a grid or pattern for many words at once
  • Maximising XOR, where numbers are treated as strings of bits

Variations

  • Plain trie: insert, search, and prefix search
  • Wildcard search that branches at a dot
  • Extra data stored at each node, such as top suggestions
  • Trie plus backtracking on a grid
  • Binary trie over the bits of numbers

Core intuition: Words that share a prefix share a path, so one walk down the trie tests every word with that prefix at once. That's why tries shine when you'd otherwise check many words at every step.

Template

class TrieNode:
    __slots__ = ("children", "end")
    def __init__(self):
        self.children = {}
        self.end = False               # a word ends at this node

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.end = True

    def find(self, prefix):            # the node for this prefix, or None
        node = self.root
        for ch in prefix:
            node = node.children.get(ch)
            if node is None:
                return None
        return node

Implement Trie is this class plus two one-line methods; write it once yourself before using the template.

Canonical 1

The pattern in its plainest form. Solve these first.

  1. Implement Trie (Prefix Tree)MediumLeetCode 208

    The data structure itself.

    TrieDesign
    Key insight

    Each node maps characters to children and marks whether a word ends there. search needs the end flag; startsWith only needs the path to exist.

Variations 3

The same idea with a twist that changes the details.

  1. Design Add and Search Words Data StructureMediumLeetCode 211

    A trie search that branches.

    TrieDFS

    Solve first: Implement Trie (Prefix Tree)

    Key insight

    A . tries every child: DFS from that node over the rest of the pattern. Without dots it's a plain lookup.

  2. Replace WordsMediumLeetCode 648

    Stopping at the shortest matching prefix.

    Trie

    Solve first: Implement Trie (Prefix Tree)

    Key insight

    Insert every root. For each word, walk the trie and stop at the first node that ends a root; replace the word with that prefix, or keep it if none is found.

  3. Search Suggestions SystemMediumLeetCode 1268

    Storing answers at nodes to make queries instant.

    TrieSorting

    Solve first: Implement Trie (Prefix Tree)

    Key insight

    Walk the trie as each character is typed. Inserting words in sorted order and keeping up to three at every node makes each query O(1) after the walk. Sorting plus binary search is an equally good approach.

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Word Search IIHardLeetCode 212

    A trie that turns many searches into one.

    TrieBacktracking

    Solve first: Word Search, Implement Trie (Prefix Tree)

    Key insight

    Put every word in one trie and DFS the board once, following trie edges. Remove words once found and prune empty nodes, or the search repeats work and times out.

  2. Maximum XOR of Two Numbers in an ArrayMediumLeetCode 421

    A trie over bits instead of letters.

    Binary trieBit manipulationGreedy

    Solve first: Implement Trie (Prefix Tree), Single Number

    Key insight

    Insert each number bit by bit, from the highest bit down. For each number, walk toward the opposite bit at every level when possible; that maximises the XOR from the top bit down.

Pattern 22

Bit manipulation

Treat integers as rows of bits. A handful of identities, such as XOR cancelling pairs and x & (x − 1) clearing the lowest bit, solve problems that look like they need extra memory.

Recognise it when

  • "Every element appears twice except one"
  • O(1) extra space with values in a known range
  • Counting set bits, powers of two, or reversing bits
  • Addition or arithmetic without the usual operators
  • Sets of up to about 20 items represented as a mask

Variations

  • XOR to cancel pairs
  • Clearing or isolating the lowest set bit
  • Counting each bit position across all numbers
  • Masks for 32-bit behaviour in Python

Core intuition: XOR is addition without carrying: it's its own inverse, so anything XORed twice disappears. Python integers are unbounded, so problems about 32-bit numbers need an explicit mask.

Template

x & 1              # lowest bit: is x odd?
x >> 1             # halve, rounding down
x & (x - 1)        # clear the lowest set bit
x & -x             # keep only the lowest set bit
x ^ x == 0         # XOR cancels pairs, and x ^ 0 == x
(mask >> i) & 1    # is bit i set?
mask | (1 << i)    # set bit i
x.bit_count()      # number of set bits (Python 3.10+)
MASK = 0xFFFFFFFF  # emulate 32 bits; convert back if the top bit is set

A reference, not a program. Bitmask DP in the advanced DP pattern used the set-a-bit and test-a-bit lines.

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Single NumberEasyLeetCode 136

    The XOR trick in its plainest form.

    XOR
    Key insight

    a ^ a = 0 and a ^ 0 = a, so XORing every number cancels the pairs and leaves the single one. O(1) space.

  2. Number of 1 BitsEasyLeetCode 191

    Clearing the lowest set bit.

    Bit counting
    Key insight

    n & (n − 1) removes the lowest set bit; count how many times until n is 0. n.bit_count() does it in one call.

Variations 4

The same idea with a twist that changes the details.

  1. Counting BitsEasyLeetCode 338

    Bits combined with a tiny DP.

    BitsDP

    Solve first: Number of 1 Bits

    Key insight

    bits[i] = bits[i >> 1] + (i & 1): drop the last bit, which you've already counted, then add it back. O(n) overall.

  2. Missing NumberEasyLeetCode 268

    XOR cancelling across two sequences.

    XORMath

    Solve first: Single Number

    Key insight

    XOR every index 0 to n with every value; present numbers cancel, leaving the missing one. The sum formula n(n + 1) / 2 − total works too.

  3. Reverse BitsEasyLeetCode 190

    Moving bits one at a time, with a fixed width.

    Bit shifting
    Key insight

    Take the lowest bit of n 32 times, shifting each onto the result. Python integers are unbounded, so loop exactly 32 times.

  4. Single Number IIMediumLeetCode 137

    Counting per bit position when XOR alone can't help.

    Bit counting

    Solve first: Single Number

    Key insight

    For each of the 32 bits, count how many numbers have it set; counts that aren't a multiple of 3 belong to the single number. Convert from 32-bit two's complement if the result is negative.

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. Sum of Two IntegersMediumLeetCode 371

    Addition from bit operations, and Python's unbounded integers.

    XORCarryMasking

    Solve first: Single Number

    Key insight

    a ^ b adds without carrying and (a & b) << 1 is the carry; repeat until the carry is 0. In Python, mask with 0xFFFFFFFF each round or negative inputs loop forever, then convert back to a signed value.

Pattern 23

Math, matrices, and simulation

Problems solved by careful index work or a known mathematical fact rather than a data structure. The difficulty is usually in the boundaries.

Recognise it when

  • Rotating, spiralling, or transforming a matrix in place
  • Digits, primes, or big-number arithmetic
  • A process that repeats until it stabilises or loops
  • O(1) extra space on a matrix, which means reusing the matrix itself

Variations

  • Layer and boundary walking
  • Reusing part of the input as marker storage
  • Cycle detection on a numeric process
  • Sieves and number theory
  • Grade-school arithmetic on strings

Core intuition: Write the boundaries down before coding: which rows and columns are still unprocessed, and when a loop should stop. Most wrong answers here are off-by-one errors, not wrong ideas.

Template

from math import gcd, isqrt

gcd(12, 18)                          # 6
def is_prime(n):
    return n > 1 and all(n % d for d in range(2, isqrt(n) + 1))

# Walking a grid with a direction vector; turning right maps (dr, dc) to (dc, -dr)
dr, dc = 0, 1                        # moving right
dr, dc = dc, -dr                     # now moving down
dr, dc = dc, -dr                     # now moving left

A reference. The direction-vector turn is a tidy alternative to four separate loops in Spiral Matrix.

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Rotate ImageMediumLeetCode 48

    In-place matrix transformation by composing simple steps.

    MatrixIn place
    Key insight

    Rotating 90° clockwise equals transposing (swapping across the main diagonal) then reversing each row. Both steps are in place.

  2. Spiral MatrixMediumLeetCode 54

    Boundary bookkeeping, the core skill of this pattern.

    MatrixSimulation
    Key insight

    Keep top, bottom, left and right boundaries. Walk a side, then shrink its boundary; before walking back along the opposite side, check the boundaries haven't crossed.

Variations 3

The same idea with a twist that changes the details.

  1. Set Matrix ZeroesMediumLeetCode 73

    Reusing part of the input as marker storage.

    MatrixIn place

    Solve first: Rotate Image

    Key insight

    Use the first row and column to record which rows and columns to clear, plus one flag for whether the first column itself had a zero. Clear from the bottom-right so markers aren't overwritten early.

  2. Happy NumberEasyLeetCode 202

    Floyd's cycle detection on numbers instead of nodes.

    Cycle detectionMath

    Solve first: Linked List Cycle

    Key insight

    Replacing a number with the sum of its digit squares either reaches 1 or loops forever. Detect the loop with a set, or with fast and slow pointers.

  3. Count PrimesMediumLeetCode 204

    The Sieve of Eratosthenes.

    Sieve
    Key insight

    For each prime p, mark multiples from p² onward as composite. O(n log log n). Count primes strictly less than n.

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. Multiply StringsMediumLeetCode 43

    Grade-school multiplication with careful positions.

    MathSimulation
    Key insight

    Digits i and j (counted from the left) contribute to position i + j + 1 of the result. Accumulate every product into an array, then carry, then strip leading zeros.

Pattern 24

Data structure design

Build a class with required time bounds by combining structures you already know. Almost every answer is a hash map plus one ordering structure: a list, a linked list, a heap, or a sorted history.

Recognise it when

  • "Design a class" with operations and O(1) or O(log n) requirements
  • Caches with eviction rules
  • Random access combined with fast insert and delete
  • Versioned or time-stamped data

Variations

  • Hash map plus doubly linked list (LRU)
  • Hash map plus array with swap-to-delete
  • Per-key history with binary search
  • Heaps to merge several streams
  • Frequency buckets of ordered keys (LFU)

Core intuition: List every operation with its required complexity first. Each requirement usually names a structure: O(1) lookup means a hash map, recency order means a linked list, "most recent before t" means a sorted list and binary search.

Template

from collections import OrderedDict

# A hash map that remembers order: the backbone of LRU-style designs
cache = OrderedDict()
cache["a"] = 1
cache["b"] = 2
cache.move_to_end("a")               # "a" is now the most recently used
oldest_key, _ = cache.popitem(last=False)   # evicts "b"

Interviewers often ask for the doubly linked list behind this. Build LRU Cache both ways.

Canonical 1

The pattern in its plainest form. Solve these first.

  1. LRU CacheMediumLeetCode 146

    The most asked design problem.

    DesignHash mapLinked list

    Solve first: Reverse Linked List

    Key insight

    A hash map for O(1) lookup and a doubly linked list for recency: move a node to the front on each access and evict from the back. OrderedDict gives the same behaviour in a few lines.

Variations 3

The same idea with a twist that changes the details.

  1. Insert Delete GetRandom O(1)MediumLeetCode 380

    Swap-to-delete, a trick worth knowing.

    DesignHash map
    Key insight

    Keep values in a list and each value's index in a map. To remove, move the last element into the removed slot, update its index, and pop the end. random.choice is O(1).

  2. Snapshot ArrayMediumLeetCode 1146

    Versioned data stored sparsely.

    DesignBinary search

    Solve first: Time Based Key-Value Store

    Key insight

    Keep each index's history as (snapshot id, value) pairs, appending only when set. get is a binary search for the last entry at or before the requested snapshot.

  3. Design TwitterMediumLeetCode 355

    A design that combines hash maps and a k-way merge.

    DesignHeap

    Solve first: Merge k Sorted Lists

    Key insight

    Store tweets per user with a global timestamp and followees in sets. A feed merges the most recent tweets of the user and followees with a heap, stopping at 10.

Combinations and stretch 1

The pattern mixed with another, or pushed harder.

  1. LFU CacheHardLeetCode 460

    The hardest standard cache, with two levels of ordering.

    DesignHash map

    Solve first: LRU Cache

    Key insight

    Map key → (value, frequency) and frequency → an OrderedDict of keys. Track the minimum frequency: evict the oldest key at that frequency, and a newly inserted key resets the minimum to 1.

Pattern 25 (optional)

Range queries and string algorithms

Tools that rarely appear in standard interviews but make certain problems tractable: trees that answer range sums under updates, merge sort that counts while it sorts, and linear-time string matching.

Recognise it when

  • Range sums or counts while values keep changing
  • "How many elements to the right are smaller?"
  • Substring search, repeated prefixes, or borders of a string
  • Duplicate substrings, where hashing windows helps

Variations

  • Fenwick tree: point update, prefix sum
  • Coordinate compression to index by rank
  • Merge sort that counts pairs while merging
  • Prefix function (KMP) for prefix-suffix matching
  • Rolling hash with binary search on length

Core intuition: Treat this section as long-term strength rather than must-know interview material. If time is short, skip it; the rest of the sheet covers what interviews usually ask.

Template

class Fenwick:                         # prefix sums with updates, both O(log n)
    def __init__(self, n):
        self.tree = [0] * (n + 1)      # 1-indexed
    def add(self, i, delta):           # a[i] += delta, i is 1-based
        while i < len(self.tree):
            self.tree[i] += delta
            i += i & -i
    def prefix(self, i):               # sum of a[1..i]
        total = 0
        while i > 0:
            total += self.tree[i]
            i -= i & -i
        return total

def prefix_function(s):
    """pi[i] = length of the longest proper prefix of s[:i + 1] that is also its suffix."""
    pi = [0] * len(s)
    k = 0
    for i in range(1, len(s)):
        while k and s[i] != s[k]:
            k = pi[k - 1]
        if s[i] == s[k]:
            k += 1
        pi[i] = k
    return pi

i & -i, from the bit manipulation pattern, is what makes the Fenwick tree work.

Canonical 2

The pattern in its plainest form. Solve these first.

  1. Range Sum Query: MutableMediumLeetCode 307

    The problem these trees exist for.

    Fenwick treeSegment tree

    Solve first: Range Sum Query: Immutable

    Key insight

    A Fenwick tree gives point updates and prefix sums in O(log n) in a few lines. Keep the current values separately so an update becomes a delta. A segment tree does the same and extends to range minimum or maximum.

  2. Find the Index of the First Occurrence in a StringEasyLeetCode 28

    The standard place to learn KMP.

    KMPString matching
    Key insight

    str.find is the practical answer. For KMP, compute the prefix function of pattern + "#" + text; a value equal to len(pattern) marks where a match ends. O(n + m).

Variations 4

The same idea with a twist that changes the details.

  1. Count of Smaller Numbers After SelfHardLeetCode 315

    A Fenwick tree indexed by value rank.

    Fenwick treeCoordinate compression

    Solve first: Range Sum Query: Mutable

    Key insight

    Process from the right. Map values to ranks, then for each value count already-seen smaller ranks with a prefix query and add it. A merge sort that counts elements jumping past each one also works.

  2. Reverse PairsHardLeetCode 493

    Counting pairs while sorting.

    Merge sortDivide and conquer

    Solve first: Sorting (Prerequisites), Merge Sorted Array

    Key insight

    In merge sort, before merging two sorted halves, count pairs with nums[i] > 2 × nums[j] using two pointers. The sorted halves make the count linear per merge, so O(n log n) overall.

  3. Repeated DNA SequencesMediumLeetCode 187

    Fixed-length window hashing.

    HashingRolling hash

    Solve first: Maximum Average Subarray I

    Key insight

    Slide a length-10 window and keep seen substrings in a set; record one the second time it appears. A rolling hash, or 2 bits per letter, makes each step O(1).

  4. Longest Happy PrefixHardLeetCode 1392

    A direct test of the prefix function.

    KMP

    Solve first: Find the Index of the First Occurrence in a String

    Key insight

    The longest prefix that is also a suffix is exactly the last value of the prefix function.

Combinations and stretch 2

The pattern mixed with another, or pushed harder.

  1. Shortest PalindromeHardLeetCode 214

    Using the prefix function to find the longest palindromic prefix.

    KMPPalindromes

    Solve first: Longest Happy Prefix

    Key insight

    Only the part after the longest palindromic prefix needs mirroring in front. Its length is the last prefix-function value of s + "#" + reversed(s).

  2. Longest Duplicate SubstringHardLeetCode 1044

    Binary search on a length, checked with hashing.

    Binary search on the answerRolling hash

    Solve first: Koko Eating Bananas, Repeated DNA Sequences

    Key insight

    If a duplicate of length L exists, one of length L − 1 does too, so binary search the length. Check a length by rolling-hashing every window; on a hash match, compare the actual strings to rule out collisions.

Stage 6

Mixed practice

Twenty problems not seen earlier, with no pattern labels. This is the skill interviews actually test: recognising the pattern yourself.

For each problem: read it, write down the pattern you think applies and why, then solve it. Open the hint only if you're stuck for more than 15 minutes, and the reveal only after you've committed to an answer.

Recognition cheat sheet

Try the problems first; use this when you can't decide between patterns.

Show the cheat sheet
If the problem mentions…Reach for
Pairs, duplicates, or grouping in unsorted dataHashing and counting
Sorted input, pairs with a target, or in-place editsTwo pointers
Longest or shortest contiguous run, with a condition that only breaks as it growsSliding window
Subarray sums with negative numbers, or "exactly k"Prefix sums with a hash map
"Minimum value that works", or O(log n) on sorted dataBinary search (on the answer)
Next or previous greater and smaller, spans, histogramsMonotonic stack
Ranges, meetings, overlapsIntervals and sweep line
"Generate all" with n up to about 20Backtracking
Top k, a running minimum or maximum, merging sorted streamsHeap
Grid regions, reachability, fewest equal-cost movesDFS or BFS
Dependencies or prerequisitesTopological sort
Groups that merge as connections arriveUnion-find
Cheapest or best path with weighted edgesDijkstra and its variants
Count the ways, or the best value, with repeated subproblemsDynamic programming
One local choice is provably safe after sortingGreedy
Many words and prefix questionsTrie
"Every element appears twice except one", O(1) spaceBit manipulation

The problems

Roughly easiest first. The order doesn't hint at the pattern.

  1. Find the Town JudgeEasyLeetCode 997

    Hint

    Think about what each trust relationship adds to or takes away from a person.

    Reveal the pattern

    Counting (graph degrees). Keep one score per person: +1 when trusted, −1 when trusting. The judge is the person whose score is n − 1.

    Same idea as: In-degree counting from Course Schedule

  2. Longest PalindromeEasyLeetCode 409

    Hint

    Which letters must come in pairs, and how many can sit in the middle?

    Reveal the pattern

    Counting. Use every pair: add count // 2 × 2 for each letter, then add 1 if any count is odd, since one letter can sit in the middle.

    Same idea as: Valid Anagram

  3. Rotate ArrayMediumLeetCode 189

    Hint

    Can you do it in place with O(1) extra space, using an operation you already know well?

    Reveal the pattern

    Two pointers (reversal). Reduce k modulo n. Reverse the whole array, then reverse the first k elements and the remaining n − k separately.

    Same idea as: The converging pointers in Valid Palindrome

  4. Minimum Operations to Reduce X to ZeroMediumLeetCode 1658

    Hint

    Removing elements from both ends leaves what in the middle?

    Reveal the pattern

    Sliding window (on the complement). Find the longest subarray summing to total − x; every value is positive, so a variable window works. The answer is n minus that length, or −1 if none exists.

    Same idea as: Maximum Points You Can Obtain from Cards

  5. Count Number of Nice SubarraysMediumLeetCode 1248

    Hint

    Replace each number with whether it's odd. What's the question now?

    Reveal the pattern

    Prefix sums with a hash map. Count subarrays with exactly k odd numbers: keep counts of prefix odd-totals and look up running − k. The "at most k minus at most k − 1" window trick works too.

    Same idea as: Subarray Sum Equals K; Subarrays with K Different Integers

  6. Search a 2D Matrix IIMediumLeetCode 240

    Hint

    Rows and columns are both sorted. Which corner lets every comparison rule something out?

    Reveal the pattern

    Staircase search. Start at the top-right corner: if the value is too big, move left; if too small, move down. O(m + n).

    Same idea as: The counting walk in Kth Smallest Element in a Sorted Matrix

  7. Minimum Number of Days to Make m BouquetsMediumLeetCode 1482

    Hint

    If the bouquets can be made by day d, can they be made by day d + 1?

    Reveal the pattern

    Binary search on the answer. Check a day by counting runs of k adjacent flowers already bloomed. Return −1 immediately if m × k exceeds the number of flowers.

    Same idea as: Koko Eating Bananas

  8. Minimum Number of Arrows to Burst BalloonsMediumLeetCode 452

    Hint

    Where should the first arrow go to burst as many balloons as possible?

    Reveal the pattern

    Greedy on intervals. Sort by end. Shoot at the first end and skip every balloon that starts at or before that point; the next balloon needs a new arrow.

    Same idea as: Non-overlapping Intervals

  9. Remove Duplicate LettersMediumLeetCode 316

    Hint

    When is it safe to remove a letter you've already placed?

    Reveal the pattern

    Monotonic stack with greedy removal. Pop a larger stack top when it appears again later; track which letters are already in the stack so each is placed once.

    Same idea as: Remove K Digits

  10. Sum of Subarray MinimumsMediumLeetCode 907

    Hint

    Instead of looping over subarrays, ask how many subarrays each element is the minimum of.

    Reveal the pattern

    Monotonic stack (contribution counting). Find each element's previous-smaller and next-smaller boundaries; it's the minimum of left × right subarrays. Use < on one side and ≤ on the other so equal values aren't double-counted.

    Same idea as: Largest Rectangle in Histogram

  11. Sum Root to Leaf NumbersMediumLeetCode 129

    Hint

    What does each node need to know about the path above it?

    Reveal the pattern

    Top-down tree DFS. Pass down the number formed so far (value × 10 + digit); at a leaf, add it to the total.

    Same idea as: Count Good Nodes in Binary Tree

  12. Letter Tile PossibilitiesMediumLeetCode 1079

    Hint

    Tiles repeat, and every non-empty sequence counts, not only full-length ones.

    Reveal the pattern

    Backtracking over counts. Recurse over letter counts: for each letter with a count left, use one, add 1 to the total, recurse, and restore. Working on counts avoids duplicate sequences without any sorting tricks.

    Same idea as: Permutations II

  13. Number of Closed IslandsMediumLeetCode 1254

    Hint

    Which land cells can never belong to the answer?

    Reveal the pattern

    Grid DFS (eliminate the border first). Flood-fill all land connected to the border first; every island found after that is closed. Here 0 is land and 1 is water.

    Same idea as: Surrounded Regions

  14. Most Stones Removed with Same Row or ColumnMediumLeetCode 947

    Hint

    When can you remove a stone, and what remains when you can't remove any more?

    Reveal the pattern

    Union-find (connected components). Stones sharing a row or column are connected, and each component shrinks to one stone. Answer: stones minus components. Union each stone's row with its (offset) column to avoid comparing all pairs.

    Same idea as: Number of Provinces

  15. Path with Maximum ProbabilityMediumLeetCode 1514

    Hint

    Probabilities multiply along a path and can only shrink.

    Reveal the pattern

    Dijkstra (maximising a product). Use a max-heap on probability and relax with multiplication. It works because multiplying by values between 0 and 1 never increases a path's probability.

    Same idea as: Path With Minimum Effort

  16. Furthest Building You Can ReachMediumLeetCode 1642

    Hint

    Which climbs deserve the ladders?

    Reveal the pattern

    Greedy with a heap. Put every climb in a min-heap as if using a ladder. When the heap holds more climbs than you have ladders, pay for the smallest with bricks; stop when bricks run out.

    Same idea as: Minimum Number of Refueling Stops

  17. Partition Array for Maximum SumMediumLeetCode 1043

    Hint

    Think about where the last group ends.

    Reveal the pattern

    1-D dynamic programming. dp[i] is the best for the first i elements: the maximum over group lengths L ≤ k of dp[i − L] + L × max(last L elements), updating the max as L grows.

    Same idea as: Word Break's "last piece" recurrence

  18. Longest Palindromic SubsequenceMediumLeetCode 516

    Hint

    A palindrome reads the same reversed.

    Reveal the pattern

    Two-string DP (or interval DP). It's the Longest Common Subsequence of s and its reverse. Or fill by range: matching ends add 2, otherwise drop one end.

    Same idea as: Longest Common Subsequence

  19. Sliding PuzzleHardLeetCode 773

    Hint

    The board is tiny. How many different boards can there be?

    Reveal the pattern

    BFS on an implicit graph of states. Encode each board as a string; neighbours swap 0 with an adjacent cell (precompute the neighbours of each of the six positions). There are at most 720 states.

    Same idea as: Open the Lock

  20. Trapping Rain Water IIHardLeetCode 407

    Hint

    Water is held in by the lowest wall around it, and the border is the first wall.

    Reveal the pattern

    Min-heap from the border inward. Push every border cell into a min-heap. Pop the lowest; each unvisited neighbour holds max(0, level − height) water and is pushed with max(level, height).

    Same idea as: Trapping Rain Water; Swim in Rising Water

After the sheet

Redo every problem where you needed the key insight or the reveal, a week later and without notes. Then practise with timed mixed sets, such as LeetCode weekly contests, to keep recognition sharp.

How this sheet was builtHow the sheet works, the resources compared, the gaps they share, and the curation decisions. Optional reading.

Eight well-known resources were compared, then combined. The sheet keeps what they agree on, fills gaps they share, and cuts problems that repeat a lesson.

How the sheet works

285 problems in total: 265 across the 25 patterns and 20 in the mixed set.

Each problem shows its name and link, platform, difficulty, pattern tags, why it's included, what to learn, and what to solve first. Problems within a pattern are grouped as canonical, variation, or combination. Ticked problems are saved in your browser only.

The resources compared

Established agreed across strong resources Synthesis combined from several Judgment call a curation decision of this sheet
ResourceSizeOrganised byStrengthsWeaknesses
Blind 7575TopicThe minimum viable set, and the ancestor of most later lists.Thin on greedy, backtracking, and advanced graphs; one trie problem; no teaching.
Grind 7575–169Weekly scheduleBy the Blind 75 author; easy problems first; adjusts to your hours.A schedule, not a pattern course; the author sees diminishing returns past 169.
NeetCode 15015018 categoriesBlind 75 plus 75 more, in a roadmap order, with a free video for every problem.Light on prefix sums, sweep line, binary search on the answer, and shortest-path variants.
NeetCode 250250CategoriesDescribed by NeetCode as its most beginner-friendly list.More volume in the same categories rather than new patterns.
Sean Prashad's LeetCode Patterns179Pattern tagsExplicit pattern grouping, drawn from Grokking and Blind 75; company filters.No order or explanation within a pattern.
Striver's A2Z~45019 modulesStarts from zero; theory lessons; strong fundamentals; widely used in Indian prep.Long (about six months at a steady pace) with many near-duplicate basics.
Striver's SDE Sheet191TopicInterview-focused revision list.Assumes the fundamentals; topic order rather than pattern order.
Grokking the Coding Interview215–300+26–42 patternsThe origin of pattern-first teaching; every pattern explained.Paid. Editions differ on the pattern count, and niche patterns such as cyclic sort get equal weight.
CSES Problem Set400Algorithm areaRigorous classic algorithms: range queries, graphs, DP.Competitive-programming format (stdin, tight limits), not interview style.

Sizes are as published by each resource. Two claims conflict: one analysis found 64 of the Blind 75 in NeetCode 150, another says every one is included. It doesn't change this sheet, which draws on both.

What the strong resources agree on Established

Around 60 problems recur across Blind 75, Grind 75, NeetCode 150 and the SDE Sheet. They form the spine of this sheet: Two Sum, Group Anagrams, Top K Frequent Elements, Product of Array Except Self, Longest Consecutive Sequence, 3Sum, Container With Most Water, Longest Substring Without Repeating Characters, Minimum Window Substring, Search in Rotated Sorted Array, Reverse Linked List, Merge k Sorted Lists, Valid Parentheses, Validate BST, Binary Tree Level Order Traversal, Number of Islands, Course Schedule, Combination Sum, Word Search, Coin Change, Longest Increasing Subsequence, Word Break, Merge Intervals, Implement Trie, and Find Median from Data Stream, among others.

They also agree on the core pattern families: hashing, two pointers, sliding window, binary search, linked lists, stacks, trees, heaps, backtracking, graphs, dynamic programming, greedy, intervals, tries, and bit manipulation.

Gaps they share Synthesis

  • Prefix sums with a hash map ("subarray sum equals k") and difference arrays: common in interviews, barely covered by the 75 and 150 lists.
  • Binary search on the answer (Koko Eating Bananas, Split Array Largest Sum): usually one or two problems, though it's a distinct way of thinking.
  • Monotonic deque and the full range of monotonic stack uses.
  • Graph variants: multi-source BFS, 0-1 BFS, Dijkstra with state, Bellman-Ford with a limit.
  • DP beyond 1-D and 2-D: state machines, interval DP, bitmask DP, and DP on trees.
  • Pattern recognition itself: every list tells you the pattern in advance. None trains you to find it.

Judgment calls Judgment call

  1. Hybrid base. NeetCode 150's spine, Sean Prashad's pattern breadth, A2Z's fundamentals, and a few CSES classics where LeetCode lacks a clean canonical version.
  2. Free problems only. LeetCode Premium problems are replaced with free equivalents, or with the free NeetCode or LintCode version of the same problem.
  3. Fewer, broader patterns. Cyclic sort folds into hashing ("the array as its own hash map"), and trees are one pattern with DFS, BFS, and BST variations, rather than three.
  4. DP split by the shape of its state (1-D, knapsack, grids and strings, advanced), not one 50-problem block.
  5. Marked as optional: segment trees, Fenwick trees, and string algorithms such as KMP. They're rare in interviews but build long-term strength.
  6. A final mixed set with no pattern labels, so the last step is recognising the pattern yourself.
  7. Python throughout, in LeetCode's function-signature style, using collections, heapq, bisect, and functools.cache.
Sources

All seen through search excerpts; none were loaded in full.

Strengths and weaknesses combine what these sources report with this sheet's own reading of the lists; the gap analysis is a synthesis, not a quoted finding.