The Hard List
Every other list on this site teaches you the patterns. This one stress-tests them. It is hard-only and pattern-complete: 19 patterns, at least four hard problems each, chosen so that finishing a section means you have seen the ways that pattern gets bent in a real interview - not just the textbook form.
This is Uber-flavoured. Uber’s DSA rounds lean on graphs, shortest path, heaps, intervals, and a heavy dose of DP, and they expect runnable code against real test cases. So the DP section is the biggest one here, and the graph sections assume you can write Dijkstra and Kahn’s algorithm from memory without a reference.
Work one pattern at a time. Do not shuffle. The value of a hard set comes from solving five problems that fail in five different ways inside the same pattern - that is what builds the instinct to recognise the trap before you write code. Each section links back to its fundamentals deep dive if you need to reload the base technique first.
Which list should I use?
- 1 week of prep? → Quick-Fire 50
- 4 weeks of prep? → The Blind 75
- 8 weeks of prep? → NeetCode 150
- Already confident on the patterns? → You’re here. The Hard List.
💡 How to use: Give each problem 35 minutes. If you’re stuck, read the editorial, then close it and write the solution from scratch. A problem you read is not a problem you know.
A few problems appear under two patterns - Word Search II is both trie and backtracking, Count of Range Sum is both prefix sum and BIT. That repetition is deliberate: the second time you meet it, solve it with the other pattern’s lens. Repeats are noted in the table.
Two Pointers
Hard two-pointer problems stop being about “left and right walking inward” and start being about what invariant you can maintain in O(1) per step. The trigger is a sorted (or sortable) array plus a target relation, or a problem where the naive answer is O(n²) pair enumeration. At the hard level the pointers often live inside a merge sort, so the “two pointers” are the two halves of a divide-and-conquer, and the invariant is a count you accumulate while merging.
Trapping Rain Water - the invariant is the taller wall
L -> <- R
| leftMax = 2 rightMax = 3 |
# #
# . . . . . . . . . . . . . . . . . . . #
# ~ ~ # ~ ~ ~ # ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ # #
2 0 1 3 0 1 2 1 ...
move the pointer on the SHORTER side:
water at that index = min(leftMax, rightMax) - height[i]
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Trapping Rain Water | The canonical two-pointer invariant. Water above index i is bounded by the shorter of the two running maxes, so you can always safely advance the shorter side. | Solve → |
| 2 | Trapping Rain Water II | 1D intuition breaks completely in 2D - there is no “left and right”. Forces you to see it as a heap-driven boundary shrink instead. | Solve → |
| 3 | Reverse Pairs | The pointers are the two halves of a merge sort. Teaches counting inversions during the merge instead of comparing pairs. | Solve → |
| 4 | Count of Range Sum | Merge-sort two pointers over prefix sums, with a sliding lower and upper bound. Two moving windows inside one merge. | LeetCode → |
| 5 | Self Crossing | No data structure at all - just careful case analysis on the last few moves. Drills the discipline of enumerating states instead of simulating. | Solve → |
| 6 | 4Sum | Hard-Medium. The k-Sum generalisation. Gets you to write a recursive kSum that bottoms out in a two-pointer scan, with correct duplicate skipping at every level. | Solve → |
Deep dive: Two Pointers fundamentals →
Sliding Window
The easy version of this pattern is “expand right, shrink left while invalid”. Hard versions break one of the two halves. Either the window is not shrinkable in the obvious direction (the answer needs “exactly K”, which you get as atMost(K) - atMost(K-1)), or the value you want at the window boundary is not a count but a maximum, which needs a monotonic deque inside the window. The trigger is still “contiguous subarray or substring, optimise a length or a count”.
Exactly K distinct = atMost(K) - atMost(K - 1)
nums = 1 2 1 2 3 K = 2
atMost(2): [1 2] [1 2 1] [2 1 2] ... -> 7 subarrays
atMost(1): [1] [2] [1] [2] [3] ... -> 5 subarrays
exactly 2 = 7 - 5 = 2 windows too many to shrink directly
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Minimum Window Substring | Two-map validity check with a formed counter. Getting the shrink condition right without an O(52) rescan per step is the whole problem. |
Solve → |
| 2 | Sliding Window Maximum | Monotonic deque inside a window. The insight is that a smaller element behind a larger one is dead forever and can be dropped. | Solve → |
| 3 | Subarrays with K Different Integers | Not shrinkable directly. Teaches the atMost(K) - atMost(K-1) decomposition, which shows up in a dozen other counting problems. |
Solve → |
| 4 | Shortest Subarray with Sum at Least K | Negative numbers kill the monotonic window. Needs a monotonic deque over prefix sums, not over the array. | Solve → |
| 5 | Constrained Subsequence Sum | DP where the transition is a max over the last k states - a sliding window maximum embedded in a recurrence. | Solve → |
| 6 | Longest Substring with At Most K Distinct Characters | The clean template every other variant is built from. Solve it once, cleanly, and reuse it. | Solve → |
| 7 | Substring with Concatenation of All Words | Fixed-size window but the unit is a word, not a character. Forces you to run wordLen independent windows. |
LeetCode → |
Deep dive: Sliding Window fundamentals →
Binary Search (including Search on Answer)
At the hard level binary search is almost never over an array. It is over the answer space: you guess an answer, ask a monotone yes/no question about it, and halve. The trigger is “minimise the maximum” or “maximise the minimum” plus a feasibility check you can write in O(n). The hard part is never the search - it is proving the predicate is monotone and writing feasible(x) without an off-by-one.
Search on answer - the shape you are looking for
candidate: 1 2 3 4 5 6 7 8 9
feasible? N N N N Y Y Y Y Y
^
first Y = the answer
lo = min possible, hi = max possible
while lo < hi:
mid = lo + (hi - lo) // 2
if feasible(mid): hi = mid
else: lo = mid + 1
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Median of Two Sorted Arrays | Binary search on the partition point, not the value. The one problem where the index arithmetic genuinely has to be perfect. | Solve → |
| 2 | Split Array Largest Sum | The archetype of minimise-the-maximum. feasible(cap) is a greedy single pass. Once you see this, five other problems collapse into it. |
Solve → |
| 3 | Find K-th Smallest Pair Distance | The predicate needs its own two-pointer count of pairs below a distance - binary search wrapped around a sliding window. | Solve → |
| 4 | Super Egg Drop | Binary search inside a DP transition to turn O(n²k) into O(nk log n). Drills recognising monotonicity in a recurrence. | Solve → |
| 5 | Magnetic Force Between Two Balls | Maximise-the-minimum - the mirror image of Split Array. Confirms you can flip the predicate direction without confusion. | Solve → |
| 6 | Minimum Limit of Balls in a Bag | The feasibility check is a division-with-ceiling that most people get wrong on the first try. | Solve → |
| 7 | Capacity to Ship Packages Within D Days | Hard-Medium, and the cleanest possible warm-up for this whole section. | Solve → |
| 8 | Find in Mountain Array | Binary search to find the peak, then two more searches, all under a call budget. Composition of three searches. | LeetCode → |
Deep dive: Binary Search fundamentals →
Stacks & Queues / Monotonic Stack
A monotonic stack answers “for each element, what is the nearest element to the left or right that is larger or smaller”. Every hard problem here is that question in disguise, or it is a parser - nested expressions where the stack holds the suspended outer context. The trigger for the monotonic version is “previous or next greater or smaller”; for the parser version it is nesting of any kind: brackets, formulas, encoded strings.
Largest Rectangle in Histogram - pop when the bar shrinks
heights = 2 1 5 6 2 3
_
_ | |
| | | |
|5| |6| when we hit 2 < 6, pop 6:
_ | | | | width = i - stack[-1] - 1
|2| _ | | | | area = 6 * 1
| | |1| | | | |
---------------------
0 1 2 3 4 5
stack holds indices with STRICTLY INCREASING heights
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Largest Rectangle in Histogram | The parent of the whole family. Width on pop is i - stack.top - 1, and the sentinel trick at the end is what most people miss. |
Solve → |
| 2 | Maximal Rectangle | Reduces a 2D problem to n calls of the histogram solution by maintaining running column heights. Teaches reduction. | Solve → |
| 3 | Basic Calculator | Recursive-descent parsing with an explicit stack for sign and suspended totals. Nested parentheses and unary minus together. | Solve → |
| 4 | Number of Atoms | A real parser: nested groups, multipliers that apply to a whole sub-map, then sorted output. Closest thing on this list to production code. | Solve → |
| 5 | Longest Valid Parentheses | Stack of indices where the bottom is the last invalid position. Reframes “count valid” as “distance from the last break”. | Solve → |
| 6 | Max Stack | O(1) getMax with popMax. Forces a second structure and exposes the cost of the naive two-stack answer. | Solve → |
| 7 | Car Fleet | Monotonic stack on arrival time after sorting by position. A physical problem that looks nothing like next-greater until you see it. | Solve → |
| 8 | Remove Duplicate Letters | Monotonic stack plus a “can I still see this letter later” check. The greedy proof is the hard part, not the code. | Solve → |
Deep dive: Stacks & Queues fundamentals →
Linked Lists
Hard linked-list problems are pointer-discipline exams. There is rarely a clever insight; there is a lot of state you must keep straight while mutating in place, usually with a dummy head and a “tail of the previous group” pointer. The other half of this pattern is list plus map, where you glue a hash map onto a doubly linked list to get O(1) ordering - which is exactly how caches are built, and why Uber asks it.
flowchart LR
H["head sentinel"]:::client
A["node A"]:::service
B["node B"]:::service
C["node C"]:::service
T["tail sentinel"]:::client
M["hash map key to node"]:::data
H --> A
A --> B
B --> C
C --> T
M --> A
M --> C
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Merge K Sorted Lists | Heap of k heads, or pairwise divide and conquer. Compare the two and know why both are O(n log k). | Solve → |
| 2 | Reverse Nodes in k-Group | The pointer-surgery problem. You need group head, group tail, previous group tail, and next group head all correct, plus the leftover tail case. | Solve → |
| 3 | LRU Cache | Doubly linked list plus map, O(1) for both operations. Asked constantly at Uber. Write it with sentinels or you will drown in null checks. | Solve → |
| 4 | Sort List | Merge sort on a list in O(1) extra space. Splitting with fast and slow pointers and re-linking without recursion depth blowup. | Solve → |
| 5 | LFU Cache | LRU with a frequency dimension: map of frequency to its own linked list, plus a minFreq pointer. The natural follow-up interviewers use to separate candidates. | LeetCode → |
| 6 | Copy List with Random Pointer | O(1)-space clone by interleaving copies into the original list, then unweaving. Elegant and easy to get wrong. | LeetCode → |
Deep dive: Linked Lists fundamentals →
Trees & Traversal
Hard tree problems are about what you return up versus what you carry down. Almost all of them are one post-order DFS where each node returns a small tuple to its parent, and a global answer is updated on the way. The trigger phrase is “path”, “subtree”, or “any two nodes” - all three mean “compute something per subtree and combine at the node”. If you find yourself wanting a second traversal, you probably needed a bigger return tuple.
Return up vs carry down
(10) <- global best updated here
/ \
(2) (9)
/ \ / \
... ... ... ...
dfs(node) returns -> best downward path through node
global answer -> best path that BENDS at node
= node.val + max(left, 0) + max(right, 0)
never return the bent path to the parent - a path bends once
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Binary Tree Maximum Path Sum | The return-up versus update-global distinction, in its purest form. Negative subtrees must be clamped to zero. | Solve → |
| 2 | Maximum Sum BST in Binary Tree | Each node returns min, max, sum, and validity. Teaches designing the return tuple before writing the recursion. | Solve → |
| 3 | Binary Tree Cameras | Tree DP with three states per node and a greedy bottom-up placement. State design that is genuinely non-obvious. | Solve → |
| 4 | Number of Good Leaf Nodes Pairs | Each node returns a depth histogram of its leaves and pairs left against right. Small-to-large merging in miniature. | Solve → |
| 5 | House Robber III | Two-state tree DP - rob this node or don’t. The template for every “choose or skip on a tree” problem. | Solve → |
| 6 | Longest ZigZag Path in a Binary Tree | Carry direction down instead of returning it up. The mirror of the previous problems. | Solve → |
| 7 | Serialize and Deserialize Binary Tree | Design a format, then parse it back. Null markers and a shared cursor across recursive calls. | LeetCode → |
| 8 | Recover Binary Search Tree | Find two swapped nodes in one in-order pass with O(1) space. Case analysis on adjacent versus non-adjacent swaps. | LeetCode → |
Deep dive: Trees & Traversal fundamentals →
Graphs BFS/DFS
The hard version of graph traversal is rarely about the traversal. It is about defining the graph - what is a node, what is an edge - and about the state you must include in the visited key. If two paths reach the same cell with different remaining budget, the cell alone is not a state. The trigger is any problem about reachability, connectivity, or shortest number of steps on an unweighted structure, including implicit graphs where nodes are strings, board configurations, or bus stops.
flowchart TB
S["start state"]:::client
A["neighbour 1"]:::service
B["neighbour 2"]:::service
C["neighbour 3"]:::service
D["goal"]:::data
S --> A
S --> B
A --> C
B --> C
C --> D
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
BFS visits states in layers, so the first time you pop the goal you have the fewest steps. DFS commits to one branch and is the right choice when you want all paths, or a value defined recursively over a subtree.
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Word Ladder | An implicit graph where edges are one-character mutations. Generating neighbours efficiently is the entire problem. | Solve → |
| 2 | Longest Increasing Path in a Matrix | DFS plus memoisation on a DAG induced by the increasing condition. Shows why no visited set is needed here. | Solve → |
| 3 | Making a Large Island | Label components in one pass, store sizes, then evaluate each zero against its distinct neighbouring labels. Pre-computation over re-flooding. | Solve → |
| 4 | Shortest Distance from All Buildings | Multi-source BFS run once per building with a reachability counter. Teaches the “reachable from all sources” pruning. | Solve → |
| 5 | Cut Off Trees for Golf Event | Sort targets, then BFS between consecutive targets. Composing many shortest-path queries under a global order. | Solve → |
| 6 | Word Ladder II | Every shortest path, not just the length. BFS to build a parent DAG, then DFS to enumerate. The standard way to return all optima. | LeetCode → |
| 7 | Bus Routes | Nodes are routes, not stops. The whole difficulty is picking the right graph. | LeetCode → |
| 8 | Pacific Atlantic Water Flow | Reverse the flow and DFS inward from both borders, then intersect. Inverting the direction of the question. | LeetCode → |
Deep dive: Graphs BFS/DFS fundamentals →
Shortest Path (Dijkstra / Bellman-Ford)
The moment edges carry unequal weights, plain BFS is wrong and you need Dijkstra - a BFS whose queue is a min-heap keyed on total cost so far. Use Bellman-Ford instead when the constraint is on the number of edges (relax exactly k times) or when weights can be negative. The trigger words are “minimum cost”, “minimum effort”, “cheapest”, “earliest time you can”, with non-uniform step costs. A useful variant: when all weights are 0 or 1, a deque replaces the heap and you get 0-1 BFS in O(V+E).
flowchart LR
A["node A dist 0"]:::client
B["node B dist 2"]:::service
C["node C dist 5"]:::service
D["node D dist 6"]:::data
A -->|"w 2"| B
A -->|"w 5"| C
B -->|"w 4"| D
C -->|"w 1"| D
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
Pop the cheapest unsettled node, relax its edges, and never revisit a settled node. Here D settles at 6 via C, not 6 via B - and you only know that after C settles at 5.
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Network Delay Time | Dijkstra with nothing hidden. Write it once, correctly, with a stale-entry skip on pop - then reuse that shape everywhere below. | Solve → |
| 2 | Swim in Rising Water | The cost of a path is the maximum edge on it, not the sum. Dijkstra still works once you change the relaxation operator. | Solve → |
| 3 | Minimum Cost to Make at Least One Valid Path in a Grid | Edges cost 0 or 1, so a deque beats a heap. The 0-1 BFS pattern, which most candidates have never written. | Solve → |
| 4 | Jump Game IV | Unweighted BFS, but you must clear the value-to-indices bucket after visiting it or the complexity degrades to O(n²). | Solve → |
| 5 | Min Cost to Connect All Points | Minimum spanning tree, not shortest path - Prim’s is Dijkstra with a different relaxation. Knowing the difference is the point. | Solve → |
| 6 | Cheapest Flights Within K Stops | The edge-count constraint makes the state (node, stopsUsed). Bellman-Ford with k rounds, or Dijkstra over the expanded state space. |
LeetCode → |
| 7 | Path with Minimum Effort | Minimise the maximum difference. Solvable by Dijkstra, by union-find over sorted edges, or by binary search plus BFS - do all three. | LeetCode → |
| 8 | The Maze II | Movement is “roll until you hit a wall”, so edges are long and weighted. Redefining a step. | LeetCode → |
Deep dive: Shortest Path fundamentals →
Topological Sort
Any time the input is “A must come before B”, you are building a DAG and running Kahn’s algorithm: repeatedly remove a node with in-degree zero. If you cannot empty the graph, there is a cycle. Hard versions add a twist - detect ambiguity (more than one valid order), group nodes into super-nodes and sort at two levels, or return the number of rounds rather than the order. The trigger is dependency, prerequisite, ordering constraint, or “build order”.
Kahn's algorithm
edges: A->B A->C B->D C->D
in-degree: A:0 B:1 C:1 D:2
queue: [A] output: -
pop A -> B:0 C:0 output: A
queue: [B, C]
pop B -> D:1 output: A B
pop C -> D:0 output: A B C
pop D output: A B C D
queue size > 1 at any step => the order is not unique
output length < node count => there is a cycle
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Alien Dictionary | You must derive the edges from adjacent word pairs, handle the invalid prefix case, and detect cycles. Asked at Uber. | LeetCode → |
| 2 | Course Schedule II | Return the order, not just feasibility. The clean Kahn template every problem below extends. | Solve → |
| 3 | Sort Items by Groups Respecting Dependencies | Two-level topological sort: order the groups, then order within each group. The hardest state-modelling problem in this section. | LeetCode → |
| 4 | Sequence Reconstruction | Uniqueness checking - the queue must never hold two nodes at once. Turns topo sort into a verification problem. | LeetCode → |
| 5 | Parallel Courses | Count levels, not order. The answer is the depth of the DAG, which falls out of processing the queue layer by layer. | Solve → |
| 6 | Longest Cycle in a Graph | Functional graph where each node has out-degree one. Iterative traversal with visit timestamps instead of a topological order. | Solve → |
| 7 | Minimum Height Trees | Peel leaves layer by layer - Kahn’s algorithm on an undirected tree, stopping at the centroids. | Solve → |
| 8 | Find Champion II | Hard-Medium. Reduces to “exactly one node with in-degree zero”. A one-line answer once you see the framing. | Solve → |
Deep dive: Topological Sort fundamentals →
Heaps / Priority Queue
Reach for a heap when you need repeated access to the current extreme of a changing set. Hard problems here are usually one of three shapes: two heaps balanced against each other to track a median or a split point; sort by one dimension and heap on the other, which is how almost every “maximise a ratio or a product” problem is solved; or a heap used as the frontier of a lazy expansion. The trigger is “k-th”, “median”, “top k”, or “at any point, the best available”.
Two heaps keep a median at the boundary
max-heap (lower half) min-heap (upper half)
[ 1 3 5 ] | [ 7 9 11 ]
^ | ^
top | top
invariant: size(low) == size(high) or size(low) == size(high) + 1
median = low.top if sizes differ
= avg of both tops if sizes equal
every insert: push to one, then move one across to rebalance
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Find Median from Data Stream | The two-heap invariant, maintained under every insert. An Uber favourite because the follow-ups go straight into system design. | Solve → |
| 2 | Minimum Cost to Hire K Workers | Sort by wage-to-quality ratio, heap on quality. The canonical “sort one dimension, heap the other” problem. | Solve → |
| 3 | Minimize Deviation in Array | Normalise everything upward, then repeatedly halve the maximum. The stopping condition is the subtle part. | Solve → |
| 4 | Task Scheduler | Greedy with a cooldown queue alongside the heap, plus a closed-form answer you should also be able to derive. | Solve → |
| 5 | Ugly Number II | Heap as a lazy generator with deduplication - or three pointers, which is strictly better. Compare both. | Solve → |
| 6 | Smallest Range Covering Elements from K Lists | Heap over k list heads while tracking a running maximum. A k-way merge that computes a window, not a sequence. | LeetCode → |
| 7 | Maximum Performance of a Team | Sort by efficiency descending, min-heap of the k best speeds. Same shape as Hire K Workers - solve it right after, and feel the pattern lock in. | LeetCode → |
| 8 | IPO | Two heaps pulling in opposite directions: one gates by capital, one maximises profit. | LeetCode → |
Deep dive: Heaps fundamentals →
Dynamic Programming - 1D and Sequence
This is the biggest cluster at Uber, so it is split in two. A 1D DP is defined by one index and a small amount of carried state, and the work is entirely in naming the state precisely. If you cannot finish the sentence “dp[i] is the best answer for the prefix ending at i, given that …” then you have not found the state yet. The trigger is “count the ways”, “maximum or minimum over choices”, or an obvious exponential recursion with overlapping subproblems.
State design is the whole problem
Buy and Sell Stock IV - dp[t][i] = best profit using at most t
transactions through day i
hold[t] = max(hold[t], cash[t-1] - price) own a share
cash[t] = max(cash[t], hold[t] + price) own nothing
day: 1 2 3 4 5
price: 3 2 6 5 0
cash[1]: 0 0 4 4 4
cash[2]: 0 0 4 4 4
a wrong state (dp[i] = best profit through day i) cannot
express "how many transactions have I already spent"
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Best Time to Buy and Sell Stock III | Four explicit states in a fixed order. The gateway to the general k-transaction version. | Solve → |
| 2 | Best Time to Buy and Sell Stock IV | Generalises to k transactions, plus the k >= n/2 shortcut. Shows how to add a dimension to a working DP. | Solve → |
| 3 | Russian Doll Envelopes | Sort with a deliberate tie-break on width descending, then run patience LIS on heights. The tie-break is the trick. | Solve → |
| 4 | Frog Jump | The state is (index, lastJumpSize), not just index. The clearest lesson in “your state is too small”. |
Solve → |
| 5 | Constrained Subsequence Sum | Repeat from Sliding Window - solve it here as a DP whose transition is a range maximum, and see why the deque appears. | Solve → |
| 6 | Stone Game II | Game DP where the state includes how many piles the current player may take. Minimax expressed as a maximisation. | Solve → |
| 7 | Stone Game III | Same family, different move set, and the answer is a three-way comparison rather than a boolean. | Solve → |
| 8 | Predict the Winner | Hard-Medium. The relative-score formulation dp[i][j] = max(a[i] - dp[i+1][j], a[j] - dp[i][j-1]) that makes every game DP short. |
Solve → |
| 9 | Word Break II | DP for feasibility, memoised backtracking to enumerate. Teaches separating “can it be done” from “list all ways”. | LeetCode → |
| 10 | Decode Ways II | The same recurrence as Decode Ways with wildcards multiplying the transitions. Pure case-analysis endurance. | LeetCode → |
| 11 | Stone Game | The mathematical answer is one line, and interviewers ask you to prove it. Then they ask for the DP anyway. | Solve → |
Deep dive: Dynamic Programming fundamentals →
Dynamic Programming - 2D, Grid and Interval
Two indices means one of three sub-shapes. Two-sequence DP compares prefixes of two strings and fills a table row by row (edit distance, matching, subsequence counting). Grid DP walks a matrix, and the hard variants either walk it backwards, walk two paths simultaneously, or need an amortised transition instead of an O(n) inner scan. Interval DP is defined on a range [i..j] and splits on a middle element - the recurrence looks like dp[i][j] = best over k of dp[i][k] + dp[k][j] + cost, and it is the shape most people never get comfortable with.
Interval DP - iterate by LENGTH, split on the LAST balloon to pop
dp[i][j] = max points from the OPEN interval (i, j)
for len in 2 .. n:
for i in 0 .. n - len:
j = i + len
for k in i+1 .. j-1:
dp[i][j] = max(dp[i][j],
dp[i][k] + nums[i]*nums[k]*nums[j] + dp[k][j])
the reversal that makes it work: k is the LAST balloon popped,
so its neighbours are still i and j
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Edit Distance | The two-sequence table, and the one every string DP is measured against. Know the three transitions cold. | Solve → |
| 2 | Regular Expression Matching | Star handling needs a two-branch transition and the empty-pattern base cases. The hardest small DP on this list. | Solve → |
| 3 | Wildcard Matching | Simpler than regex but a good check that you can derive a transition instead of recalling it. Also has an O(1)-space greedy. | Solve → |
| 4 | Burst Balloons | Interval DP with the last-to-pop reversal. If you only do one interval DP, do this one. | Solve → |
| 5 | Minimum Cost to Merge Stones | Interval DP with an extra “number of piles” dimension and a k-step split. The advanced form of Burst Balloons. | Solve → |
| 6 | Count Different Palindromic Subsequences | Interval DP with per-character bookkeeping to avoid double counting. Inclusion-exclusion inside a DP. | Solve → |
| 7 | Interleaving String | Two pointers into two strings, one boolean table. Shows why greedy fails and DP does not. | Solve → |
| 8 | Distinct Subsequences | Counting rather than optimising, which changes the transition from max to sum. Easy to get an off-by-one. | Solve → |
| 9 | Scramble String | Memoised recursion over (i, j, len) with a split point. Looks like a string problem, behaves like an interval DP. |
Solve → |
| 10 | Dungeon Game | You must fill the grid backwards from the princess, because the constraint is on the minimum health along the path, not the total. | Solve → |
| 11 | Cherry Pickup II | Two robots walking simultaneously, so the state is (row, col1, col2). The standard follow-up after Minimum Path Sum. |
Solve → |
| 12 | Maximum Number of Points with Cost | The naive transition is O(cols) per cell; a left-and-right running maximum makes it O(1). Amortising a DP transition. | Solve → |
| 13 | Minimum Falling Path Sum II | Same amortisation idea via best and second-best per row. A short problem that proves you learned the previous one. | Solve → |
| 14 | Cherry Pickup | One trip out and back, reframed as two simultaneous downward paths. The hardest grid DP in common rotation. | Solve → |
| 15 | Minimum Path Sum | The base case for this section - start here, then immediately do Dungeon Game and Cherry Pickup II to see it deform. | Solve → |
| 16 | Maximal Square | DP on the matrix itself, min of three neighbours plus one. Compare with Maximal Rectangle in the stack section. |
Solve → |
Deep dive: Dynamic Programming fundamentals →
Backtracking
Backtracking is DFS over a decision tree with undo. The template is fixed - choose, recurse, un-choose - so all the difficulty lives in pruning and in avoiding duplicate branches. Two rules cover most of it: sort first so you can skip equal siblings, and prune the moment a partial candidate cannot possibly extend to a solution. The trigger is “all permutations”, “all combinations”, “all partitions”, or a constraint-satisfaction puzzle.
choose - recurse - un-choose
place(row):
if row == n: record solution; return
for col in 0 .. n-1:
if cols[col] or diag1[row+col] or diag2[row-col+n]:
continue <- prune
cols[col] = diag1[row+col] = diag2[row-col+n] = 1
place(row + 1)
cols[col] = diag1[row+col] = diag2[row-col+n] = 0
the three boolean arrays turn an O(n) conflict check into O(1)
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | N-Queens | O(1) conflict checks via column and diagonal sets, and clean state restoration. The reference implementation for this pattern. | Solve → |
| 2 | Palindrome Partitioning II | Backtracking is too slow, so this becomes a DP - a valuable lesson in when to abandon the pattern. | Solve → |
| 3 | Matchsticks to Square | Sort descending, prune on a full bucket, and skip equal-length buckets. Three prunings that turn TLE into instant. | Solve → |
| 4 | N-Queens II | Count instead of collect, which lets you use bitmasks for the three constraint sets. A fast follow-up. | Solve → |
| 5 | Word Search II | Repeat from Tries - the trie prunes the DFS. Without it, this times out. Backtracking guided by a data structure. | LeetCode → |
| 6 | Sudoku Solver | In-place mutation with nine-box index arithmetic. Correct undo under three simultaneous constraints. | LeetCode → |
| 7 | Expression Add Operators | Carry both the running value and the last operand so you can undo a multiplication. Also the leading-zero trap. | LeetCode → |
| 8 | Remove Invalid Parentheses | Compute the minimum removals first, then backtrack while skipping duplicate characters at the same position. Deduplication without a set. | LeetCode → |
Deep dive: Backtracking fundamentals →
Tries
A trie turns a set of strings into a tree of shared prefixes, which makes prefix queries O(length) instead of O(dictionary). Use one whenever the same string set is queried repeatedly, when you need “does any word start with”, or - the less obvious case - when you are searching bits rather than characters, which makes a binary trie the tool for maximum-XOR problems. The other high-value use is pruning: a trie lets a grid DFS abort the instant no word can continue.
flowchart TB
R["root"]:::client
C["c"]:::service
A["a"]:::service
T["t end of word cat"]:::data
R2["r"]:::service
T2["t end of word cart"]:::data
R --> C
C --> A
A --> T
A --> R2
R2 --> T2
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Implement Trie | The base you need before anything else here. Build it with an array of 26 children and an isEnd flag, not a map of maps. |
Solve → |
| 2 | Design Add and Search Words Data Structure | Wildcards force branching into every child, so the trie walk becomes a DFS. First real composition of trie and recursion. | Solve → |
| 3 | Word Search II | Trie plus grid backtracking, with node pruning after a word is found. The single most valuable trie problem to know. | LeetCode → |
| 4 | Maximum XOR of Two Numbers in an Array | A binary trie over 32 bits: walk greedily toward the opposite bit at every level. The non-string use of a trie. | Solve → |
| 5 | Concatenated Words | Trie plus DP over each word - can this word be split into other words in the set. Combines two patterns cleanly. | LeetCode → |
| 6 | Stream of Characters | Store words reversed, then query the suffix of the stream. Reversing the direction of the trie is the whole insight. | LeetCode → |
| 7 | Design Search Autocomplete System | Trie nodes carry top-k hot sentences. A design problem with a data-structure core - very close to what Uber asks in LLD rounds. | LeetCode → |
Deep dive: Tries fundamentals →
Union-Find
Union-find answers “are these two things in the same group” under a stream of merges, in effectively O(1) with path compression and union by size. Use it when merges only ever add connectivity - it cannot handle deletions. The trigger is connected components, “count groups”, cycle detection in an undirected graph, or anything processed in sorted edge order, which is why it competes with Dijkstra on threshold problems. The non-obvious skill is choosing what the elements are: sometimes they are rows and columns, or emails, not the objects in the problem statement.
Union by size plus path compression
find(x): if parent[x] != x: parent[x] = find(parent[x]) <- compress
return parent[x]
union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return False <- already connected = a cycle
if size[ra] < size[rb]: swap
parent[rb] = ra; size[ra] += size[rb]
return True
before compression after find(3)
0 <- 1 <- 2 <- 3 0 <- 1
0 <- 2
0 <- 3
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Graph Valid Tree | A tree is connected with exactly V-1 edges and no cycle. Union returning false is your cycle detector. | Solve → |
| 2 | Redundant Connection | The first union that fails is the answer. The simplest possible framing of cycle detection. | Solve → |
| 3 | Longest Consecutive Sequence | Union x with x+1, or solve it with a hash set. Two very different O(n) solutions worth comparing. |
Solve → |
| 4 | Count the Number of Complete Components | Track both node count and edge count per component and check e == v * (v-1) / 2. Carrying extra data in the DSU. |
Solve → |
| 5 | Redundant Connection II | Directed, so a node can have two parents and the graph can have a cycle. Genuine case analysis - the hardest DSU problem in rotation. | Solve → |
| 6 | Accounts Merge | The elements are emails, not accounts. Modelling the right universe is 80 percent of the work. | LeetCode → |
| 7 | Most Stones Removed with Same Row or Column | Union rows with columns in one shared namespace. The answer is n - components. A reframing that feels illegal until it clicks. |
Solve → |
| 8 | Number of Islands II | Incremental connectivity as land appears - exactly the case where union-find beats repeated BFS. | LeetCode → |
Deep dive: Union Find fundamentals →
Intervals / Greedy
Interval problems reduce to a sort plus a sweep, and the only real decision is what to sort by. Sort by start when you are merging or inserting. Sort by end when you are keeping a maximum non-overlapping set. Split each interval into a +1 start event and a -1 end event when you need the maximum concurrency at any moment. Greedy more generally needs an exchange argument: show that taking the locally best choice never makes the global answer worse. If you cannot argue that, it is a DP.
Sweep line - maximum concurrent intervals
intervals: [0,30] [5,10] [15,20]
events: 0:+1 5:+1 10:-1 15:+1 20:-1 30:-1
running: 1 2 1 2 1 0
^
peak = 2 rooms needed
sort starts and ends independently, then walk both
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Meeting Rooms II | The sweep-line or min-heap-of-end-times answer. The interval problem most likely to appear in an Uber phone screen. | Solve → |
| 2 | Candy | Two greedy passes, left to right then right to left, taking the max. Teaches that one pass cannot satisfy two-sided constraints. | Solve → |
| 3 | Minimum Number of Taps to Open to Water a Garden | Convert ranges to a jump-game and greedily extend the farthest reach. A reduction that makes a hard problem linear. | Solve → |
| 4 | Non-Overlapping Intervals | Sort by end - the one case where sorting by start gives the wrong answer. Worth doing just to feel that. | Solve → |
| 5 | Merge Intervals | The base case. Do it first, then use it as the inner step of harder problems. | Solve → |
| 6 | Partition Labels | Last-occurrence map plus a greedy extend. A sweep with no explicit intervals in the input. | Solve → |
| 7 | Employee Free Time | Merge k sorted interval lists, then invert to gaps. Composition of a heap merge and an interval sweep. | LeetCode → |
| 8 | The Skyline Problem | Sweep line with a multiset of active heights and lazy deletion. Also see the Advanced Structures section. | LeetCode → |
| 9 | Text Justification | No algorithm at all - just brutal specification handling. Uber-style: the test cases will find your off-by-one. | LeetCode → |
| 10 | Minimum Number of Arrows to Burst Balloons | Hard-Medium. Sort by end and greedily shoot at it. The same exchange argument as Non-Overlapping Intervals. | Solve → |
Deep dive: Greedy fundamentals →
Design / Data-Structure
These problems give you an API and a complexity target, and expect you to compose two structures so that each operation hits its bound. Hash map plus doubly linked list gives O(1) ordering. Hash map plus array gives O(1) random access with removal. Two heaps give a median. The interview signal here is not cleverness but interface discipline: decide your invariants before writing a line, then keep every operation honouring them. Uber weights this heavily because it sits between DSA and LLD.
Hash map plus array - O(1) insert, remove, getRandom
vals = [ a b c d ] idx = { a:0, b:1, c:2, d:3 }
remove(b):
swap b with the LAST element vals = [ a d c b ]
update idx[d] = 1 idx = { a:0, d:1, c:2 }
pop the tail vals = [ a d c ]
the swap-with-last trick is what makes removal O(1)
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | LRU Cache | Repeat from Linked Lists. The single most-asked design problem. Sentinel nodes, and a map holding node references rather than values. | Solve → |
| 2 | Find Median from Data Stream | Repeat from Heaps. Solve it here thinking about the API contract and what happens under an empty stream. | Solve → |
| 3 | Max Stack | getMax and popMax together break the simple two-stack answer. Forces a heap plus lazy deletion, or a doubly linked list. | Solve → |
| 4 | Time Based Key-Value Store | Map to a sorted list per key, then binary search on timestamp. The read-your-writes pattern from real systems. | Solve → |
| 5 | LFU Cache | Frequency buckets, each an ordered list, plus a minFreq pointer. Every operation must stay O(1) - the hardest invariant on this page. | LeetCode → |
| 6 | Insert Delete GetRandom O(1) | The swap-with-last trick. Asked in Uber’s machine-coding round, where they will also run your code. | LeetCode → |
| 7 | Design Twitter | Fan-out on read with a k-way heap merge of follower feeds. The bridge between DSA and system design. | LeetCode → |
| 8 | My Calendar III | Maintain maximum concurrent bookings under streaming inserts, with a sorted map of deltas. Sweep line as a live data structure. | LeetCode → |
Deep dive: DSA Fundamentals →
Bit Manipulation
Bit problems reward a small vocabulary used fluently: n & (n-1) clears the lowest set bit, n & -n isolates it, XOR cancels pairs, and a bitmask is a subset. Hard versions ask you to count bits per position across all numbers (which handles “every element appears three times”), to build an answer greedily from the high bit down, or to use a mask as a DP state. The trigger is a constant-space requirement on a counting problem, or an explicit ban on arithmetic operators.
Every number appears three times except one
position: 2 1 0
nums = 2 0 1 0
2 0 1 0
3 0 1 1
2 0 1 0
count of set bits per position: 0 4 1
take each count mod 3: 0 1 1 -> 3
the O(1)-space version keeps two masks, ones and twos,
which together form a mod-3 counter per bit
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Single Number II | The mod-3 bit counter. Do the per-position count first, then derive the two-mask version - that derivation is the interview answer. | Solve → |
| 2 | Single Number III | XOR everything, isolate any differing bit with x & -x, then partition. Two ideas composed. |
Solve → |
| 3 | Range Bitwise AND | The answer is the common prefix of the endpoints. Reasoning about what a range does to bits, not iterating it. | Solve → |
| 4 | Minimum Number of K Consecutive Bit Flips | Greedy with a difference array to track flip parity in O(1) per index. Bit thinking meets prefix sums. | Solve → |
| 5 | Sum of Two Integers | Add without +. carry = (a & b) << 1, sum = a ^ b, repeat - plus the signed-overflow masking that trips people up. |
Solve → |
| 6 | Maximum Product of Word Lengths | 26-bit masks for character sets, then pairwise AND to test disjointness. Bitmask as a set. | Solve → |
| 7 | Bulb Switcher | Pure number theory hiding behind a toggle simulation. The answer is a square root. | Solve → |
| 8 | Integer Replacement | Greedy on the low two bits, with the n = 3 exception. Small enough to reason about exhaustively, subtle enough to get wrong. | Solve → |
| 9 | Divide Two Integers | Long division by doubling the divisor, plus the INT_MIN overflow case that is the actual point of the question. | Solve → |
| 10 | Maximum XOR of Two Numbers in an Array | Repeat from Tries - here, build the answer bit by bit from the top using a prefix set instead of a trie. | Solve → |
Deep dive: Bit Manipulation fundamentals →
Prefix Sum / Index-as-Hash
Prefix sums turn any range-sum query into a subtraction, and combining them with a hash map turns “count subarrays with property P” into a single pass: for each prefix, ask how many earlier prefixes make the current one valid. Index-as-hash is the twin trick - when values are bounded by the array length, the array is your hash table, and you can encode presence by sign or by placement to hit O(1) space. The trigger for the first is subarray sum or count; for the second, “O(1) extra space” on a problem about values in 1..n.
Count subarrays with sum k, in one pass
nums = 1 2 3 k = 3
prefix: 0 1 3 6
seen: {0:1}
at 1: need 1-3 = -2 -> 0 seen = {0:1, 1:1}
at 3: need 3-3 = 0 -> 1 seen = {0:1, 1:1, 3:1}
at 6: need 6-3 = 3 -> 1 total = 2
seeding seen with {0: 1} is what makes prefixes
starting at index 0 count
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | First Missing Positive | O(1) space by placing each value v at index v-1. The cyclic-swap loop and its termination condition. |
Solve → |
| 2 | Subarray Sum Equals K | Hard-Medium, and the template for this whole section. Negative numbers are exactly why a sliding window will not work. | Solve → |
| 3 | Shortest Subarray with Sum at Least K | Repeat from Sliding Window - approach it here as a monotonic deque over the prefix array. | Solve → |
| 4 | Find the Duplicate Number | Index-as-hash pushed to its limit: the array is a functional graph, so Floyd’s cycle detection finds the duplicate in O(1) space. | Solve → |
| 5 | Count of Range Sum | Repeat from Two Pointers - prefix sums plus an order-statistic count. Solvable by merge sort or by a BIT. | LeetCode → |
| 6 | Contiguous Array | Map zero to -1 and the problem becomes “longest subarray with sum zero”. A transformation worth memorising. | Solve → |
| 7 | Maximum Size Subarray Sum Equals k | Store the first index per prefix rather than a count, because you want a length, not a total. A small but instructive change. | Solve → |
| 8 | Range Sum Query 2D - Immutable | Two-dimensional prefix sums and the inclusion-exclusion rectangle formula. The base for the mutable version below. | LeetCode → |
Deep dive: DSA Fundamentals →
Advanced Structures (Segment Tree / Fenwick BIT / Ordered Set)
Rarely asked at Uber - included for completeness, and because the pattern is worth recognising even if you never implement it under time pressure. Reach for a Fenwick tree or segment tree when you need both point updates and range queries in logarithmic time; a prefix-sum array gives you O(1) queries but O(n) updates, and a plain array gives the reverse. The other common use is counting inversions or order statistics on the fly, which usually needs coordinate compression first because values are large and sparse. Every problem below also has a merge-sort or heap solution - know that one first, and treat the BIT as the second answer you offer.
Fenwick tree - each index covers a power-of-two block
index: 1 2 3 4 5 6 7 8
covers: 1 1-2 3 1-4 5 5-6 7 1-8
update(i): while i <= n: tree[i] += v; i += i & -i
query(i): while i > 0 : s += tree[i]; i -= i & -i
range [l, r] = query(r) - query(l - 1)
both operations touch at most log n nodes
| # | Problem | Why it’s hard / what it drills | Link |
|---|---|---|---|
| 1 | Reverse Pairs | Repeat from Two Pointers - solve it here with a BIT over compressed values instead of a merge sort. Direct comparison of the two tools. | Solve → |
| 2 | Count of Smaller Numbers After Self | Sweep right to left, querying a BIT of seen values. The clearest order-statistic use of a Fenwick tree. | LeetCode → |
| 3 | Range Sum Query - Mutable | The reason BITs exist. Compare against the immutable prefix-sum version to see exactly what you are buying with log n updates. | LeetCode → |
| 4 | Count of Range Sum | Repeat again - prefix sums plus a BIT over compressed prefix values. Two bounds instead of one. | LeetCode → |
| 5 | My Calendar III | Repeat from Design. An ordered map of deltas is the practical answer; a segment tree with lazy propagation is the textbook one. | LeetCode → |
| 6 | The Skyline Problem | Repeat from Intervals. Needs a multiset or an ordered map with lazy deletion to track the current maximum height. | LeetCode → |
Deep dive: DSA Fundamentals →
How to know you’re done
For each pattern, you should be able to do three things without looking anything up: state the trigger that makes you reach for it, write the core template from memory, and name the one variant that breaks the template and what you do instead. If a section fails that test, the fix is not more problems from that section - it is re-solving the two hardest ones you already did, from a blank file.
For the all-difficulty lists, see Quick-Fire 50, The Blind 75, and NeetCode 150. For pattern fundamentals, read DSA Fundamentals. For company-specific sets, start with Uber Interview Prep.