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.
| Pattern | Problems | Builds on |
|---|---|---|
| Hashing and counting (including the array as its own hash map) | ~12 | Prerequisites |
| Two pointers | ~12 | Hashing, sorting |
| Sliding window (fixed and variable) | ~12 | Two pointers, hashing |
| Prefix sums and difference arrays | ~10 | Hashing |
| Binary search (on indexes, rotated arrays, and on the answer) | ~14 | Two pointers |
| Linked lists, including fast and slow pointers | ~12 | Two pointers |
| Stacks, monotonic stacks, and monotonic deques | ~14 | Sliding window |
| Intervals and sweep line | ~9 | Sorting, prefix sums |
Stage 2: Recursion and trees
Thinking in subproblems. Backtracking comes first because tree DFS is the same idea with a fixed shape.
| Pattern | Problems | Builds on |
|---|---|---|
| Recursion and backtracking | ~15 | Prerequisites (recursion) |
| Trees: DFS, BFS, and binary search trees | ~24 | Backtracking, queues |
| Heaps: top-K, two heaps, and k-way merge | ~12 | Hashing, linked lists |
Stage 3: Graphs
Trees without the rules: cycles, many components, and weights.
| Pattern | Problems | Builds on |
|---|---|---|
| Graph traversal: DFS, BFS, grids, multi-source BFS | ~14 | Trees |
| Topological sort | ~6 | Graph traversal |
| Union-find | ~7 | Graph traversal |
| Shortest paths and minimum spanning trees | ~8 | Heaps, 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).
| Pattern | Problems | Builds on |
|---|---|---|
| Greedy | ~12 | Sorting, heaps, intervals |
| DP: one-dimensional | ~12 | Backtracking |
| DP: knapsack and subset sums | ~8 | 1-D DP |
| DP: grids and two strings | ~10 | Knapsack |
| DP: advanced (LIS, state machines, intervals, bitmasks, trees) | ~10 | All earlier DP, trees |
Stage 5: Specialised tools
Smaller patterns that come up less often but are hard to invent under pressure.
| Pattern | Problems | Builds on |
|---|---|---|
| Tries | ~6 | Trees, backtracking |
| Bit manipulation | ~7 | Hashing |
| Math, matrices, and simulation | ~6 | Stage 1 |
| Data structure design (LRU cache and similar) | ~5 | Hashing, linked lists, heaps |
| Optional: range queries (Fenwick and segment trees) and string algorithms | ~8 | Prefix 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
| Structure | In Python | Fast | Slow |
|---|---|---|---|
| Dynamic array | list | Index, append, pop(), length: O(1) | insert(0, x), pop(0), x in list, remove: O(n) |
| Hash map | dict, Counter, defaultdict | Get, set, delete, in: O(1) average | Iterating: O(n) |
| Hash set | set | Add, remove, in: O(1) average | Iterating: O(n) |
| Double-ended queue | collections.deque | Append and pop at either end: O(1) | Indexing the middle: O(n) |
| Min-heap | heapq on a list | Push, pop: O(log n); peek: O(1); heapify: O(n) | Finding an arbitrary item: O(n) |
| Sorted list | bisect | Search: O(log n) | insort: O(n), because it shifts elements |
| String | str (immutable) | Index, length: O(1) | Slicing: O(k); += in a loop: can be O(n²) |
| Sort | sorted, list.sort | O(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 n | Target | Typical approaches |
|---|---|---|
| Up to about 10 | O(n!) or O(2ⁿ · n) | Permutations, backtracking |
| Up to about 20 | O(2ⁿ) | Subsets, bitmask DP |
| Up to about 500 | O(n³) | Interval DP, Floyd–Warshall |
| Up to about 5,000 | O(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 more | O(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.
Binary searchHalve the search space each step. Works on any yes/no condition that flips once, not just sorted arrays.
bisect_left finds the first index whose value is at least x; bisect_right finds the first index greater than x. The hand-written template below finds the first value where a condition becomes true. The binary search pattern uses it throughout.
from bisect import bisect_left, bisect_right
a = [1, 3, 3, 3, 7]
bisect_left(a, 3) # 1
bisect_right(a, 3) # 4
count = bisect_right(a, 3) - bisect_left(a, 3) # 3
def first_true(lo, hi, ok):
"""Smallest x in [lo, hi) where ok(x) is True, assuming ok goes
False, ..., False, True, ..., True. Returns hi if none is True."""
while lo < hi:
mid = (lo + hi) // 2
if ok(mid):
hi = mid
else:
lo = mid + 1
return lo
print(first_true(0, len(a), lambda i: a[i] >= 3)) # 1, same as bisect_left
Watch out: Most binary search bugs are off-by-one errors from mixing templates. Pick one, the half-open [lo, hi) form above, and always write the condition as a function of mid.
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.
-
Two SumEasyLeetCode 1
The problem the pattern is named after. Brute force is O(n²); a map makes it one pass.
Key insight
Store each value's index, and before storing, look up
target - x. Checking first stops a number pairing with itself. -
Valid AnagramEasyLeetCode 242
The simplest frequency-counting problem.
Key insight
Two strings are anagrams exactly when their character counts match. A 26-slot array or a
Counterbeats sorting: O(n) instead of O(n log n). -
Group AnagramsMediumLeetCode 49
Introduces grouping by a canonical key, one of the most reused ideas in the sheet.
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.
-
Isomorphic StringsEasyLeetCode 205
A mapping must hold in both directions, which a single map misses.
Key insight
Keep two maps, s-to-t and t-to-s, and fail on any conflict. One map alone accepts "ab" and "aa".
-
Contains Duplicate IIEasyLeetCode 219
Stores an index instead of a flag, so the map answers "how far back?"
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. -
Majority ElementEasyLeetCode 169
Counting solves it in O(n) space; the follow-up asks for O(1).
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.
-
Top K Frequent ElementsMediumLeetCode 347
Counting is the easy half; choosing the top k without a full sort is the lesson.
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.
-
Valid SudokuMediumLeetCode 36
Several sets checked at once, with tuple keys.
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). -
Longest Consecutive SequenceMediumLeetCode 128
Sorting gives O(n log n); a set gives O(n) if you avoid recounting.
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.
-
Find All Numbers Disappeared in an ArrayEasyLeetCode 448
Introduces using the array itself as the map, for O(1) extra space.
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.
-
4Sum IIMediumLeetCode 454
Splits a four-way search into two halves that meet in a map.
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⁴).
-
First Missing PositiveHardLeetCode 41
A classic hard problem that needs O(n) time and O(1) space together.
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.
-
Valid PalindromeEasyLeetCode 125
Converging pointers with a filter: skip characters that don't count.
Key insight
Move each pointer past non-alphanumeric characters before comparing, and compare lowercase. No need to build a cleaned copy.
-
Two Sum II: Input Array Is SortedMediumLeetCode 167
The converging template itself. Compare it with the hash map version.
Solve first: Two Sum
Key insight
Sorted order tells you which pointer to move: too small means move
loright, too big means movehileft. O(1) space, unlike the map. -
Remove Duplicates from Sorted ArrayEasyLeetCode 26
The read-and-write template in its plainest form.
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.
-
Move ZeroesEasyLeetCode 283
Read-and-write where the discarded items must end up somewhere.
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.
-
Squares of a Sorted ArrayEasyLeetCode 977
Converging pointers that build the output from the largest end.
Key insight
The largest square is at one of the two ends. Compare the ends and write into the result from the back.
-
Merge Sorted ArrayEasyLeetCode 88
Two sequences, merged in place. Going forward overwrites data; going backward doesn't.
Key insight
Fill
nums1from the end with the larger of the two current tails. Only leftovernums2elements need copying at the end. -
Valid Palindrome IIEasyLeetCode 680
Adds one branching decision to the converging pattern.
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).
-
3SumMediumLeetCode 15
The most asked two-pointer problem: fix one value, then run Two Sum II on the rest.
Solve first: Two Sum II: Input Array Is Sorted
Key insight
Sort, then for each
irun converging pointers on the right side. Skip equal neighbours for bothiand the pointers to avoid duplicate triplets. -
Container With Most WaterMediumLeetCode 11
Converging pointers on unsorted input, justified by an argument rather than order.
Key insight
Area is limited by the shorter line, so moving the taller one can never help. Always move the shorter side.
-
Sort ColorsMediumLeetCode 75
Three regions instead of two: the Dutch national flag partition.
Key insight
Keep
low,midandhigh. A 0 swaps tolow, a 2 swaps tohigh, a 1 stays. Don't advancemidafter swapping withhigh; the incoming value is unchecked.
Combinations and stretch 2
The pattern mixed with another, or pushed harder.
-
Boats to Save PeopleMediumLeetCode 881
Sorting plus converging pointers plus a greedy choice.
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.
-
Trapping Rain WaterHardLeetCode 42
A famous hard problem with prefix-max, stack, and two-pointer solutions. The two-pointer one is the most elegant.
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.
-
Best Time to Buy and Sell StockEasyLeetCode 121
The simplest "track the best so far while scanning" problem.
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.
-
Maximum Average Subarray IEasyLeetCode 643
The fixed-size window in its plainest form.
Key insight
Keep a running sum: add the entering element and subtract the leaving one. Compare sums, not averages, since k is constant.
-
Longest Substring Without Repeating CharactersMediumLeetCode 3
The canonical variable window, and one of the most asked problems anywhere.
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.
-
Minimum Size Subarray SumMediumLeetCode 209
Flips the goal to the shortest valid 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.
-
Max Consecutive Ones IIIMediumLeetCode 1004
Validity defined by a budget: at most k zeros in the 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.
-
Fruit Into BasketsMediumLeetCode 904
"Longest window with at most two distinct values", disguised as a story.
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.
-
Longest Repeating Character ReplacementMediumLeetCode 424
A validity condition that needs a small insight to state.
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. -
Permutation in StringMediumLeetCode 567
Fixed window plus frequency matching.
Solve first: Valid Anagram
Key insight
Slide a window of length
len(s1)overs2and compare letter counts. Tracking how many of the 26 counts currently match makes each step O(1). -
Maximum Points You Can Obtain from CardsMediumLeetCode 1423
The window is hidden: you pick from the ends, not the middle.
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.
-
Frequency of the Most Frequent ElementMediumLeetCode 1838
Sorting first makes a window condition possible.
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. -
Subarrays with K Different IntegersHardLeetCode 992
Teaches the "exactly K" trick, which applies well beyond this problem.
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 + 1per step. -
Minimum Window SubstringHardLeetCode 76
The hardest standard window problem; it combines everything above.
Solve first: Minimum Size Subarray Sum
Key insight
Track how many required characters are fully satisfied (
haveversusneed). 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.
-
Range Sum Query: ImmutableEasyLeetCode 303
The reason prefix sums exist: many range queries on unchanging data.
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. -
Find Pivot IndexEasyLeetCode 724
Left and right sums from one total, without a second array.
Key insight
Keep a running left sum; the right sum is
total − left − nums[i]. The pivot is where they're equal. -
Product of Array Except SelfMediumLeetCode 238
Prefix and suffix passes, and a staple of every major list.
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.
-
Maximum SubarrayMediumLeetCode 53
Links prefix sums to Kadane's algorithm, which reappears in DP.
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. -
Subarray Sum Equals KMediumLeetCode 560
The key prefix-plus-map problem. A sliding window fails because of negative numbers.
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. -
Contiguous ArrayMediumLeetCode 525
Transform the input first, then apply the pattern.
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.
-
Subarray Sums Divisible by KMediumLeetCode 974
Prefix sums compared by remainder instead of value.
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. -
Range Sum Query 2D: ImmutableMediumLeetCode 304
Extends prefix sums to grids with inclusion-exclusion.
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. -
Corporate Flight BookingsMediumLeetCode 1109
The canonical difference array: many range updates, one read.
Key insight
For each booking add seats at
first − 1and subtract atlast; 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.
-
Number of Submatrices That Sum to TargetHardLeetCode 1074
Combines 2-D prefix sums with Subarray Sum Equals K.
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 5
Binary search
Repeatedly halve a search space using a yes/no condition. It applies to sorted arrays, but also to any answer range where "is x good enough?" flips from no to yes exactly once.
Recognise it when
- The input is sorted, or sorted in pieces (rotated)
- You need O(log n), or n is huge
- The question asks for the minimum value that works, or the maximum
- Checking a candidate answer is easy even though finding it isn't
Variations
- Exact match and lower or upper bounds
- Rotated or partly sorted arrays: decide which half is ordered
- Search on a slope or parity condition, without sorting
- Binary search on the answer: search the value range with a feasibility check
Core intuition: Write the condition as a function that is False, then True. Binary search finds the boundary. For "maximise the minimum" problems, flip it so the answer is the last True.
Template
def first_true(lo, hi, ok):
"""Smallest x in [lo, hi) with ok(x) True; hi if none."""
while lo < hi:
mid = (lo + hi) // 2
if ok(mid):
hi = mid
else:
lo = mid + 1
return lo
# On indexes: first position with a[i] >= target
i = first_true(0, len(a), lambda i: a[i] >= target)
# On the answer: smallest value that is feasible
def feasible(x):
return cost_at(x) <= limit # must flip False -> True once
answer = first_true(low, high + 1, feasible)
Same template as the Prerequisites card. Once it's automatic, every problem below is just a new ok function.
Canonical 3
The pattern in its plainest form. Solve these first.
-
Binary SearchEasyLeetCode 704
Get the loop exactly right once, then never think about it again.
Key insight
With the half-open template, search for the first index with
nums[i] ≥ target, then check whether it holds the target. -
Search Insert PositionEasyLeetCode 35
The lower-bound question in its purest form.
Key insight
The insert position is the first index with
nums[i] ≥ target, which is exactly what the template returns, includinglen(nums)when every value is smaller. -
Find First and Last Position of Element in Sorted ArrayMediumLeetCode 34
Two boundaries from two conditions.
Solve first: Search Insert Position
Key insight
First position: first index with value ≥ target. Last position: (first index with value > target) − 1. The same as
bisect_leftandbisect_right.
Variations 8
The same idea with a twist that changes the details.
-
Search a 2D MatrixMediumLeetCode 74
A 2-D structure that's secretly one sorted list.
Key insight
Search indexes 0 to
m × n − 1, and map index i tomatrix[i // n][i % n]. -
Find Minimum in Rotated Sorted ArrayMediumLeetCode 153
The first problem where the condition isn't "≥ target".
Key insight
Compare
nums[mid]with the last element. Everything at or before the minimum's position is greater than the last element; everything after is not. That's a False-then-True condition. -
Search in Rotated Sorted ArrayMediumLeetCode 33
Decide which half is sorted, then whether the target lies inside it.
Solve first: Find Minimum in Rotated Sorted Array
Key insight
At each step one half is in order. If the target lies within that half's range, search it; otherwise search the other. Or find the minimum first, then binary search the right piece.
-
Find Peak ElementMediumLeetCode 162
Binary search on data that isn't sorted at all.
Key insight
If
nums[mid] < nums[mid + 1], you're on an upward slope, so a peak exists to the right. Otherwise one exists at mid or to the left. -
Time Based Key-Value StoreMediumLeetCode 981
Binary search inside a data structure you design.
Key insight
Timestamps arrive in increasing order, so each key's list stays sorted.
getisbisect_right(times, t) − 1, the last time ≤ t. -
Koko Eating BananasMediumLeetCode 875
The canonical binary search on the answer.
Key insight
Search speeds from 1 to
max(piles). A speed is feasible ifsum(ceil(p / speed))≤ h. Faster is always at least as feasible, so the condition flips once. -
Capacity To Ship Packages Within D DaysMediumLeetCode 1011
Binary search on the answer with a greedy feasibility check.
Solve first: Koko Eating Bananas
Key insight
Search capacities from
max(weights)tosum(weights). To check a capacity, fill each day greedily in order and count the days needed. -
Magnetic Force Between Two BallsMediumLeetCode 1552
"Maximise the minimum": the condition runs the other way.
Solve first: Capacity To Ship Packages Within D Days
Key insight
Sort positions. "Can I place m balls at least d apart?" is True for small d and False for large d. Place greedily from the left, and search for the largest d that's still True.
Combinations and stretch 3
The pattern mixed with another, or pushed harder.
-
Split Array Largest SumHardLeetCode 410
The hard version of the shipping problem, and a common interview favourite.
Solve first: Capacity To Ship Packages Within D Days
Key insight
It's Capacity To Ship with days renamed to subarrays: the smallest maximum sum for which a greedy split needs at most k pieces.
-
Kth Smallest Element in a Sorted MatrixMediumLeetCode 378
Search over values rather than positions.
Solve first: Koko Eating Bananas
Key insight
For a candidate value x, count entries ≤ x by walking a staircase from the bottom-left in O(n). The answer is the smallest x whose count is ≥ k. A heap also works; compare both.
-
Median of Two Sorted ArraysHardLeetCode 4
The most famous binary search problem: search on a partition point.
Solve first: Find First and Last Position of Element in Sorted Array
Key insight
Binary search how many elements to take from the shorter array. A partition is right when both left maxima are ≤ both right minima. Use ±infinity at the edges.
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.
-
Reverse Linked ListEasyLeetCode 206
The most reused linked-list operation. Write it until it's automatic.
Key insight
Walk the list with
prevandcur. Savecur.next, pointcur.nextatprev, then advance both. Also write the recursive version once. -
Merge Two Sorted ListsEasyLeetCode 21
Merging with a dummy head, the linked-list twin of Merge Sorted Array.
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.
-
Linked List CycleEasyLeetCode 141
Floyd's cycle detection: O(1) space instead of a visited set.
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.
-
Middle of the Linked ListEasyLeetCode 876
Fast and slow used for position rather than cycles.
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.
-
Remove Nth Node From End of ListMediumLeetCode 19
Gap pointers, plus the dummy head that handles removing the first node.
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.
-
Intersection of Two Linked ListsEasyLeetCode 160
A neat trick for lists of unknown, different lengths.
Key insight
Walk both pointers; when one reaches the end, restart it at the other list's head. Both then travel
a + bsteps and meet at the intersection, or both reach None together. -
Linked List Cycle IIMediumLeetCode 142
Finds where the cycle starts, not just whether it exists.
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.
-
Add Two NumbersMediumLeetCode 2
Careful simulation with a carry across two lists of different lengths.
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.
-
Reorder ListMediumLeetCode 143
Three linked-list skills chained together.
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.
-
Find the Duplicate NumberMediumLeetCode 287
Floyd's algorithm on an array: the most surprising reuse in this pattern.
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. -
Copy List with Random PointerMediumLeetCode 138
Deep-copying a structure whose pointers reach anywhere.
Key insight
First pass: map each old node to a new copy. Second pass: set each copy's
nextandrandomthrough the map. An O(1)-space version interleaves copies into the original list. -
Reverse Nodes in k-GroupHardLeetCode 25
Reversal applied repeatedly to segments, with careful reconnection.
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.
-
Valid ParenthesesEasyLeetCode 20
The canonical stack problem.
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.
-
Min StackMediumLeetCode 155
Keeping extra state alongside each stack entry.
Key insight
Store the minimum so far with every push, as a pair, or in a second stack.
getMinis then the top's stored minimum. -
Evaluate Reverse Polish NotationMediumLeetCode 150
Using a stack to evaluate expressions.
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.
-
Implement Queue using StacksEasyLeetCode 232
A short lesson in amortised O(1).
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.
-
Daily TemperaturesMediumLeetCode 739
The canonical monotonic stack problem.
Key insight
Keep a stack of indexes with decreasing temperatures. A warmer day pops every colder index and records the distance
i − popped. -
Next Greater Element IIMediumLeetCode 503
The same idea on a circular array.
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. -
Online Stock SpanMediumLeetCode 901
A monotonic stack for the previous greater element, processed as a stream.
Key insight
Store (price, span) pairs. A new price pops all smaller-or-equal prices and absorbs their spans.
-
Remove K DigitsMediumLeetCode 402
A monotonic stack used greedily to build the smallest number.
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.
-
Decode StringMediumLeetCode 394
Nesting handled with a stack of saved states.
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. -
Asteroid CollisionMediumLeetCode 735
A stack as a simulation of what's still standing.
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.
-
Car FleetMediumLeetCode 853
Sort first; then the stack question becomes simple.
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.
-
Sliding Window MaximumHardLeetCode 239
Introduces the monotonic deque, joining this pattern with sliding windows.
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.
-
Largest Rectangle in HistogramHardLeetCode 84
The classic hard monotonic stack problem.
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.
-
Shortest Subarray with Sum at Least KHardLeetCode 862
What to do when Minimum Size Subarray Sum meets negative numbers.
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.
-
Merge IntervalsMediumLeetCode 56
The canonical interval problem.
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. -
Insert IntervalMediumLeetCode 57
Merging without sorting, since the input is already in order.
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.
-
Non-overlapping IntervalsMediumLeetCode 435
Sorting by end instead of start, and why that's the greedy choice.
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.
-
Interval List IntersectionsMediumLeetCode 986
Two pointers over two sorted interval lists.
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. -
Remove Covered IntervalsMediumLeetCode 1288
A two-key sort that makes one pass enough.
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.
-
My Calendar IMediumLeetCode 729
Overlap checks inside a data structure.
Key insight
A booking conflicts with an existing one when
start < other_endandother_start < end. A list check is O(n) per booking; a sorted list withbisectchecks only the neighbours. -
Car PoolingMediumLeetCode 1094
A sweep line with a capacity check.
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.
-
Divide Intervals Into Minimum Number of GroupsMediumLeetCode 2406
"Meeting Rooms II" as a free problem: the minimum groups equals the maximum overlap.
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.
-
Minimum Interval to Include Each QueryHardLeetCode 1851
Sorting both intervals and queries, then a heap. Come back to it after the Heaps pattern.
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.
-
Pow(x, n)MediumLeetCode 50
Plain recursion that halves the problem, before any backtracking.
Key insight
xn = (xn/2)², times x when n is odd, which makes it O(log n). For negative n, compute 1 / x−n.
-
SubsetsMediumLeetCode 78
The decision tree in its simplest form: every element is in or out.
Key insight
There are 2ⁿ subsets. Either branch on include or exclude for each element, or loop from
startand record the path at every call; every node of that recursion tree is a subset. -
PermutationsMediumLeetCode 46
Order matters, so any unused element can go in each position.
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.
-
Combination SumMediumLeetCode 39
Combinations where a number can be reused, with pruning on the running sum.
Key insight
Recurse with the same index
irather thani + 1so 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.
-
Subsets IIMediumLeetCode 90
Introduces the standard way to handle duplicate input.
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. -
Combination Sum IIMediumLeetCode 40
Combines two earlier ideas: no reuse, and no duplicate combinations.
Solve first: Combination Sum, Subsets II
Key insight
Move to
i + 1so each number is used once, and skip equal siblings as in Subsets II. -
Permutations IIMediumLeetCode 47
Duplicates in permutations need a slightly different rule.
Solve first: Permutations, Subsets II
Key insight
Sort, then skip
nums[i]when it equalsnums[i − 1]and that previous copy isn't currently used. This forces equal values into a fixed order, so each arrangement appears once. -
Letter Combinations of a Phone NumberMediumLeetCode 17
A fixed number of levels, with a different set of choices at each.
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.
-
Generate ParenthesesMediumLeetCode 22
Pruning so strong that no invalid string is ever built.
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. -
Palindrome PartitioningMediumLeetCode 131
Partitioning: the choice is where the next piece ends.
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. -
Word SearchMediumLeetCode 79
Backtracking on a grid, where "un-choose" means restoring a cell.
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.
-
Partition to K Equal Sum SubsetsMediumLeetCode 698
Pruning decides whether this finishes in time.
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. -
Different Ways to Add ParenthesesMediumLeetCode 241
Divide and conquer that returns lists of results, a bridge towards DP.
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.
-
N-QueensHardLeetCode 51
The classic constraint-search problem.
Solve first: Permutations
Key insight
Place one queen per row. Keep sets of used columns and both diagonals; a cell's diagonals are
r − candr + c, so every check is O(1). -
Sudoku SolverHardLeetCode 37
Constraint search where the order of choices matters for speed.
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.
-
Maximum Depth of Binary TreeEasyLeetCode 104
The simplest bottom-up DFS.
Key insight
Depth is 1 + the deeper child's depth, and 0 for an empty tree. Solve it with BFS too, counting levels.
-
Invert Binary TreeEasyLeetCode 226
A structural change applied at every node.
Key insight
Swap each node's children, then invert both subtrees. Any traversal order works as long as every node is visited once.
-
Same TreeEasyLeetCode 100
Recursion over two trees at once.
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.
-
Binary Tree Level Order TraversalMediumLeetCode 102
The BFS template, and the base for every level-based question.
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.
-
Diameter of Binary TreeEasyLeetCode 543
The first "return one thing, update another" problem.
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.
-
Balanced Binary TreeEasyLeetCode 110
Returning a sentinel to avoid repeated work.
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²).
-
Symmetric TreeEasyLeetCode 101
Same Tree, but comparing mirrored positions.
Solve first: Same Tree
Key insight
Compare the two subtrees as mirrors:
left.leftagainstright.right, andleft.rightagainstright.left. -
Subtree of Another TreeEasyLeetCode 572
Reusing a helper at every node.
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).
-
Count Good Nodes in Binary TreeMediumLeetCode 1448
Top-down DFS: state flows from parent to child.
Key insight
Pass down the largest value seen on the path from the root. A node is good when its value is ≥ that maximum.
-
Path Sum IIMediumLeetCode 113
Backtracking on a tree: build a path, record it, undo.
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.
-
Lowest Common Ancestor of a Binary TreeMediumLeetCode 236
A bottom-up search where the return value carries meaning.
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.
-
Binary Tree Right Side ViewMediumLeetCode 199
A per-level question answered with the BFS template.
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.
-
Maximum Width of Binary TreeMediumLeetCode 662
Positions, not just values, matter within a level.
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.
-
Validate Binary Search TreeMediumLeetCode 98
The BST property is about whole subtrees, not just children.
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. -
Kth Smallest Element in a BSTMediumLeetCode 230
In-order traversal is sorted 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.
-
Lowest Common Ancestor of a Binary Search TreeMediumLeetCode 235
The same question as the general tree, answered faster with ordering.
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.
-
Convert Sorted Array to Binary Search TreeEasyLeetCode 108
Building a balanced tree from sorted data.
Key insight
Make the middle element the root so both sides are the same size, and build each half recursively.
-
Delete Node in a BSTMediumLeetCode 450
Changing a BST's structure while keeping it valid.
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.
-
Binary Search Tree IteratorMediumLeetCode 173
In-order traversal done lazily, one step at a time.
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.
-
Construct Binary Tree from Preorder and Inorder TraversalMediumLeetCode 105
Rebuilding a tree from two traversals.
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).
-
Path Sum IIIMediumLeetCode 437
Prefix sums from Stage 1 applied along tree paths.
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.
-
Binary Tree Maximum Path SumHardLeetCode 124
The hardest version of "return one thing, update another".
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. -
All Nodes Distance K in Binary TreeMediumLeetCode 863
Turns a tree into a graph, a bridge to Stage 3.
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.
-
Serialize and Deserialize Binary TreeHardLeetCode 297
Designing a format that captures structure, not just values.
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.
-
Kth Largest Element in a StreamEasyLeetCode 703
The size-k heap in its plainest form.
Key insight
Keep a min-heap of exactly k elements; its top is the k-th largest. Each
addis O(log k). -
Last Stone WeightEasyLeetCode 1046
A max-heap in Python, and a simple heap simulation.
Key insight
Push negated weights. Pop the two heaviest and push back their difference if it isn't zero.
-
K Closest Points to OriginMediumLeetCode 973
Top-K where you keep the smallest, so the heap flips to a max-heap.
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.
-
Kth Largest Element in an ArrayMediumLeetCode 215
The classic place to compare a heap with quickselect.
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.
-
Top K Frequent WordsMediumLeetCode 692
Top-K with a tie-break rule that runs in the opposite direction.
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. -
Task SchedulerMediumLeetCode 621
A greedy heap simulation that also has a closed-form answer.
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. -
Reorganize StringMediumLeetCode 767
Greedy with a heap, plus holding an item back for one turn.
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. -
Merge k Sorted ListsHardLeetCode 23
The canonical k-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).
-
Find Median from Data StreamHardLeetCode 295
The canonical two-heap problem.
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.
-
Find K Pairs with Smallest SumsMediumLeetCode 373
Expanding a frontier instead of generating every pair.
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.
-
IPOHardLeetCode 502
Sorting plus a heap to pick the best option you can currently afford.
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.
-
Smallest Range Covering Elements from K ListsHardLeetCode 632
K-way merge while tracking a moving range.
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.
-
Find if Path Exists in GraphEasyLeetCode 1971
Building an adjacency list from an edge list, then a plain search.
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.
-
Number of IslandsMediumLeetCode 200
The canonical grid problem: counting connected components.
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.
-
Clone GraphMediumLeetCode 133
Traversal that builds a copy as it goes.
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.
-
Max Area of IslandMediumLeetCode 695
Each search returns a value instead of just marking.
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.
-
Rotting OrangesMediumLeetCode 994
Introduces multi-source BFS, where time is the number of levels.
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.
-
01 MatrixMediumLeetCode 542
Reversing the direction of the search to save work.
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.
-
Pacific Atlantic Water FlowMediumLeetCode 417
Searching backwards from the goal instead of from every start.
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.
-
Surrounded RegionsMediumLeetCode 130
Mark what survives, then flip everything else.
Solve first: Pacific Atlantic Water Flow
Key insight
Only regions touching the border survive. Mark every
Oconnected to the border first, then turn the remainingOs intoXand restore the marked ones. -
Is Graph Bipartite?MediumLeetCode 785
Traversal that assigns a label and checks a rule.
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.
-
Shortest Path in Binary MatrixMediumLeetCode 1091
BFS for the shortest path, with eight directions.
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.
-
Open the LockMediumLeetCode 752
A graph that's never given to you: states and moves.
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.
-
Shortest BridgeMediumLeetCode 934
DFS and BFS in sequence, each doing what it's best at.
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.
-
Word LadderHardLeetCode 127
BFS on an implicit graph where finding neighbours is the hard part.
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. -
Shortest Path to Get All KeysHardLeetCode 864
BFS where a node is a cell plus what you're carrying.
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.
-
Course ScheduleMediumLeetCode 207
The canonical topological sort question.
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.
-
Course Schedule IIMediumLeetCode 210
Return the order itself.
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.
-
Find Eventual Safe StatesMediumLeetCode 802
Kahn's algorithm on the reversed 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.
-
Minimum Height TreesMediumLeetCode 310
Topological-style peeling on an undirected tree.
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.
-
Parallel Courses IIIHardLeetCode 2050
DP along a topological order: the longest path in a DAG.
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.
-
Longest Increasing Path in a MatrixHardLeetCode 329
Spotting a DAG hidden in a grid.
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.
-
Number of ProvincesMediumLeetCode 547
Counting components, solvable by both union-find and DFS.
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.
-
Redundant ConnectionMediumLeetCode 684
Cycle detection in an undirected graph as edges are added.
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.
-
Number of Operations to Make Network ConnectedMediumLeetCode 1319
Turning a component count into an answer.
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.
-
Satisfiability of Equality EquationsMediumLeetCode 990
Union first, then check constraints.
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. -
Accounts MergeMediumLeetCode 721
Union-find over strings instead of numbered nodes.
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.
-
Smallest String With SwapsMediumLeetCode 1202
Grouping positions, then working inside each group.
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.
-
Evaluate DivisionMediumLeetCode 399
A graph with multiplicative weights, solvable two ways.
Solve first: Clone Graph, Redundant Connection
Key insight
Each equation
a / b = kis 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.
-
Network Delay TimeMediumLeetCode 743
Dijkstra in its standard form.
Key insight
Run Dijkstra from k. The answer is the largest shortest distance, or −1 if any node is unreachable.
-
Min Cost to Connect All PointsMediumLeetCode 1584
The canonical minimum spanning tree problem.
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.
-
Path With Minimum EffortMediumLeetCode 1631
Dijkstra where the cost is the worst step, not the total.
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. -
Cheapest Flights Within K StopsMediumLeetCode 787
A limit on edges breaks plain Dijkstra.
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.
-
Find the City With the Smallest Number of Neighbors at a Threshold DistanceMediumLeetCode 1334
All-pairs shortest paths on a small graph.
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.
-
Minimum Obstacle Removal to Reach CornerHardLeetCode 2290
0-1 BFS: Dijkstra's result without a heap.
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.
-
Swim in Rising WaterHardLeetCode 778
One problem, three valid techniques.
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.
-
Number of Ways to Arrive at DestinationMediumLeetCode 1976
Dijkstra that counts shortest paths as well as measuring them.
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.
-
Assign CookiesEasyLeetCode 455
The simplest sort-and-match greedy, with an easy exchange argument.
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.
-
Jump GameMediumLeetCode 55
Tracking the farthest reach instead of exploring every path.
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²).
-
Best Time to Buy and Sell Stock IIMediumLeetCode 122
Greedy that works because the choices don't interact.
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.
-
Jump Game IIMediumLeetCode 45
The minimum jumps, found with BFS-style levels in O(1) space.
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.
-
Gas StationMediumLeetCode 134
A running balance that tells you where not to start.
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.
-
Partition LabelsMediumLeetCode 763
Greedy extension of a boundary, using precomputed last positions.
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.
-
Hand of StraightsMediumLeetCode 846
The smallest remaining item forces your hand.
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.
-
Valid Parenthesis StringMediumLeetCode 678
Tracking a range of possibilities instead of branching.
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. -
Two City SchedulingMediumLeetCode 1029
Sorting by a derived key, justified by an exchange argument.
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. -
Largest NumberMediumLeetCode 179
A custom comparator where no simple key works.
Key insight
Put a before b when
a + b > b + aas strings; usefunctools.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.
-
CandyHardLeetCode 135
Two one-directional passes combined.
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.
-
Minimum Number of Refueling StopsHardLeetCode 871
Greedy with a heap: decide retroactively which past option to use.
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.
-
Climbing StairsEasyLeetCode 70
The first DP: counting ways by the last step.
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.
-
Min Cost Climbing StairsEasyLeetCode 746
Minimisation instead of counting, with a careful definition of "the top".
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.
-
House RobberMediumLeetCode 198
The take-or-skip recurrence that half of 1-D DP reuses.
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.
-
House Robber IIMediumLeetCode 213
Reducing a circular problem to two linear ones.
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.
-
Delete and EarnMediumLeetCode 740
Recognising a known DP after transforming the input.
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. -
Decode WaysMediumLeetCode 91
Counting with validity checks, where edge cases are the lesson.
Solve first: Climbing Stairs
Key insight
f(i)getsf(i + 1)ifs[i]isn't '0', plusf(i + 2)ifs[i:i+2]is between 10 and 26. A '0' can never start a code. -
Word BreakMediumLeetCode 139
Prefix feasibility: can the string from i onward be split?
Solve first: Decode Ways
Key insight
can(i)is true if some dictionary word matches at i andcan(i + len(word))is true. Memoise on i, and use a set of words. -
Maximum Product SubarrayMediumLeetCode 152
Kadane's idea with two tracked values, because of negative numbers.
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.
-
Minimum Cost For TicketsMediumLeetCode 983
Choices that cover several future positions at once.
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. -
Solving Questions With BrainpowerMediumLeetCode 2140
Why some DPs are easier solved from the back.
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.
-
Maximum Sum Circular SubarrayMediumLeetCode 918
Two Kadane runs combined.
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.
-
Word Break IIHardLeetCode 140
DP returning lists instead of booleans: memoised backtracking.
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.
-
Partition Equal Subset SumMediumLeetCode 416
The canonical 0/1 knapsack, with booleans.
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. -
Coin ChangeMediumLeetCode 322
The canonical unbounded knapsack, minimising a count.
Key insight
dp[a] = min(dp[a − coin] + 1)over coins, withdp[0] = 0and infinity for unreachable amounts. For a minimum, loop order doesn't change the answer. -
Coin Change IIMediumLeetCode 518
Counting combinations, where loop order starts to matter.
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.
-
Target SumMediumLeetCode 494
A sign problem that reduces to subset counting.
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.
-
Combination Sum IVMediumLeetCode 377
The mirror image of Coin Change II: here order matters.
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.
-
Perfect SquaresMediumLeetCode 279
Recognising coins in disguise.
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. -
Last Stone Weight IIMediumLeetCode 1049
A simulation that is secretly a partition problem.
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.
-
Ones and ZeroesMediumLeetCode 474
A knapsack with two budgets at once.
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.
-
Unique PathsMediumLeetCode 62
The simplest 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. -
Longest Common SubsequenceMediumLeetCode 1143
The two-string table that the rest of this pattern reuses.
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.
-
Minimum Path SumMediumLeetCode 64
The same table, minimising instead of counting.
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.
-
Maximal SquareMediumLeetCode 221
A cell that combines three neighbours.
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. -
Edit DistanceMediumLeetCode 72
The most important two-string DP after LCS.
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.
-
Longest Palindromic SubstringMediumLeetCode 5
Two indexes inside one string, and a simpler alternative to the table.
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.
-
Palindromic SubstringsMediumLeetCode 647
Counting with the same centre expansion.
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.
-
Interleaving StringMediumLeetCode 97
A boolean table where each cell has two possible sources.
Solve first: Longest Common Subsequence
Key insight
dp[i][j]says whethers3[:i + j]can come froms1[:i]ands2[:j]. It's true if the last character matches s1 anddp[i − 1][j]holds, or matches s2 anddp[i][j − 1]holds. Check lengths first.
Combinations and stretch 2
The pattern mixed with another, or pushed harder.
-
Distinct SubsequencesHardLeetCode 115
Counting over two strings.
Solve first: Longest Common Subsequence
Key insight
dp[i][j]counts ways to formt[:j]froms[:i]. Always inheritdp[i − 1][j](skip this character of s); when the characters match, also adddp[i − 1][j − 1]. An empty t can be formed exactly once. -
Regular Expression MatchingHardLeetCode 10
The classic hard string DP.
Solve first: Edit Distance
Key insight
dp[i][j]says whethers[:i]matchesp[:j]. A*either uses zero of the previous element (dp[i][j − 2]) or one more of it, if that element matchess[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.
-
Longest Increasing SubsequenceMediumLeetCode 300
The best-known DP with a faster, non-obvious improvement.
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 withbisect_left. -
Best Time to Buy and Sell Stock with CooldownMediumLeetCode 309
The state machine DP in its clearest form.
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.
-
House Robber IIIMediumLeetCode 337
DP on a tree, returning a pair from each node.
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.
-
Russian Doll EnvelopesHardLeetCode 354
Reducing a 2-D problem to LIS with one sorting trick.
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.
-
Longest String ChainMediumLeetCode 1048
LIS where "previous" is defined by a rule, not an index.
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).
-
Best Time to Buy and Sell Stock IVHardLeetCode 188
A state machine with a transaction counter.
Solve first: Best Time to Buy and Sell Stock with Cooldown
Key insight
Keep
buy[j]andsell[j]for j = 1 to k, wherebuy[j]builds onsell[j − 1]. If k ≥ n / 2, the limit never binds and it's the unlimited case. -
Predict the WinnerMediumLeetCode 486
Interval DP for a two-player game.
Key insight
Let
diff(i, j)be the best score margin the player to move can force onnums[i..j]:max(nums[i] − diff(i + 1, j), nums[j] − diff(i, j − 1)). Player 1 wins ifdiff(0, n − 1) ≥ 0. -
Burst BalloonsHardLeetCode 312
The classic interval DP, where the trick is choosing which balloon goes last.
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.
-
Binary Tree CamerasHardLeetCode 968
Tree DP with three states, which also has a greedy reading.
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)readscountbefore the DFS has updated it. -
Minimum Number of Work Sessions to Finish the TasksMediumLeetCode 1986
Bitmask DP: the state is which tasks are done.
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.
-
Implement Trie (Prefix Tree)MediumLeetCode 208
The data structure itself.
Key insight
Each node maps characters to children and marks whether a word ends there.
searchneeds the end flag;startsWithonly needs the path to exist.
Variations 3
The same idea with a twist that changes the details.
-
Design Add and Search Words Data StructureMediumLeetCode 211
A trie search that branches.
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. -
Replace WordsMediumLeetCode 648
Stopping at the shortest matching prefix.
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.
-
Search Suggestions SystemMediumLeetCode 1268
Storing answers at nodes to make queries instant.
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.
-
Word Search IIHardLeetCode 212
A trie that turns many searches into one.
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.
-
Maximum XOR of Two Numbers in an ArrayMediumLeetCode 421
A trie over bits instead of letters.
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.
-
Single NumberEasyLeetCode 136
The XOR trick in its plainest form.
Key insight
a ^ a = 0anda ^ 0 = a, so XORing every number cancels the pairs and leaves the single one. O(1) space. -
Number of 1 BitsEasyLeetCode 191
Clearing the lowest set bit.
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.
-
Counting BitsEasyLeetCode 338
Bits combined with a tiny DP.
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. -
Missing NumberEasyLeetCode 268
XOR cancelling across two sequences.
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 − totalworks too. -
Reverse BitsEasyLeetCode 190
Moving bits one at a time, with a fixed width.
Key insight
Take the lowest bit of n 32 times, shifting each onto the result. Python integers are unbounded, so loop exactly 32 times.
-
Single Number IIMediumLeetCode 137
Counting per bit position when XOR alone can't help.
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.
-
Sum of Two IntegersMediumLeetCode 371
Addition from bit operations, and Python's unbounded integers.
Solve first: Single Number
Key insight
a ^ badds without carrying and(a & b) << 1is the carry; repeat until the carry is 0. In Python, mask with0xFFFFFFFFeach 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.
-
Rotate ImageMediumLeetCode 48
In-place matrix transformation by composing simple steps.
Key insight
Rotating 90° clockwise equals transposing (swapping across the main diagonal) then reversing each row. Both steps are in place.
-
Spiral MatrixMediumLeetCode 54
Boundary bookkeeping, the core skill of this pattern.
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.
-
Set Matrix ZeroesMediumLeetCode 73
Reusing part of the input as marker storage.
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.
-
Happy NumberEasyLeetCode 202
Floyd's cycle detection on numbers instead of nodes.
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.
-
Count PrimesMediumLeetCode 204
The Sieve of Eratosthenes.
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.
-
Multiply StringsMediumLeetCode 43
Grade-school multiplication with careful positions.
Key insight
Digits i and j (counted from the left) contribute to position
i + j + 1of 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.
-
LRU CacheMediumLeetCode 146
The most asked design problem.
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.
OrderedDictgives the same behaviour in a few lines.
Variations 3
The same idea with a twist that changes the details.
-
Insert Delete GetRandom O(1)MediumLeetCode 380
Swap-to-delete, a trick worth knowing.
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.choiceis O(1). -
Snapshot ArrayMediumLeetCode 1146
Versioned data stored sparsely.
Solve first: Time Based Key-Value Store
Key insight
Keep each index's history as (snapshot id, value) pairs, appending only when set.
getis a binary search for the last entry at or before the requested snapshot. -
Design TwitterMediumLeetCode 355
A design that combines hash maps and a k-way merge.
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.
-
LFU CacheHardLeetCode 460
The hardest standard cache, with two levels of ordering.
Solve first: LRU Cache
Key insight
Map key → (value, frequency) and frequency → an
OrderedDictof 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.
-
Range Sum Query: MutableMediumLeetCode 307
The problem these trees exist for.
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.
-
Find the Index of the First Occurrence in a StringEasyLeetCode 28
The standard place to learn KMP.
Key insight
str.findis the practical answer. For KMP, compute the prefix function ofpattern + "#" + text; a value equal tolen(pattern)marks where a match ends. O(n + m).
Variations 4
The same idea with a twist that changes the details.
-
Count of Smaller Numbers After SelfHardLeetCode 315
A Fenwick tree indexed by value rank.
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.
-
Reverse PairsHardLeetCode 493
Counting pairs while sorting.
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. -
Repeated DNA SequencesMediumLeetCode 187
Fixed-length window hashing.
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).
-
Longest Happy PrefixHardLeetCode 1392
A direct test of the prefix function.
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.
-
Shortest PalindromeHardLeetCode 214
Using the prefix function to find the longest palindromic prefix.
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). -
Longest Duplicate SubstringHardLeetCode 1044
Binary search on a length, checked with hashing.
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 data | Hashing and counting |
| Sorted input, pairs with a target, or in-place edits | Two pointers |
| Longest or shortest contiguous run, with a condition that only breaks as it grows | Sliding 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 data | Binary search (on the answer) |
| Next or previous greater and smaller, spans, histograms | Monotonic stack |
| Ranges, meetings, overlaps | Intervals and sweep line |
| "Generate all" with n up to about 20 | Backtracking |
| Top k, a running minimum or maximum, merging sorted streams | Heap |
| Grid regions, reachability, fewest equal-cost moves | DFS or BFS |
| Dependencies or prerequisites | Topological sort |
| Groups that merge as connections arrive | Union-find |
| Cheapest or best path with weighted edges | Dijkstra and its variants |
| Count the ways, or the best value, with repeated subproblems | Dynamic programming |
| One local choice is provably safe after sorting | Greedy |
| Many words and prefix questions | Trie |
| "Every element appears twice except one", O(1) space | Bit manipulation |
The problems
Roughly easiest first. The order doesn't hint at the pattern.
-
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
-
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 × 2for each letter, then add 1 if any count is odd, since one letter can sit in the middle.Same idea as: Valid Anagram
-
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
-
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
-
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
-
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
-
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 × kexceeds the number of flowers.Same idea as: Koko Eating Bananas
-
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
-
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
-
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 × rightsubarrays. Use < on one side and ≤ on the other so equal values aren't double-counted.Same idea as: Largest Rectangle in Histogram
-
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
-
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
-
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
-
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
-
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
-
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
-
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 ofdp[i − L] + L × max(last L elements), updating the max as L grows.Same idea as: Word Break's "last piece" recurrence
-
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
-
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
-
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 withmax(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
| Resource | Size | Organised by | Strengths | Weaknesses |
|---|---|---|---|---|
| Blind 75 | 75 | Topic | The minimum viable set, and the ancestor of most later lists. | Thin on greedy, backtracking, and advanced graphs; one trie problem; no teaching. |
| Grind 75 | 75–169 | Weekly schedule | By 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 150 | 150 | 18 categories | Blind 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 250 | 250 | Categories | Described by NeetCode as its most beginner-friendly list. | More volume in the same categories rather than new patterns. |
| Sean Prashad's LeetCode Patterns | 179 | Pattern tags | Explicit pattern grouping, drawn from Grokking and Blind 75; company filters. | No order or explanation within a pattern. |
| Striver's A2Z | ~450 | 19 modules | Starts 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 Sheet | 191 | Topic | Interview-focused revision list. | Assumes the fundamentals; topic order rather than pattern order. |
| Grokking the Coding Interview | 215–300+ | 26–42 patterns | The 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 Set | 400 | Algorithm area | Rigorous 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
- 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.
- Free problems only. LeetCode Premium problems are replaced with free equivalents, or with the free NeetCode or LintCode version of the same problem.
- 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.
- DP split by the shape of its state (1-D, knapsack, grids and strings, advanced), not one 50-problem block.
- Marked as optional: segment trees, Fenwick trees, and string algorithms such as KMP. They're rare in interviews but build long-term strength.
- A final mixed set with no pattern labels, so the last step is recognising the pattern yourself.
- Python throughout, in LeetCode's function-signature style, using
collections,heapq,bisect, andfunctools.cache.
Sources
All seen through search excerpts; none were loaded in full.
- NeetCode 150 and NeetCode roadmap, neetcode.io
- Blind 75 vs NeetCode 150, LastRound AI (NeetCode's 18 categories, NeetCode 250 description)
- Blind 75 vs NeetCode 150, Crackr AI (overlap analysis)
- NeetCode 150 vs Blind 75, Codeintuition (gaps, overlap with the SDE Sheet)
- Grind 75 FAQ, Tech Interview Handbook
- LeetCode Patterns, Sean Prashad, and its GitHub repository
- Striver's A2Z DSA playlist, takeUforward
- Striver's A2Z tracker, crackalgo (SDE Sheet size)
- Grokking the Coding Interview Patterns and Design Gurus course page
- heapq documentation, Python (max-heap functions added in 3.14; used in Prerequisites)
- CSES Problem Set introduction and May 2025 update
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.