FAANG Interview Prep
Foundations, Memory & Complexity
Big-O notation, time/space analysis, memory layoutRecursion Complete Guide
Base cases, call stack, tail recursion, memoizationArrays & Array ADT
Static/dynamic arrays, operations, amortized analysisStrings
Pattern matching, string algorithms, encoding, manipulationMatrices
2D arrays, sparse matrices, matrix operations, traversalsLinked Lists
Singly, doubly, circular lists, pointer manipulationStack
LIFO, push/pop, expression evaluation, backtrackingQueue
FIFO, circular queue, deque, priority queueTrees
Binary trees, traversals, expression trees, threaded treesBST & Balanced Trees
Search, insert, delete, AVL, red-black, B-treesHeaps, Sorting & Hashing
Min/max heaps, heapsort, hash tables, collision handlingGraphs, DP, Greedy & Backtracking
BFS, DFS, shortest paths, dynamic programming, optimizationGraph Fundamentals
A Graph is a collection of nodes (vertices) and edges connecting them. Graphs model relationships and networks, appearing frequently in interview problems.
Graph Terminology
- Directed: Edges have direction (A?B)
- Undirected: Edges go both ways (A—B)
- Weighted: Edges have costs/weights
- Cycle: Path that starts and ends at same node
- Connected: Path exists between all vertex pairs
Graph Representations
Choosing the right representation determines the efficiency of every graph algorithm. The Adjacency List maps each vertex to a list of its neighbors — space-efficient for sparse graphs (\(O(V + E)\) space) and fast neighbor iteration. It's the default choice for most interview and production scenarios.
The Adjacency Matrix stores a \(V \times V\) boolean/weight matrix where entry \([i][j]\) indicates an edge from \(i\) to \(j\). It enables \(O(1)\) edge existence checks and is ideal for dense graphs (where \(E \approx V^2\)) or algorithms like Floyd-Warshall that iterate over all pairs. The tradeoff is \(O(V^2)\) space regardless of actual edge count.
The Edge List is the simplest representation: just a flat list of \((u, v, weight)\) tuples. It's optimal for algorithms that iterate over all edges (Bellman-Ford, Kruskal's MST) and uses \(O(E)\) space. However, finding neighbors of a specific vertex requires \(O(E)\) scan, making it unsuitable for BFS/DFS.
BFS & DFS Traversal
flowchart TD
A["Graph Problem"] --> B{"What do you need?"}
B -->|"Shortest path
(unweighted)"| C["BFS"]
B -->|"Cycle detection
or exhaustive search"| D["DFS"]
B -->|"Shortest path
(weighted, no negatives)"| E["Dijkstra"]
B -->|"Shortest path
(negative edges)"| F["Bellman-Ford"]
B -->|"Connected
components"| G["BFS or DFS"]
B -->|"Topological
ordering"| H["DFS + finish time"]
C --> C1["Queue-based
O(V+E) time"]
D --> D1["Stack/Recursion
O(V+E) time"]
E --> E1["Priority Queue
O((V+E) log V)"]
F --> F1["Relax all edges V-1 times
O(V·E)"]
Interactive: BFS vs DFS Traversal
Step through both BFS and DFS on the same graph. Watch how the queue (BFS) explores level-by-level while the stack (DFS) dives deep before backtracking.
BFS vs DFS — Live Comparison
Click Next to step through BFS first, then DFS on the same graph.
Breadth-First Search (BFS)
BFS explores a graph level by level, visiting all neighbors at distance 1 before any at distance 2, and so on. It uses a queue (FIFO) to maintain the frontier. BFS naturally finds the shortest path in unweighted graphs because it discovers nodes in order of increasing distance from the source. Time: \(O(V + E)\), Space: \(O(V)\) for the visited set and queue.
When to use BFS: shortest path in unweighted graphs, level-order traversal, finding connected components, testing bipartiteness, or any problem where you need to explore "closest first" (like word ladder, minimum mutations, or maze shortest path).
Depth-First Search (DFS)
DFS explores as deep as possible along each branch before backtracking. It uses a stack (explicit or via recursion's call stack). DFS is the natural choice for problems requiring exhaustive exploration: detecting cycles, topological sorting, finding connected/strongly-connected components, and solving puzzles (mazes, Sudoku). Time: \(O(V + E)\), Space: \(O(V)\) for the visited set plus \(O(h)\) stack depth.
Recursive vs Iterative: Recursive DFS is cleaner for tree-like structures, but risks stack overflow on deep graphs (100K+ nodes). Iterative DFS with an explicit stack handles arbitrary depth safely.
Number of Islands (Classic Graph Problem)
Given a 2D grid of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and formed by connecting adjacent lands horizontally or vertically. This is the canonical flood-fill / connected components problem: scan the grid; when you hit unvisited land, start a DFS/BFS to mark the entire island as visited, then increment the count. Each cell is visited at most once, giving \(O(m \times n)\) time.
Shortest Path Algorithms
flowchart TD
A["Shortest Path Problem"] --> B{"Negative
Weights?"}
B -->|No| C{"Single Source?"}
B -->|Yes| D{"Negative
Cycles?"}
C -->|Yes| E{"Weighted?"}
C -->|No| F["Floyd-Warshall
O V cubed"]
E -->|Yes| G["Dijkstra
O V+E log V"]
E -->|No| H["BFS
O V+E"]
D -->|No| I["Bellman-Ford
O VE"]
D -->|Yes| J["No Solution
Detect and Report"]
style G fill:#e8f4f4,stroke:#3B9797
style H fill:#e8f4f4,stroke:#3B9797
style I fill:#f0f4f8,stroke:#16476A
style J fill:#fff5f5,stroke:#BF092F
Dijkstra's Algorithm
Dijkstra's algorithm finds the shortest path from a single source vertex to all other vertices in a graph with non-negative edge weights. It works on a greedy principle: always process the unvisited vertex with the smallest known distance, then update its neighbors. This greedy choice is safe because non-negative weights guarantee that once a vertex is finalized (popped from the priority queue), no shorter path to it can exist.
Key Insight
Think of Dijkstra as a "wavefront" expanding outward from the source — it always expands to the nearest unvisited node first, like ripples on a pond. The priority queue (min-heap) efficiently selects which node to expand next. Time complexity: \(O((V + E) \log V)\) with a binary heap.
Algorithm steps: (1) Initialize all distances to ∞ except source = 0. (2) Push source into a min-heap. (3) Pop the minimum-distance vertex, relax all its edges (if \(dist[u] + weight(u,v) < dist[v]\), update \(dist[v]\)). (4) Repeat until the heap is empty. The relaxation step is the core — it asks "can we reach \(v\) faster by going through \(u\)?"
Bellman-Ford Algorithm
Bellman-Ford solves the single-source shortest path problem even when edges have negative weights — something Dijkstra cannot handle. The tradeoff is speed: \(O(V \times E)\) versus Dijkstra's \(O((V+E) \log V)\). Its key advantage is the ability to detect negative-weight cycles — loops where total weight is negative, making "shortest path" undefined (you could loop forever getting shorter).
How it works: Relax every edge in the graph \(V-1\) times. After \(k\) iterations, the algorithm guarantees correct shortest distances for all paths using at most \(k\) edges. Since the longest simple path has at most \(V-1\) edges, \(V-1\) iterations suffice. If a \(V\)-th iteration still reduces a distance, a negative cycle exists.
When to Choose Bellman-Ford over Dijkstra
Use Bellman-Ford when: (1) edges can have negative weights (e.g., currency arbitrage, network routing with costs/rebates), (2) you need to detect negative cycles, or (3) the graph is given as an edge list (Bellman-Ford iterates over edges directly). For all other cases, prefer Dijkstra for speed.
Topological Sort
A topological ordering of a Directed Acyclic Graph (DAG) arranges vertices so that for every directed edge \(u \to v\), vertex \(u\) appears before \(v\). Think of it as a valid execution order — like determining which courses to take first given prerequisites, or which build targets to compile before others.
Two classic approaches exist: Kahn's Algorithm (BFS-based) repeatedly removes vertices with zero in-degree, while the DFS-based approach adds vertices to the result in reverse post-order. Both run in \(O(V + E)\). A topological sort exists if and only if the graph has no cycles — if Kahn's algorithm processes fewer than \(V\) vertices, a cycle exists.
Real-World Applications
Build systems (Makefile dependencies), package managers (npm/pip install order), spreadsheet cell evaluation (formulas depend on other cells), course scheduling (prerequisites), and task pipelines (CI/CD stages). Any problem with "do X before Y" constraints maps to topological sort.
Dynamic Programming Introduction
Dynamic Programming (DP) solves problems by breaking them into overlapping subproblems, storing solutions to avoid recomputation. It's essential for optimization problems.
DP Characteristics
- Optimal Substructure: Optimal solution contains optimal solutions to subproblems
- Overlapping Subproblems: Same subproblems solved multiple times
- Memoization: Top-down approach with caching
- Tabulation: Bottom-up approach building solution iteratively
flowchart TD
A["Optimization Problem"] --> B{"Overlapping
subproblems?"}
B -->|"Yes"| C{"Optimal
substructure?"}
B -->|"No"| D{"Greedy choice
property?"}
C -->|"Yes"| E["Dynamic Programming"]
C -->|"No"| F["Backtracking / Brute Force"]
D -->|"Yes"| G["Greedy Algorithm"]
D -->|"No"| F
E --> E1["Top-down: Memoization"]
E --> E2["Bottom-up: Tabulation"]
G --> G1["Make locally optimal
choice at each step"]
F --> F1["Explore all possibilities
with pruning"]
Fibonacci - DP Example
Common DP Patterns
DP problems fall into recognizable families based on the shape of their state space. 1D DP uses a single variable (array index, amount remaining, step count). 2D DP uses two variables (two string indices, grid coordinates, item × capacity). Recognizing the pattern lets you define the recurrence immediately.
1D DP: Climbing Stairs
You can climb 1 or 2 steps at a time. How many distinct ways to reach step \(n\)? This is structurally identical to Fibonacci: \(dp[i] = dp[i-1] + dp[i-2]\). The "number of ways" at each step is the sum of ways to reach it from one step below (1 step) and two steps below (2 steps). State: \(dp[i]\) = ways to reach step \(i\). Transition: \(dp[i] = dp[i-1] + dp[i-2]\). Base: \(dp[0]=1, dp[1]=1\).
1D DP: House Robber
Given an array of house values where adjacent houses cannot both be robbed (they trigger an alarm), maximize total loot. This is the classic "include/exclude" DP pattern: at each house \(i\), you either rob it (adding its value to the best from \(i-2\)) or skip it (carrying forward the best from \(i-1\)). State: \(dp[i]\) = max loot from houses \(0..i\). Transition: \(dp[i] = \max(dp[i-1],\; dp[i-2] + nums[i])\). This pattern appears in many variants: circular houses (LeetCode 213), binary tree robber (LeetCode 337).
2D DP: Longest Common Subsequence
Given two strings, find the length of their longest common subsequence (characters in order but not necessarily contiguous). This is the gateway 2D DP problem — state is defined by two pointers into the two strings. If characters match, both advance; otherwise, try advancing each pointer separately and take the maximum. State: \(dp[i][j]\) = LCS length of \(text1[0..i-1]\) and \(text2[0..j-1]\). Transition: if \(text1[i-1] = text2[j-1]\), then \(dp[i][j] = dp[i-1][j-1] + 1\); else \(dp[i][j] = \max(dp[i-1][j], dp[i][j-1])\).
This pattern generalizes to Edit Distance (add insert/delete/replace costs), Shortest Common Supersequence, and many string-comparison problems.
2D DP: Coin Change
Given coin denominations and a target amount, find the minimum number of coins to make that amount. This is an unbounded knapsack variant — each coin can be used unlimited times. Build up solutions from amount 0 to target: for each amount \(a\), try every coin that fits and take the minimum. State: \(dp[a]\) = fewest coins to make amount \(a\). Transition: \(dp[a] = \min(dp[a-coin] + 1)\) for each \(coin \le a\). Unlike 0/1 Knapsack where items are used once, here we iterate over coins in the inner loop because reuse is allowed.
Knapsack Problem
The 0/1 Knapsack is the quintessential DP problem: given \(n\) items each with a weight and value, maximize total value without exceeding a weight capacity. "0/1" means each item is either taken or left — no fractions. State: \(dp[w]\) = max value achievable with capacity \(w\). The key insight is traversing the capacity backwards in the 1D optimization — this prevents using the same item twice. The Unbounded Knapsack variant (items reusable) traverses forwards instead.
Knapsack Family
Many problems reduce to knapsack: Subset Sum (can you make target?), Partition Equal Subset Sum (split into two equal halves), Coin Change (unbounded knapsack with min count), and Target Sum (assign +/- signs). The 0/1 vs unbounded distinction determines inner loop direction.
Greedy Algorithms
Greedy algorithms make locally optimal choices at each step, hoping to find a global optimum. They work when the problem has the greedy-choice property (a local optimum leads to a global optimum) and optimal substructure (an optimal solution contains optimal solutions to subproblems). Unlike DP which considers all possibilities, greedy commits to a choice without looking back — making it faster (\(O(n \log n)\) typical) but applicable to fewer problems.
Greedy vs DP: How to Decide
If the problem says "maximize/minimize" and you can prove that choosing the best option now never hurts the future — use greedy. If choices now constrain future options in non-obvious ways (e.g., taking a lighter item now might block a more valuable combination later) — you need DP. Activity Selection and Huffman Encoding are provably greedy; 0/1 Knapsack is not.
Activity Selection
Given a set of activities with start and end times, select the maximum number of non-overlapping activities. The greedy strategy is deceptively simple: sort by end time, then always pick the activity that finishes earliest and doesn't overlap with the last selected. This works because finishing early leaves the most room for future activities — a provably optimal greedy choice. Time: \(O(n \log n)\) for sorting.
Jump Game
Given an array where each element represents the maximum jump length from that position, determine (a) if you can reach the last index, and (b) the minimum number of jumps. The greedy insight: maintain the farthest reachable index as you scan left-to-right. If your current position ever exceeds the farthest reach, you're stuck. For minimum jumps, use a "BFS-like" greedy: track the current reachable range (a "level"), and when you exhaust it, jump to the new farthest point.
Interval Scheduling
A family of problems involving overlapping intervals: merge overlapping intervals, find minimum meeting rooms, or insert a new interval. The common preprocessing step is sorting by start time (or end time, depending on the variant). For merging: sort by start, then extend the current interval's end if the next overlaps; otherwise close and start a new interval. For meeting rooms: count the maximum overlap at any point (equivalent to a sweep line or min-heap approach).
Backtracking
Backtracking is a systematic way to explore all possible solutions by building candidates incrementally and abandoning ("backtracking") candidates that fail to satisfy constraints.
Backtracking Template
def backtrack(candidate):
if is_solution(candidate):
output(candidate)
return
for choice in choices:
if is_valid(choice):
make_choice(choice)
backtrack(candidate)
undo_choice(choice) # Backtrack
Permutations
Generate all possible orderings of a given set. At each position, try every unused element, recurse to fill the next position, then undo the choice (backtrack). The decision tree has \(n!\) leaves (one per permutation). Key constraint: use a "used" boolean array to avoid placing the same element twice. For duplicates, sort first and skip adjacent identical unused elements to avoid duplicate permutations.
Subsets
Generate all \(2^n\) subsets (the power set) of a given array. At each element, make a binary choice: include it or exclude it. The recursion tree branches two ways at each level, giving \(2^n\) total subsets. Implementation uses an index \(i\) to avoid revisiting earlier elements (since subset order doesn't matter). This is the foundation for many search problems — Subsets II (with duplicates) adds a skip condition for consecutive identical values.
Combination Sum
Find all unique combinations that sum to a target. Unlike subsets (which enumerate all), here we prune branches where the running sum exceeds the target — this is backtracking's power over brute force. The key decision at each step: which candidate to add next (starting from index \(i\) to avoid reusing earlier positions). In the "reuse allowed" variant, recurse with the same index \(i\) (not \(i+1\)); in "use once" variants, advance to \(i+1\).
N-Queens
Place \(N\) queens on an \(N \times N\) chessboard so no two queens attack each other (no shared row, column, or diagonal). This is the classic constraint-satisfaction backtracking problem. Place queens row by row; for each row, try each column and check if it's safe (no queen in the same column, or either diagonal). Use sets or boolean arrays to track occupied columns and diagonals. The diagonal trick: cells on the same diagonal have equal \(row - col\) (main) or \(row + col\) (anti-diagonal).
LeetCode Practice Problems
Essential Problems
| # | Problem | Difficulty | Key Concept |
|---|---|---|---|
| 200 | Number of Islands | Medium | Graph DFS/BFS |
| 207 | Course Schedule | Medium | Topological Sort |
| 743 | Network Delay Time | Medium | Dijkstra |
| 70 | Climbing Stairs | Easy | 1D DP |
| 198 | House Robber | Medium | 1D DP |
| 322 | Coin Change | Medium | DP - Unbounded Knapsack |
| 1143 | Longest Common Subsequence | Medium | 2D DP |
| 55 | Jump Game | Medium | Greedy |
| 46 | Permutations | Medium | Backtracking |
| 51 | N-Queens | Hard | Backtracking |
Complete DSA Series
FAANG Interview Preparation - Series Complete! ??
- Stack & Applications
- Queue & Variants
- Trees & Traversals
- BST & Balanced Trees
- Heaps, Sorting & Hashing
- Graphs, DP, Greedy & Backtracking (You are here)
Quick Check — Test Yourself
- Given graph:
A-B, A-C, B-D, C-D, D-E. Find the shortest path A→E using Dijkstra. Show the priority queue state at each step. - Define “optimal substructure” and “overlapping subproblems”. Give one problem that has both, one that has only optimal substructure.
- Solve coin change for coins=[1,5,11] and amount=15. Is greedy optimal here? Why or why not?
Common Bugs
- Not settling nodes in Dijkstra: When popping, check
if dist > distances[node]: continue. Without this, stale heap entries cause wrong results. - DP array size off by one: For coin change up to amount n, allocate
dp[n+1]. Missing the +1 causes an out-of-bounds on the target value. - Confusing greedy with DP: Greedy works when local optimal = global optimal. Non-canonical coin systems break greedy but DP handles them correctly.
Interview Lens
For graph problems, always clarify: directed or undirected? Weighted? Can it have cycles? Negative weights? Each combination unlocks a different algorithm. For DP: define dp[i] meaning precisely, write the recurrence, handle base cases, then decide top-down vs bottom-up.
Production Lens
Google Maps uses A* (Dijkstra + heuristic). Social networks use BFS for degree-of-separation. Backtracking powers SAT solvers and scheduling. DP appears in bioinformatics (sequence alignment), financial option pricing (binomial models), and compiler optimisation (instruction selection).