Hãy tưởng tượng bạn đang khám phá một thành phố lạ:
Nodes = các địa điểm (nhà hàng, trường học, công viên)
Edges = các con đường nối giữa chúng
Directed edge = đường một chiều
Undirected edge = đường hai chiều
DFS = Explorer tham vọng
Chọn một con phố và đi thẳng, không quay đầu. Khi gặp ngõ cụt, mới backtrack và thử con đường khác. Có thể đi rất xa trước khi khám phá được khu vực lân cận.
BFS = Sóng nước lan ra từ điểm ném đá
Từ điểm xuất phát, sóng lan ra theo vòng tròn đồng tâm — tất cả địa điểm cách 1 bước được thăm trước, rồi 2 bước, rồi 3 bước. Đây là lý do BFS đảm bảo tìm đường ngắn nhất trong unweighted graph.
Graph vs Tree
Tree là một trường hợp đặc biệt của graph: directed, acyclic, connected, có đúng 1 root. Graph tổng quát hơn: có thể có cycles, disconnected components, undirected edges.
Context thực tế:
Social network: Facebook friends graph, Twitter follower graph
Navigation: Google Maps tìm đường ngắn nhất (Dijkstra’s = BFS với weights)
Web crawling: Google spider crawls URLs — BFS from seed URLs
Recommendation systems: “Friends of friends” suggestions
2. The FAANG Standard
Graph = Khả năng model hóa bài toán thực tế
FAANG interviewer không chỉ test algorithm — họ test khả năng nhận ra rằng bài toán là một graph problem và model nó đúng cách.
Những gì FAANG interviewer mong đợi:
Kỹ năng
Mức độ quan trọng
Ghi chú
DFS/BFS traversal
⭐⭐⭐⭐⭐
Cả iterative và recursive
Grid as implicit graph
⭐⭐⭐⭐⭐
Number of Islands pattern
Cycle detection
⭐⭐⭐⭐⭐
Directed graph — topological sort
Topological Sort
⭐⭐⭐⭐⭐
Course Schedule — classic L5
Shortest path unweighted
⭐⭐⭐⭐
BFS guarantees shortest
Connected components
⭐⭐⭐⭐
Union-Find hoặc DFS
Bipartite graph check
⭐⭐⭐
Graph coloring
Dijkstra’s (weighted)
⭐⭐⭐
L5/L6, dùng priority queue
L4 expectation: BFS/DFS, number of islands, cycle detection trong 25 phút.
L5 expectation: Topological sort, shortest path, nhận biết ẩn graph từ bài toán không rõ ràng là graph.
3. Visual Thinking (Mermaid)
3.1 Adjacency List vs Adjacency Matrix
graph LR
subgraph List["Adjacency List — Recommended"]
direction TB
L0["0: [1, 2]"]
L1["1: [0, 3]"]
L2["2: [0, 4]"]
L3["3: [1]"]
L4["4: [2]"]
end
subgraph Graph["Actual Graph"]
direction TB
N0((0)) --- N1((1))
N0((0)) --- N2((2))
N1((1)) --- N3((3))
N2((2)) --- N4((4))
end
subgraph Matrix["Adjacency Matrix — Dense graphs only"]
direction TB
M[" 0 1 2 3 4<br/>0[0 1 1 0 0]<br/>1[1 0 0 1 0]<br/>2[1 0 0 0 1]<br/>3[0 1 0 0 0]<br/>4[0 0 1 0 0]"]
end
3.2 BFS Shortest Path với khoảng cách
flowchart TD
subgraph BFS_Levels["BFS từ Node 0 — khoảng cách"]
A["Node 0 — dist: 0"]
B["Node 1 — dist: 1"]
C["Node 2 — dist: 1"]
D["Node 3 — dist: 2"]
E["Node 4 — dist: 2"]
F["Node 5 — dist: 3"]
A --> B
A --> C
B --> D
C --> E
D --> F
end
subgraph Queue_State["Queue states"]
Q0["Init: [0]"]
Q1["After 0: [1, 2]"]
Q2["After 1,2: [3, 4]"]
Q3["After 3,4: [5]"]
end
3.3 Topological Sort — DAG với dependencies
graph LR
A["Course A<br/>(no prereq)"]
B["Course B<br/>(needs A)"]
C["Course C<br/>(needs A)"]
D["Course D<br/>(needs B, C)"]
E["Course E<br/>(needs D)"]
A --> B
A --> C
B --> D
C --> D
D --> E
style A fill:#27ae60,color:#fff
style E fill:#e74c3c,color:#fff
Topological order: A → B → C → D → E (hoặc A → C → B → D → E)
Nếu có cycle trong graph → topological sort không tồn tại
4. TypeScript Template
4.1 Graph Representation — Adjacency List
// Undirected graphfunction buildUndirectedGraph(n: number, edges: number[][]): Map<number, number[]> { const graph = new Map<number, number[]>(); for (let i = 0; i < n; i++) { graph.set(i, []); } for (const [u, v] of edges) { graph.get(u)!.push(v); graph.get(v)!.push(u); // Undirected: cả hai chiều } return graph;}// Directed graphfunction buildDirectedGraph(n: number, edges: number[][]): Map<number, number[]> { const graph = new Map<number, number[]>(); for (let i = 0; i < n; i++) { graph.set(i, []); } for (const [u, v] of edges) { graph.get(u)!.push(v); // Chỉ một chiều } return graph;}
4.2 DFS — Iterative (Explicit Stack) và Recursive
// DFS Recursive — Clean và dễ hiểufunction dfsRecursive( graph: Map<number, number[]>, start: number): number[] { const visited = new Set<number>(); const result: number[] = []; function dfs(node: number): void { visited.add(node); result.push(node); for (const neighbor of graph.get(node) || []) { if (!visited.has(neighbor)) { dfs(neighbor); } } } dfs(start); return result;}// DFS Iterative — Dùng khi graph quá sâu (stack overflow risk với recursive)function dfsIterative( graph: Map<number, number[]>, start: number): number[] { const visited = new Set<number>(); const stack: number[] = [start]; const result: number[] = []; while (stack.length > 0) { const node = stack.pop()!; if (visited.has(node)) continue; // Skip nếu đã visited visited.add(node); result.push(node); for (const neighbor of graph.get(node) || []) { if (!visited.has(neighbor)) { stack.push(neighbor); } } } return result;}
Recursive vs Iterative DFS
Recursive: cleaner code, nhưng risk stack overflow với graph rất sâu (>10,000 nodes)
Iterative: dùng explicit stack trên heap → không bị stack overflow
Trong interview, recursive thường được chấp nhận trừ khi đề bài cảnh báo về input size lớn
4.3 BFS — Shortest Path trong Unweighted Graph
function bfsShortestPath( graph: Map<number, number[]>, start: number, end: number): number { if (start === end) return 0; const visited = new Set<number>([start]); const queue: number[] = [start]; let head = 0; // Index pointer — O(1) dequeue (xem 03-Stack-and-Queue § Pitfall 2) let distance = 0; while (head < queue.length) { const levelSize = queue.length - head; distance++; for (let i = 0; i < levelSize; i++) { const node = queue[head++]; for (const neighbor of graph.get(node) || []) { if (neighbor === end) return distance; if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); } } } } return -1; // Không tìm thấy đường}
Tại sao BFS đảm bảo shortest path?
BFS khám phá theo vòng tròn đồng tâm từ source. Node nào được thăm đầu tiên, đó là đường ngắn nhất. Với unweighted graph, mỗi edge có weight = 1, nên “số bước” = “tổng weight”.
4.4 numIslands — Grid as Implicit Graph
// Grid = implicit graph: mỗi cell là 1 node, 4 neighbors là các edgefunction numIslands(grid: string[][]): number { if (!grid || grid.length === 0) return 0; const rows = grid.length; const cols = grid[0].length; let count = 0; // DFS để "sink" một island (mark visited bằng cách đổi '1' → '0') function dfs(r: number, c: number): void { // Boundary checks và water/visited check if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== '1') { return; } grid[r][c] = '0'; // Mark as visited (sink the island) dfs(r + 1, c); // Down dfs(r - 1, c); // Up dfs(r, c + 1); // Right dfs(r, c - 1); // Left } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === '1') { count++; dfs(r, c); // Sink entire island } } } return count;}// ===== COMPLEXITY =====// Time: O(M * N) — visit each cell at most once// Space: O(M * N) — recursion stack in worst case (all land)
Grid Pattern — Rất phổ biến trong FAANG
Nhiều bài “không rõ ràng là graph” thực ra là grid problems: Flood Fill, Walls and Gates, Rotten Oranges, Pacific Atlantic Water Flow. Pattern chung: 4-directional DFS/BFS, check bounds trước khi access.
4.5 canFinish — Cycle Detection cho Topological Sort
// LeetCode #207: Course Schedule// Có thể hoàn thành tất cả n courses không? (có prerequisite)// Tương đương: graph có cycle không?function canFinish(numCourses: number, prerequisites: number[][]): boolean { // Build adjacency list const graph = new Map<number, number[]>(); for (let i = 0; i < numCourses; i++) graph.set(i, []); for (const [course, prereq] of prerequisites) { graph.get(prereq)!.push(course); } // 0 = unvisited, 1 = visiting (in current DFS path), 2 = visited (done) const state = new Array(numCourses).fill(0); function hasCycle(node: number): boolean { if (state[node] === 1) return true; // Cycle detected! if (state[node] === 2) return false; // Already fully processed state[node] = 1; // Mark as "being visited" for (const neighbor of graph.get(node) || []) { if (hasCycle(neighbor)) return true; } state[node] = 2; // Mark as "fully visited" return false; } // Check all nodes (graph có thể disconnected) for (let i = 0; i < numCourses; i++) { if (hasCycle(i)) return false; } return true;}
3 states trong cycle detection — không được dùng boolean visited
0 = chưa thăm
1 = đang thăm (trong DFS call stack hiện tại) — nếu gặp lại node này → cycle!
2 = đã thăm xong (toàn bộ subtree đã được explore)
Nếu chỉ dùng boolean visited, sẽ không phân biệt được “đang trong DFS path” vs “đã xử lý xong” → false positive cycles.
// Kahn's Algorithm: BFS, dùng in-degree// Thích hợp hơn DFS-based vì dễ detect cycle và code cleanerfunction topologicalSort(numCourses: number, prerequisites: number[][]): number[] { const graph = new Map<number, number[]>(); const inDegree = new Array(numCourses).fill(0); for (let i = 0; i < numCourses; i++) graph.set(i, []); for (const [course, prereq] of prerequisites) { graph.get(prereq)!.push(course); inDegree[course]++; } // Start with nodes that have no prerequisites (in-degree = 0) const queue: number[] = []; for (let i = 0; i < numCourses; i++) { if (inDegree[i] === 0) queue.push(i); } const order: number[] = []; let head = 0; // Index pointer (O(1) dequeue) while (head < queue.length) { const node = queue[head++]; order.push(node); for (const neighbor of graph.get(node) || []) { inDegree[neighbor]--; if (inDegree[neighbor] === 0) { queue.push(neighbor); // Tất cả prerequisites đã được hoàn thành } } } // Nếu order.length < numCourses → có cycle → không thể sort return order.length === numCourses ? order : [];}// ===== COMPLEXITY =====// Time: O(V + E) — process each node and edge once// Space: O(V + E) — graph + queue + inDegree array
4.7 cloneGraph
class GraphNode { val: number; neighbors: GraphNode[]; constructor(val: number = 0, neighbors: GraphNode[] = []) { this.val = val; this.neighbors = neighbors; }}function cloneGraph(node: GraphNode | null): GraphNode | null { if (!node) return null; // Map từ original node → cloned node const cloned = new Map<GraphNode, GraphNode>(); function dfs(original: GraphNode): GraphNode { if (cloned.has(original)) return cloned.get(original)!; const clone = new GraphNode(original.val); cloned.set(original, clone); // Phải set TRƯỚC khi recurse để handle cycles for (const neighbor of original.neighbors) { clone.neighbors.push(dfs(neighbor)); } return clone; } return dfs(node);}
cloneGraph — Xử lý Cycle
Phải thêm node vào map trước khi recurse vào neighbors. Nếu không, cycle trong graph sẽ gây infinite recursion.
5. Complexity Deep-dive
DFS và BFS
Operation
Time
Space
Ghi chú
DFS
O(V+E)
O(V)
V = nodes, E = edges
BFS
O(V+E)
O(V)
Queue chứa tối đa V nodes
Topological Sort
O(V+E)
O(V+E)
Phải build adjacency list
Cycle Detection
O(V+E)
O(V)
3-state DFS
Shortest Path (unweighted)
O(V+E)
O(V)
BFS
Adjacency List vs Adjacency Matrix
Adjacency List
Adjacency Matrix
Space
O(V+E)
O(V2)
Add edge
O(1)
O(1)
Check edge (u, v)
O(degree)
O(1)
Iterate neighbors
O(degree)
O(V)
Khi dùng
Sparse graphs (hầu hết)
Dense graphs (E≈V2)
Hầu hết FAANG interview problems dùng sparse graphs
E<<V2 (ví dụ: social networks — mỗi người có vài trăm friends, không phải N million friends). Adjacency list là lựa chọn mặc định.
6. Edge Cases & Pitfalls
Quên đánh dấu visited → Infinite Loop
// WRONG: gây infinite loop nếu graph có cyclefunction dfs(node: number): void { for (const neighbor of graph.get(node) || []) { dfs(neighbor); // Không check visited! }}// CORRECT: check visited trước khi recursefunction dfs(node: number): void { visited.add(node); // Mark BEFORE processing neighbors for (const neighbor of graph.get(node) || []) { if (!visited.has(neighbor)) dfs(neighbor); }}
Disconnected Graph — Outer Loop bắt buộc
// WRONG: chỉ start DFS từ node 0 → bỏ sót các connected components khácdfs(0);// CORRECT: loop qua tất cả nodesfor (let i = 0; i < n; i++) { if (!visited.has(i)) dfs(i); // Start new DFS from each unvisited node}
Grid: Check Bounds TRƯỚC khi Access
// WRONG: crash nếu r hoặc c out of boundsif (grid[r][c] !== '1') return;// CORRECT: bounds check trướcif (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== '1') return;
Directed vs Undirected — Build Adjacency List khác nhau
Undirected: thêm edge cả hai chiều u → v VÀ v → u
Directed: chỉ thêm một chiều u → v
Đây là lỗi phổ biến khi code vội — luôn đọc kỹ đề
Visited: Set vs boolean[]
boolean[]: nhanh hơn, nhưng chỉ dùng được khi nodes là integers từ 0 đến n-1
Set<number>: linh hoạt hơn, dùng được với bất kỳ node type nào
Set<string>: dùng khi nodes là strings (word ladder, grid coordinates)
7. Pattern Recognition
Khi thấy những từ khóa này → nghĩ đến Graph DFS/BFS:
Từ khóa trong đề
Pattern
”number of islands / components”
DFS/BFS, outer loop for disconnected
”shortest path” (unweighted)
BFS — guarantees shortest
”cycle detection”
3-state DFS hoặc Union-Find
”topological order” / “dependencies”
Kahn’s algorithm (BFS) hoặc DFS postorder
”course schedule” / “prerequisites”
Topological sort + cycle detection
”word ladder” / “transform one word”
BFS, từng node là 1 word
”clone graph”
DFS + HashMap (original → clone)
“flood fill” / “pacific atlantic”
Multi-source BFS/DFS
”bipartite graph”
Graph coloring, 2-state DFS
”rotten oranges”
Multi-source BFS
"Grid" problems = Implicit Graph
Khi thấy grid với directions (up/down/left/right), đó là graph problem với implicit adjacency list. Pattern chuẩn:
const directions = [[0,1],[0,-1],[1,0],[-1,0]]; // 4-directionalfor (const [dr, dc] of directions) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) { // Process neighbor }}
Cho beginWord, endWord, và một wordList. Tìm shortest transformation sequence từ beginWord đến endWord, mỗi bước chỉ thay đổi một chữ cái, và mỗi từ trung gian phải nằm trong wordList. Trả về độ dài của sequence ngắn nhất (0 nếu không tồn tại).
Tại sao đây là BFS?
Cần shortest path → BFS
Mỗi “edge” là một single-character transformation
Graph: nodes = words, edges = words differ by 1 character
Naive approach: O(N2⋅L) — compare every pair of words
Optimized approach: Pattern matching — *ot, h*t, ho* để group words
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number { const wordSet = new Set(wordList); if (!wordSet.has(endWord)) return 0; const L = beginWord.length; // Build adjacency list using pattern matching // "hot" → ["*ot", "h*t", "ho*"] (L patterns per word) // Nhóm các words có cùng pattern lại → họ là neighbors của nhau const patternMap = new Map<string, string[]>(); function addToPattern(word: string): void { for (let i = 0; i < L; i++) { const pattern = word.slice(0, i) + '*' + word.slice(i + 1); if (!patternMap.has(pattern)) patternMap.set(pattern, []); patternMap.get(pattern)!.push(word); } } addToPattern(beginWord); for (const word of wordSet) { addToPattern(word); } // BFS from beginWord const visited = new Set<string>([beginWord]); const queue: string[] = [beginWord]; let head = 0; // Index pointer (O(1) dequeue) let steps = 1; // beginWord itself counts as step 1 while (head < queue.length) { const levelSize = queue.length - head; steps++; for (let i = 0; i < levelSize; i++) { const word = queue[head++]; // Generate all L patterns for current word for (let j = 0; j < L; j++) { const pattern = word.slice(0, j) + '*' + word.slice(j + 1); for (const neighbor of patternMap.get(pattern) || []) { if (neighbor === endWord) return steps; if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); } } } } } return 0; // No path found}// ===== TEST CASES =====// beginWord = "hit", endWord = "cog"// wordList = ["hot","dot","dog","lot","log","cog"]// hit → hot → dot → dog → cog = 5 steps// beginWord = "hit", endWord = "cog"// wordList = ["hot","dot","dog","lot","log"] (no "cog")// → return 0// ===== COMPLEXITY =====// N = số words trong wordList, L = độ dài mỗi word// Time: O(N * L²) — N words, L patterns per word, L chars per pattern// Space: O(N * L²) — patternMap stores all patterns// ===== FOLLOW-UP: Bidirectional BFS =====// Có thể optimize thêm bằng bidirectional BFS:// BFS từ cả beginWord VÀ endWord đồng thời, dừng khi hai fronts gặp nhau// Giảm từ O(b^d) → O(b^(d/2)) trong practice, b = branching factor, d = depth
Bidirectional BFS — Optimization nâng cao
Khi interviewer hỏi “làm sao optimize hơn nữa?”, đề xuất bidirectional BFS: mở rộng từ cả hai đầu (beginWord và endWord) đồng thời, luôn expand phía có frontier nhỏ hơn. Giảm complexity đáng kể trong practice.
9. Self-Assessment Checklist
Kiểm tra kiến thức của bạn
Graph Fundamentals:
Giải thích sự khác biệt giữa directed và undirected graph
Viết code build adjacency list từ edge list trong 3 phút
Biết khi nào dùng adjacency list vs adjacency matrix
DFS:
Viết DFS recursive với visited set
Viết DFS iterative với explicit stack
Handle disconnected graph với outer loop
Giải thích tại sao cần 3 states trong cycle detection (không phải boolean)
BFS:
Viết BFS trả về shortest path
Giải thích tại sao BFS đảm bảo shortest path trong unweighted graph