Graphs & Graph Algorithms: Hướng Dẫn Toàn Diện

Giới Thiệu
Graph là cấu trúc dữ liệu tổng quát và linh hoạt nhất trong khoa học máy tính. Khác với tree — có cấu trúc phân cấp với một root duy nhất — graph có thể model bất kỳ mối quan hệ nào giữa các thực thể: mạng xã hội, bản đồ đường, web pages, điều kiện tiên quyết của môn học, network topology, và nhiều hơn nữa.
Nếu bạn đã học Trees & Binary Search Trees, đây là mối liên hệ: tree chỉ là một graph đặc biệt — connected, acyclic, với đúng một path giữa bất kỳ hai nodes nào. Graph bỏ đi những ràng buộc đó, cho phép cycle, disconnected components, và nhiều paths.
Trong coding interviews, các bài toán graph xuất hiện trong khoảng 30-35% các buổi phỏng vấn kỹ thuật. Các pattern bạn học ở đây — BFS, DFS, topological sort, shortest paths — là công cụ bạn sẽ dùng đi dùng lại mãi.
Bạn Sẽ Học Được Gì
✅ Hiểu thuật ngữ, các loại graph, và cách biểu diễn
✅ Cài đặt graph dùng adjacency list và adjacency matrix
✅ Nắm vững BFS và DFS — iterative và recursive
✅ Phát hiện cycle trong directed và undirected graphs
✅ Thực hiện topological sort để sắp xếp dependency
✅ Tìm đường đi ngắn nhất với giải thuật Dijkstra
✅ Dùng Union-Find cho các bài toán connected component
✅ Giải 8 bài toán graph kinh điển từng bước
Yêu Cầu Trước
- Hiểu về Ký hiệu Big O & Phân tích Độ phức tạp
- Quen thuộc với Các Pattern Giải Quyết Vấn Đề
- Hoàn thành Stacks & Queues (BFS dùng queue, DFS dùng stack)
- Hoàn thành Trees & Binary Search Trees (graph là dạng tổng quát hóa của tree)
1. Nền Tảng Về Graph
1.1 Graph Là Gì?
Graph là tập hợp các vertices (nodes) được kết nối bởi các edges (liên kết). Khác với tree, graph không có root, không có cấu trúc phân cấp parent-child, và có thể chứa cycle.
1.2 Thuật Ngữ Về Graph
| Thuật ngữ | Định nghĩa |
|---|---|
| Vertex (Node) | Một điểm trong graph |
| Edge | Kết nối giữa hai vertices |
| Adjacent | Hai vertices được nối bởi một edge |
| Degree | Số edges kết nối với một vertex |
| In-degree / Out-degree | Với directed graph: edges đi vào / đi ra khỏi một vertex |
| Path | Chuỗi vertices được kết nối bởi các edges |
| Cycle | Path bắt đầu và kết thúc tại cùng một vertex |
| Connected | Mọi vertex đều có thể đến từ mọi vertex khác |
| Component | Subgraph connected tối đại |
| DAG | Directed Acyclic Graph — directed graph không có cycle |
1.3 Các Loại Graph
| Loại | Mô tả | Ví dụ |
|---|---|---|
| Undirected | Edge không có hướng | Tình bạn (mutual) |
| Directed (Digraph) | Edge có hướng (A → B ≠ B → A) | Follow trên Twitter, dependencies |
| Weighted | Edge có giá trị số | Khoảng cách đường, độ trễ mạng |
| Unweighted | Tất cả edges đều bằng nhau | Kết nối mạng xã hội |
| Cyclic | Chứa ít nhất một cycle | Mạng đường bộ |
| Acyclic | Không có cycle (DAG = Directed Acyclic Graph) | Điều kiện tiên quyết môn học, build systems |
| Dense | Nhiều edges (gần V² edges) | Mạng mostly-connected |
| Sparse | Ít edges (gần V edges) | Lưới giao thông |
1.4 Ứng Dụng Thực Tế
| Lĩnh vực | Ứng dụng graph |
|---|---|
| Mạng xã hội | Users là vertices, friendship là edges (Facebook, LinkedIn) |
| Navigation | Thành phố là vertices, đường là weighted edges (Google Maps) |
| Web Crawling | Web pages là vertices, hyperlinks là edges (PageRank) |
| Dependency Resolution | Packages là vertices, dependencies là directed edges (npm, Maven) |
| Network Routing | Routers là vertices, kết nối là weighted edges |
| Recommendation Systems | Users/items là vertices, interactions là edges |
2. Biểu Diễn Graph
Cách bạn lưu trữ graph ảnh hưởng đến hiệu quả của mọi giải thuật. Có ba cách biểu diễn chính:
2.1 Adjacency Matrix
Mảng 2D trong đó matrix[i][j] = 1 (hoặc trọng số edge) nếu có edge từ vertex i đến vertex j.
Với graph trên (vertices: A=0, B=1, C=2, D=3):
A B C D
A [ 0 1 1 0 ]
B [ 0 0 1 0 ]
C [ 0 0 0 1 ]
D [ 0 0 0 0 ]Ưu điểm: Kiểm tra edge O(1), dễ cài đặt Nhược điểm: O(V²) space — lãng phí với sparse graphs
2.2 Adjacency List
Mỗi vertex map tới danh sách các neighbors. Cách biểu diễn phổ biến nhất cho interview problems.
A → [B, C]
B → [C]
C → [D]
D → []Ưu điểm: O(V + E) space — hiệu quả với sparse graphs Nhược điểm: Kiểm tra edge O(degree) — phải duyệt qua list
2.3 Edge List
Danh sách đơn giản tất cả các edges: [(A, B), (A, C), (B, C), (C, D)]
Ưu điểm: Đơn giản, dễ duyệt tất cả edges Nhược điểm: Kiểm tra edge O(E), không có truy cập neighbor trực tiếp
2.4 So Sánh
| Biểu diễn | Space | Thêm Edge | Kiểm tra Edge | Duyệt Neighbors |
|---|---|---|---|---|
| Adjacency Matrix | O(V²) | O(1) | O(1) | O(V) |
| Adjacency List | O(V + E) | O(1) | O(degree) | O(degree) |
| Edge List | O(E) | O(1) | O(E) | O(E) |
Default trong interview: Dùng adjacency list (Map/dict của arrays). Linh hoạt nhất và tiết kiệm bộ nhớ nhất cho các sparse graphs điển hình.
3. Cài Đặt Graph
TypeScript
// TypeScript - Graph dùng Adjacency List
class Graph {
private adjacencyList: Map<number, number[]>;
private directed: boolean;
constructor(directed = false) {
this.adjacencyList = new Map();
this.directed = directed;
}
addVertex(vertex: number): void {
if (!this.adjacencyList.has(vertex)) {
this.adjacencyList.set(vertex, []);
}
}
addEdge(u: number, v: number): void {
this.addVertex(u);
this.addVertex(v);
this.adjacencyList.get(u)!.push(v);
if (!this.directed) {
this.adjacencyList.get(v)!.push(u); // undirected: thêm cả hai chiều
}
}
getNeighbors(vertex: number): number[] {
return this.adjacencyList.get(vertex) ?? [];
}
getVertices(): number[] {
return Array.from(this.adjacencyList.keys());
}
print(): void {
for (const [vertex, neighbors] of this.adjacencyList) {
console.log(`${vertex} → [${neighbors.join(', ')}]`);
}
}
}
// Undirected Graph
const g = new Graph();
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 3);
g.addEdge(3, 4);
g.print();
// 0 → [1, 2]
// 1 → [0, 3]
// 2 → [0, 3]
// 3 → [1, 2, 4]
// 4 → [3]
// Directed Graph
const dg = new Graph(true);
dg.addEdge(0, 1);
dg.addEdge(0, 2);
dg.addEdge(1, 3);
dg.print();
// 0 → [1, 2]
// 1 → [3]
// 2 → []
// 3 → []Python
# Python - Graph dùng Adjacency List
from collections import defaultdict
class Graph:
def __init__(self, directed=False):
self.adjacency_list = defaultdict(list)
self.directed = directed
def add_vertex(self, vertex: int) -> None:
if vertex not in self.adjacency_list:
self.adjacency_list[vertex] = []
def add_edge(self, u: int, v: int) -> None:
self.adjacency_list[u].append(v)
if not self.directed:
self.adjacency_list[v].append(u)
def get_neighbors(self, vertex: int) -> list[int]:
return self.adjacency_list[vertex]
def print_graph(self) -> None:
for vertex, neighbors in self.adjacency_list.items():
print(f"{vertex} → {neighbors}")
# Dùng thử
g = Graph()
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 3)
g.add_edge(2, 3)
g.add_edge(3, 4)
g.print_graph()Weighted Graph
Với Dijkstra và các giải thuật weighted khác, lưu tuple (neighbor, weight):
// TypeScript - Weighted Graph
class WeightedGraph {
private adjacencyList: Map<number, [number, number][]>; // [neighbor, weight]
constructor() {
this.adjacencyList = new Map();
}
addEdge(u: number, v: number, weight: number): void {
if (!this.adjacencyList.has(u)) this.adjacencyList.set(u, []);
if (!this.adjacencyList.has(v)) this.adjacencyList.set(v, []);
this.adjacencyList.get(u)!.push([v, weight]);
this.adjacencyList.get(v)!.push([u, weight]); // undirected
}
getNeighbors(vertex: number): [number, number][] {
return this.adjacencyList.get(vertex) ?? [];
}
}# Python - Weighted Graph
class WeightedGraph:
def __init__(self):
self.adjacency_list = defaultdict(list) # {node: [(neighbor, weight), ...]}
def add_edge(self, u: int, v: int, weight: int) -> None:
self.adjacency_list[u].append((v, weight))
self.adjacency_list[v].append((u, weight)) # undirected4. BFS (Breadth-First Search)
BFS khám phá graph theo từng level — trước tiên tất cả neighbors của node bắt đầu, sau đó neighbors của chúng, v.v. Nó dùng queue (FIFO).
Thứ tự BFS từ vertex 0: 0 → 1 → 2 → 3 → 4 → 5
Giải Thuật BFS
- Khởi tạo queue với vertex bắt đầu
- Đánh dấu vertex bắt đầu là đã visited
- Khi queue không rỗng:
- Dequeue vertex ở đầu
- Xử lý nó
- Enqueue tất cả neighbors chưa visited và đánh dấu chúng đã visited
// TypeScript - BFS
function bfs(graph: Map<number, number[]>, start: number): number[] {
const visited = new Set<number>();
const queue: number[] = [start];
const result: number[] = [];
visited.add(start);
while (queue.length > 0) {
const vertex = queue.shift()!; // dequeue từ đầu
result.push(vertex);
for (const neighbor of graph.get(vertex) ?? []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}
// Build graph dạng adjacency list Map
const graph = new Map<number, number[]>([
[0, [1, 2]],
[1, [0, 3, 4]],
[2, [0, 5]],
[3, [1]],
[4, [1]],
[5, [2]],
]);
console.log(bfs(graph, 0)); // [0, 1, 2, 3, 4, 5]# Python - BFS
from collections import deque
def bfs(graph: dict[int, list[int]], start: int) -> list[int]:
visited = set([start])
queue = deque([start])
result = []
while queue:
vertex = queue.popleft()
result.append(vertex)
for neighbor in graph.get(vertex, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return resultBFS Tìm Đường Đi Ngắn Nhất (Unweighted)
BFS tự nhiên tìm được đường đi ngắn nhất trong unweighted graph, vì nó khám phá nodes theo thứ tự khoảng cách từ source.
// TypeScript - BFS Shortest Path
function bfsShortestPath(
graph: Map<number, number[]>,
start: number,
end: number
): number[] | null {
if (start === end) return [start];
const visited = new Set<number>([start]);
// Queue lưu [vertex, path để đến nó]
const queue: [number, number[]][] = [[start, [start]]];
while (queue.length > 0) {
const [vertex, path] = queue.shift()!;
for (const neighbor of graph.get(vertex) ?? []) {
if (neighbor === end) return [...path, neighbor];
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, [...path, neighbor]]);
}
}
}
return null; // không tìm thấy path
}# Python - BFS Shortest Path
def bfs_shortest_path(graph: dict, start: int, end: int) -> list[int] | None:
if start == end:
return [start]
visited = {start}
queue = deque([(start, [start])])
while queue:
vertex, path = queue.popleft()
for neighbor in graph.get(vertex, []):
if neighbor == end:
return path + [neighbor]
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None # không tìm thấy pathKhi nào dùng BFS:
- Đường đi ngắn nhất trong unweighted graphs
- Duyệt level-by-level (ví dụ: "số bước tối thiểu", "số hop tối thiểu")
- Tìm tất cả nodes trong một khoảng cách nhất định
- Web crawling (khám phá các trang gần trước)
5. DFS (Depth-First Search)
DFS khám phá graph bằng cách đi sâu nhất có thể theo một path trước khi backtrack. Nó dùng stack (hoặc đệ quy).
Thứ tự DFS từ vertex 0 (cùng graph trên): 0 → 1 → 3 → 4 → 2 → 5 (thứ tự phụ thuộc vào thứ tự neighbor)
DFS — Đệ Quy
// TypeScript - DFS Đệ Quy
function dfsRecursive(
graph: Map<number, number[]>,
vertex: number,
visited: Set<number> = new Set(),
result: number[] = []
): number[] {
visited.add(vertex);
result.push(vertex);
for (const neighbor of graph.get(vertex) ?? []) {
if (!visited.has(neighbor)) {
dfsRecursive(graph, neighbor, visited, result);
}
}
return result;
}# Python - DFS Đệ Quy
def dfs_recursive(graph: dict, vertex: int, visited: set = None, result: list = None) -> list[int]:
if visited is None:
visited = set()
if result is None:
result = []
visited.add(vertex)
result.append(vertex)
for neighbor in graph.get(vertex, []):
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited, result)
return resultDFS — Iterative (dùng stack tường minh)
// TypeScript - DFS Iterative
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 vertex = stack.pop()!; // pop từ đỉnh (LIFO)
if (visited.has(vertex)) continue;
visited.add(vertex);
result.push(vertex);
// Push neighbors theo thứ tự ngược để duyệt từ trái sang phải
for (const neighbor of [...(graph.get(vertex) ?? [])].reverse()) {
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
return result;
}# Python - DFS Iterative
def dfs_iterative(graph: dict, start: int) -> list[int]:
visited = set()
stack = [start]
result = []
while stack:
vertex = stack.pop()
if vertex in visited:
continue
visited.add(vertex)
result.append(vertex)
for neighbor in reversed(graph.get(vertex, [])):
if neighbor not in visited:
stack.append(neighbor)
return resultKhi nào dùng DFS:
- Phát hiện cycle
- Topological sort
- Tìm tất cả paths giữa hai nodes
- Giải mê cung, các bài toán backtracking
- Connected components
6. Connected Components
Connected component là subgraph tối đại trong đó mọi vertex đều có thể đến từ mọi vertex khác. Trong disconnected graph, bạn cần chạy BFS/DFS từ mỗi vertex chưa visited.
// TypeScript - Đếm Connected Components
function countComponents(n: number, edges: number[][]): number {
// Build adjacency list
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);
}
const visited = new Set<number>();
let components = 0;
function dfs(vertex: number): void {
visited.add(vertex);
for (const neighbor of graph.get(vertex) ?? []) {
if (!visited.has(neighbor)) dfs(neighbor);
}
}
for (let i = 0; i < n; i++) {
if (!visited.has(i)) {
dfs(i);
components++;
}
}
return components;
}
// Ví dụ: 5 vertices, edges [[0,1],[1,2],[3,4]]
// → 2 components: {0,1,2} và {3,4}
console.log(countComponents(5, [[0,1],[1,2],[3,4]])); // 2# Python - Đếm Connected Components
def count_components(n: int, edges: list[list[int]]) -> int:
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
def dfs(vertex: int) -> None:
visited.add(vertex)
for neighbor in graph[vertex]:
if neighbor not in visited:
dfs(neighbor)
components = 0
for i in range(n):
if i not in visited:
dfs(i)
components += 1
return componentsTime: O(V + E) — visit mọi vertex và edge một lần Space: O(V) — visited set + recursion stack
7. Phát Hiện Cycle
7.1 Undirected Graph — Cách DFS
Trong undirected graph, cycle tồn tại nếu DFS visit một neighbor đã visited VÀ không phải parent của vertex hiện tại.
// TypeScript - Phát Hiện Cycle trong Undirected Graph
function hasCycleUndirected(n: number, edges: number[][]): boolean {
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);
}
const visited = new Set<number>();
function dfs(vertex: number, parent: number): boolean {
visited.add(vertex);
for (const neighbor of graph.get(vertex) ?? []) {
if (!visited.has(neighbor)) {
if (dfs(neighbor, vertex)) return true;
} else if (neighbor !== parent) {
return true; // đã visited và không phải parent → có cycle!
}
}
return false;
}
for (let i = 0; i < n; i++) {
if (!visited.has(i) && dfs(i, -1)) return true;
}
return false;
}# Python - Phát Hiện Cycle trong Undirected Graph
def has_cycle_undirected(n: int, edges: list[list[int]]) -> bool:
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
def dfs(vertex: int, parent: int) -> bool:
visited.add(vertex)
for neighbor in graph[vertex]:
if neighbor not in visited:
if dfs(neighbor, vertex):
return True
elif neighbor != parent:
return True # back edge → có cycle
return False
for i in range(n):
if i not in visited and dfs(i, -1):
return True
return False7.2 Directed Graph — DFS với Ba Màu
Với directed graphs, chúng ta cần theo dõi DFS path hiện tại (recursion stack), không chỉ các nodes đã visited. Cycle tồn tại nếu chúng ta đến một node đang ở trên path hiện tại.
Dùng ba trạng thái (màu):
- WHITE (0): Chưa visited
- GRAY (1): Đang trong DFS stack (đang được xử lý)
- BLACK (2): Đã xử lý xong (tất cả descendants đã khám phá)
// TypeScript - Phát Hiện Cycle trong Directed Graph
function hasCycleDirected(n: number, edges: number[][]): boolean {
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);
const WHITE = 0, GRAY = 1, BLACK = 2;
const color = new Array(n).fill(WHITE);
function dfs(vertex: number): boolean {
color[vertex] = GRAY; // đánh dấu đang xử lý
for (const neighbor of graph.get(vertex) ?? []) {
if (color[neighbor] === GRAY) return true; // back edge → có cycle!
if (color[neighbor] === WHITE && dfs(neighbor)) return true;
}
color[vertex] = BLACK; // đã xử lý xong
return false;
}
for (let i = 0; i < n; i++) {
if (color[i] === WHITE && dfs(i)) return true;
}
return false;
}# Python - Phát Hiện Cycle trong Directed Graph
def has_cycle_directed(n: int, edges: list[list[int]]) -> bool:
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * n
def dfs(vertex: int) -> bool:
color[vertex] = GRAY
for neighbor in graph[vertex]:
if color[neighbor] == GRAY:
return True # có cycle!
if color[neighbor] == WHITE and dfs(neighbor):
return True
color[vertex] = BLACK
return False
for i in range(n):
if color[i] == WHITE and dfs(i):
return True
return False8. Topological Sort
Topological sort sắp xếp các vertices của directed acyclic graph (DAG) sao cho với mọi directed edge u → v, vertex u xuất hiện trước v trong thứ tự.
Ví dụ thực tế: Điều kiện tiên quyết của môn học. Nếu môn A là điều kiện tiên quyết của môn B, A phải xuất hiện trước B trong thứ tự.
Thứ tự topological hợp lệ: A, B, C, D hoặc B, A, C, D
8.1 Topological Sort Dùng DFS
Xử lý nodes theo post-order (thêm vào result SAU KHI tất cả descendants được xử lý), rồi đảo ngược.
// TypeScript - Topological Sort (DFS)
function topologicalSortDFS(n: number, edges: 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);
const visited = new Set<number>();
const result: number[] = [];
function dfs(vertex: number): void {
visited.add(vertex);
for (const neighbor of graph.get(vertex) ?? []) {
if (!visited.has(neighbor)) dfs(neighbor);
}
result.push(vertex); // thêm SAU KHI tất cả descendants xong
}
for (let i = 0; i < n; i++) {
if (!visited.has(i)) dfs(i);
}
return result.reverse(); // đảo ngược để có thứ tự topological
}
// Ví dụ: 4 môn học, prerequisites [[0,2],[1,2],[1,3],[2,3]]
// → thứ tự hợp lệ: [0, 1, 2, 3] hoặc [1, 0, 2, 3]# Python - Topological Sort (DFS)
def topological_sort_dfs(n: int, edges: list[list[int]]) -> list[int]:
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
visited = set()
result = []
def dfs(vertex: int) -> None:
visited.add(vertex)
for neighbor in graph[vertex]:
if neighbor not in visited:
dfs(neighbor)
result.append(vertex) # append sau khi tất cả descendants xong
for i in range(n):
if i not in visited:
dfs(i)
return result[::-1] # đảo ngược8.2 Giải Thuật Kahn (BFS-Based)
Liên tục loại bỏ các vertices có in-degree 0 (không có dependency). Cách này cũng phát hiện cycle — nếu result không chứa tất cả vertices, có cycle tồn tại.
// TypeScript - Giải Thuật Kahn
function topologicalSortKahn(n: number, edges: number[][]): number[] | null {
const graph = new Map<number, number[]>();
const inDegree = new Array(n).fill(0);
for (let i = 0; i < n; i++) graph.set(i, []);
for (const [u, v] of edges) {
graph.get(u)!.push(v);
inDegree[v]++;
}
// Bắt đầu với tất cả vertices không có prerequisite
const queue: number[] = [];
for (let i = 0; i < n; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const result: number[] = [];
while (queue.length > 0) {
const vertex = queue.shift()!;
result.push(vertex);
for (const neighbor of graph.get(vertex) ?? []) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
// Nếu result không bao gồm tất cả vertices → có cycle
return result.length === n ? result : null;
}# Python - Giải Thuật Kahn
from collections import deque
def topological_sort_kahn(n: int, edges: list[list[int]]) -> list[int] | None:
graph = {i: [] for i in range(n)}
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
queue = deque([i for i in range(n) if in_degree[i] == 0])
result = []
while queue:
vertex = queue.popleft()
result.append(vertex)
for neighbor in graph[vertex]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result if len(result) == n else None # None nghĩa là phát hiện cycle| Cách tiếp cận | Phát hiện Cycle | Thứ tự | Độ phức tạp |
|---|---|---|---|
| DFS (post-order + đảo ngược) | Cần bước riêng | Bất kỳ thứ tự hợp lệ | O(V + E) |
| Kahn (BFS) | Tích hợp sẵn (độ dài result ≠ n) | Nhỏ nhất về mặt từ điển nếu dùng min-heap | O(V + E) |
9. Giải Thuật Đường Đi Ngắn Nhất
9.1 BFS cho Unweighted Graphs
Với unweighted graphs, BFS cho đường đi ngắn nhất (ít edges nhất). Đã đề cập ở Phần 4.
9.2 Giải Thuật Dijkstra
Với weighted graphs có trọng số không âm, Dijkstra tìm đường đi ngắn nhất từ một source đến tất cả các vertices khác.
Ý tưởng cốt lõi: Tham lam chọn vertex chưa visited có khoảng cách nhỏ nhất đã biết và relax các edges của nó.
Bắt đầu từ vertex 0: distances = {0:0, 2:2, 1:3, 3:6, 4:7}
// TypeScript - Giải Thuật Dijkstra (giả lập min-heap bằng sorted array)
function dijkstra(
graph: Map<number, [number, number][]>, // node → [(neighbor, weight)]
start: number
): Map<number, number> {
const distances = new Map<number, number>();
const visited = new Set<number>();
// Khởi tạo tất cả distances là Infinity
for (const vertex of graph.keys()) {
distances.set(vertex, Infinity);
}
distances.set(start, 0);
// Giả lập priority queue: [distance, vertex]
const pq: [number, number][] = [[0, start]];
while (pq.length > 0) {
// Lấy vertex có distance nhỏ nhất
pq.sort((a, b) => a[0] - b[0]);
const [dist, vertex] = pq.shift()!;
if (visited.has(vertex)) continue;
visited.add(vertex);
for (const [neighbor, weight] of graph.get(vertex) ?? []) {
const newDist = dist + weight;
if (newDist < (distances.get(neighbor) ?? Infinity)) {
distances.set(neighbor, newDist);
pq.push([newDist, neighbor]);
}
}
}
return distances;
}
// Dùng thử
const wGraph = new Map<number, [number, number][]>([
[0, [[1, 4], [2, 2]]],
[1, [[3, 3]]],
[2, [[1, 1], [3, 5]]],
[3, [[4, 1]]],
[4, []],
]);
const dist = dijkstra(wGraph, 0);
console.log(dist); // Map { 0 → 0, 1 → 3, 2 → 2, 3 → 6, 4 → 7 }# Python - Giải Thuật Dijkstra (dùng heapq)
import heapq
def dijkstra(graph: dict[int, list[tuple[int, int]]], start: int) -> dict[int, int]:
# graph[u] = [(v, weight), ...]
distances = {vertex: float('inf') for vertex in graph}
distances[start] = 0
# Min-heap: (distance, vertex)
heap = [(0, start)]
while heap:
dist, vertex = heapq.heappop(heap)
if dist > distances[vertex]:
continue # entry cũ, bỏ qua
for neighbor, weight in graph.get(vertex, []):
new_dist = dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
heapq.heappush(heap, (new_dist, neighbor))
return distances
# Dùng thử
graph = {
0: [(1, 4), (2, 2)],
1: [(3, 3)],
2: [(1, 1), (3, 5)],
3: [(4, 1)],
4: [],
}
print(dijkstra(graph, 0)) # {0: 0, 1: 3, 2: 2, 3: 6, 4: 7}Time: O((V + E) log V) với min-heap thực sự Hạn chế: KHÔNG hoạt động với negative edge weights (dùng Bellman-Ford cho trường hợp đó)
10. Union-Find (Disjoint Set Union)
Union-Find (còn gọi là Disjoint Set Union / DSU) là cấu trúc dữ liệu theo dõi hiệu quả các phần tử thuộc cùng connected component.
Các thao tác chính:
find(x):xthuộc component nào? (trả về root/đại diện)union(x, y): Merge các components củaxvày
Tối ưu hóa:
- Path compression: Khi gọi
find, làm cho tất cả nodes trên path trỏ trực tiếp đến root - Union by rank: Luôn gắn cây nhỏ hơn dưới root của cây lớn hơn
// TypeScript - Union-Find với Path Compression + Union by Rank
class UnionFind {
private parent: number[];
private rank: number[];
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i); // mỗi node là parent của chính nó
this.rank = new Array(n).fill(0);
}
find(x: number): number {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]); // path compression
}
return this.parent[x];
}
union(x: number, y: number): boolean {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) return false; // đã trong cùng component
// Union by rank: gắn cây nhỏ hơn dưới cây lớn hơn
if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
return true; // merge hai components khác nhau
}
connected(x: number, y: number): boolean {
return this.find(x) === this.find(y);
}
}
// Dùng thử
const uf = new UnionFind(5);
uf.union(0, 1);
uf.union(1, 2);
uf.union(3, 4);
console.log(uf.connected(0, 2)); // true (cùng component)
console.log(uf.connected(0, 3)); // false (khác component)# Python - Union-Find
class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, x: int, y: int) -> bool:
root_x, root_y = self.find(x), self.find(y)
if root_x == root_y:
return False # đã connected
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
return True # đã merge
def connected(self, x: int, y: int) -> bool:
return self.find(x) == self.find(y)Time complexity: Gần O(1) mỗi thao tác (amortized O(α(n)) trong đó α là hàm inverse Ackermann — về cơ bản là constant với mọi input thực tế)
Khi nào dùng Union-Find vs DFS/BFS:
- Dùng Union-Find khi cần merge components và kiểm tra connectivity hiệu quả và động (edges được thêm theo thời gian)
- Dùng DFS/BFS khi cần duyệt hoặc xử lý cấu trúc graph
11. Các Bài Toán Phỏng Vấn Kinh Điển
Bài 1: Number of Islands (Số Đảo)
Cho lưới 2D gồm
'1'(đất) và'0'(nước), đếm số lượng đảo. Một đảo được bao quanh bởi nước và được tạo thành bởi các ô đất liền kề theo chiều ngang hoặc dọc.
Cách tiếp cận: DFS/BFS từ mỗi ô đất chưa visited, đánh dấu tất cả đất liền kết nối là đã visited.
// TypeScript - Number of Islands
function numIslands(grid: string[][]): number {
const rows = grid.length;
const cols = grid[0].length;
let count = 0;
function dfs(r: number, c: number): void {
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== '1') return;
grid[r][c] = '0'; // đánh dấu visited bằng cách "chìm" đảo
dfs(r + 1, c);
dfs(r - 1, c);
dfs(r, c + 1);
dfs(r, c - 1);
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === '1') {
count++;
dfs(r, c);
}
}
}
return count;
}
// grid = [["1","1","0"],["0","1","0"],["0","0","1"]] → 2# Python - Number of Islands
def num_islands(grid: list[list[str]]) -> int:
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r: int, c: int) -> None:
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '0' # đánh dấu visited
dfs(r + 1, c); dfs(r - 1, c)
dfs(r, c + 1); dfs(r, c - 1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return countTime: O(M × N). Space: O(M × N) recursion stack trong trường hợp xấu nhất.
Bài 2: Clone Graph (Nhân Bản Graph)
Cho tham chiếu đến một node trong connected undirected graph, trả về deep copy của graph.
Cách tiếp cận: DFS/BFS với visited map ánh xạ old nodes đến clones của chúng.
// TypeScript - Clone Graph
class GraphNode {
val: number;
neighbors: GraphNode[];
constructor(val = 0, neighbors: GraphNode[] = []) {
this.val = val;
this.neighbors = neighbors;
}
}
function cloneGraph(node: GraphNode | null): GraphNode | null {
if (!node) return null;
const visited = new Map<GraphNode, GraphNode>();
function dfs(curr: GraphNode): GraphNode {
if (visited.has(curr)) return visited.get(curr)!;
const clone = new GraphNode(curr.val);
visited.set(curr, clone);
for (const neighbor of curr.neighbors) {
clone.neighbors.push(dfs(neighbor));
}
return clone;
}
return dfs(node);
}# Python - Clone Graph
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors or []
def clone_graph(node: 'Node | None') -> 'Node | None':
if not node:
return None
visited = {} # old_node → clone
def dfs(curr: Node) -> Node:
if curr in visited:
return visited[curr]
clone = Node(curr.val)
visited[curr] = clone
for neighbor in curr.neighbors:
clone.neighbors.append(dfs(neighbor))
return clone
return dfs(node)Time: O(V + E). Space: O(V) cho visited map.
Bài 3: Course Schedule (Phát Hiện Cycle / Topological Sort)
Có
nmôn học (0 đến n-1). Cho các cặp prerequisites[a, b]nghĩa là "học b trước a", xác định liệu bạn có thể hoàn thành tất cả các môn học không.
Cách tiếp cận: Phát hiện cycle trong directed graph. Nếu có cycle, không thể hoàn thành tất cả các môn.
// TypeScript - Course Schedule
function canFinish(numCourses: number, prerequisites: number[][]): boolean {
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 = chưa visited, 1 = đang xử lý, 2 = xong
const state = new Array(numCourses).fill(0);
function hasCycle(node: number): boolean {
if (state[node] === 1) return true; // cycle!
if (state[node] === 2) return false; // đã an toàn
state[node] = 1; // đánh dấu đang xử lý
for (const next of graph.get(node) ?? []) {
if (hasCycle(next)) return true;
}
state[node] = 2; // đánh dấu xong
return false;
}
for (let i = 0; i < numCourses; i++) {
if (hasCycle(i)) return false;
}
return true;
}# Python - Course Schedule
def can_finish(num_courses: int, prerequisites: list[list[int]]) -> bool:
graph = {i: [] for i in range(num_courses)}
for course, prereq in prerequisites:
graph[prereq].append(course)
UNVISITED, IN_PROGRESS, DONE = 0, 1, 2
state = [UNVISITED] * num_courses
def has_cycle(node: int) -> bool:
if state[node] == IN_PROGRESS:
return True
if state[node] == DONE:
return False
state[node] = IN_PROGRESS
for nxt in graph[node]:
if has_cycle(nxt):
return True
state[node] = DONE
return False
return not any(has_cycle(i) for i in range(num_courses))Time: O(V + E). Đây chính xác là phát hiện cycle directed từ Phần 7.2.
Bài 4: Word Ladder (BFS Đường Chuyển Đổi Ngắn Nhất)
Cho hai từ
beginWordvàendWord, và danh sách từ, tìm độ dài chuỗi chuyển đổi ngắn nhất từbeginWordđếnendWord, thay đổi một chữ cái mỗi lần (mỗi từ trung gian phải có trong danh sách).
Cách tiếp cận: BFS — mỗi từ là một vertex, các edges kết nối các từ khác nhau một chữ cái.
// TypeScript - Word Ladder
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const wordSet = new Set(wordList);
if (!wordSet.has(endWord)) return 0;
const queue: [string, number][] = [[beginWord, 1]]; // [word, số bước]
const visited = new Set([beginWord]);
while (queue.length > 0) {
const [word, steps] = queue.shift()!;
for (let i = 0; i < word.length; i++) {
for (let c = 97; c <= 122; c++) { // 'a' đến 'z'
const newWord = word.slice(0, i) + String.fromCharCode(c) + word.slice(i + 1);
if (newWord === endWord) return steps + 1;
if (wordSet.has(newWord) && !visited.has(newWord)) {
visited.add(newWord);
queue.push([newWord, steps + 1]);
}
}
}
}
return 0; // không tìm thấy chuỗi chuyển đổi
}# Python - Word Ladder
def ladder_length(begin_word: str, end_word: str, word_list: list[str]) -> int:
word_set = set(word_list)
if end_word not in word_set:
return 0
queue = deque([(begin_word, 1)])
visited = {begin_word}
while queue:
word, steps = queue.popleft()
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
new_word = word[:i] + c + word[i+1:]
if new_word == end_word:
return steps + 1
if new_word in word_set and new_word not in visited:
visited.add(new_word)
queue.append((new_word, steps + 1))
return 0Time: O(M² × N) trong đó M = độ dài từ, N = kích thước danh sách từ.
Bài 5: Pacific Atlantic Water Flow (Dòng Chảy Thái Bình Dương - Đại Tây Dương)
Cho đảo M×N, nước chảy đến các ô liền kề có độ cao bằng hoặc thấp hơn. Nước có thể chảy đến Thái Bình Dương (các cạnh trên/trái) và Đại Tây Dương (các cạnh dưới/phải). Trả về tất cả các ô có thể chảy đến cả hai đại dương.
Cách tiếp cận: BFS/DFS ngược từ biên đại dương vào trong. Một ô có thể đến đại dương nếu nó có thể được đến bằng cách đi "lên dốc" từ biên.
// TypeScript - Pacific Atlantic Water Flow
function pacificAtlantic(heights: number[][]): number[][] {
const rows = heights.length;
const cols = heights[0].length;
const pacific = new Set<string>();
const atlantic = new Set<string>();
const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
function bfs(starts: number[][], visited: Set<string>): void {
const queue: number[][] = [...starts];
for (const [r, c] of starts) visited.add(`${r},${c}`);
while (queue.length > 0) {
const [r, c] = queue.shift()!;
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
const key = `${nr},${nc}`;
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
if (visited.has(key)) continue;
if (heights[nr][nc] < heights[r][c]) continue; // nước chảy xuống; chúng ta đi lên
visited.add(key);
queue.push([nr, nc]);
}
}
}
// Thái Bình Dương: hàng trên + cột trái
const pacificStarts = [
...Array.from({ length: rows }, (_, r) => [r, 0]),
...Array.from({ length: cols }, (_, c) => [0, c]),
];
// Đại Tây Dương: hàng dưới + cột phải
const atlanticStarts = [
...Array.from({ length: rows }, (_, r) => [r, cols - 1]),
...Array.from({ length: cols }, (_, c) => [rows - 1, c]),
];
bfs(pacificStarts, pacific);
bfs(atlanticStarts, atlantic);
const result: number[][] = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (pacific.has(`${r},${c}`) && atlantic.has(`${r},${c}`)) {
result.push([r, c]);
}
}
}
return result;
}# Python - Pacific Atlantic Water Flow
def pacific_atlantic(heights: list[list[int]]) -> list[list[int]]:
rows, cols = len(heights), len(heights[0])
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
def bfs(starts: list[tuple[int,int]]) -> set[tuple[int,int]]:
visited = set(starts)
queue = deque(starts)
while queue:
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols
and (nr, nc) not in visited
and heights[nr][nc] >= heights[r][c]):
visited.add((nr, nc))
queue.append((nr, nc))
return visited
pacific_starts = [(r, 0) for r in range(rows)] + [(0, c) for c in range(cols)]
atlantic_starts = [(r, cols-1) for r in range(rows)] + [(rows-1, c) for c in range(cols)]
pacific = bfs(pacific_starts)
atlantic = bfs(atlantic_starts)
return [[r, c] for r in range(rows) for c in range(cols) if (r, c) in pacific and (r, c) in atlantic]Time: O(M × N). Space: O(M × N).
Bài 6: Redundant Connection (Union-Find)
Cho tree có
nnodes vànedges (một edge thừa được thêm vào, tạo ra cycle), tìm và trả về edge thừa đó.
Cách tiếp cận: Xử lý edges từng cái một với Union-Find. Edge đầu tiên mà cả hai endpoints đã trong cùng component là edge thừa.
// TypeScript - Redundant Connection
function findRedundantConnection(edges: number[][]): number[] {
const n = edges.length;
const uf = new UnionFind(n + 1); // 1-indexed
for (const [u, v] of edges) {
if (!uf.union(u, v)) return [u, v]; // đã connected → thừa
}
return [];
}
// edges = [[1,2],[1,3],[2,3]] → [2,3] (edge tạo ra cycle)# Python - Redundant Connection
def find_redundant_connection(edges: list[list[int]]) -> list[int]:
n = len(edges)
uf = UnionFind(n + 1) # 1-indexed
for u, v in edges:
if not uf.union(u, v):
return [u, v] # đã trong cùng component
return []Time: O(n × α(n)) ≈ O(n). Space: O(n).
Bài 7: Cheapest Flights Within K Stops (Biến Thể Dijkstra)
Cho các chuyến bay
(from, to, price), điểm xuất phát, điểm đến, vàksố điểm dừng tối đa, tìm giá rẻ nhất từ nguồn đến đích với tối đakđiểm dừng. Trả về-1nếu không thể.
Cách tiếp cận: Dijkstra biến đổi hoặc Bellman-Ford với ràng buộc số hop. Dùng BFS-style relaxation theo dõi (số dừng, chi phí) trong priority queue.
// TypeScript - Cheapest Flights Within K Stops
function findCheapestPrice(
n: number, flights: number[][], src: number, dst: number, k: number
): number {
const graph = new Map<number, [number, number][]>();
for (let i = 0; i < n; i++) graph.set(i, []);
for (const [from, to, price] of flights) {
graph.get(from)!.push([to, price]);
}
// [cost, node, stops_remaining]
const pq: [number, number, number][] = [[0, src, k + 1]];
const visited = new Map<number, number>(); // node → max stops đã thấy
while (pq.length > 0) {
pq.sort((a, b) => a[0] - b[0]);
const [cost, node, stopsLeft] = pq.shift()!;
if (node === dst) return cost;
if (stopsLeft === 0) continue;
// Bỏ qua nếu đã visited node này với nhiều stops hơn
if ((visited.get(node) ?? -1) >= stopsLeft) continue;
visited.set(node, stopsLeft);
for (const [next, price] of graph.get(node) ?? []) {
pq.push([cost + price, next, stopsLeft - 1]);
}
}
return -1;
}# Python - Cheapest Flights Within K Stops
def find_cheapest_price(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
graph = defaultdict(list)
for frm, to, price in flights:
graph[frm].append((to, price))
# (cost, node, stops_remaining)
heap = [(0, src, k + 1)]
visited = {} # node → max stops_remaining đã thấy
while heap:
cost, node, stops_left = heapq.heappop(heap)
if node == dst:
return cost
if stops_left == 0:
continue
if visited.get(node, -1) >= stops_left:
continue
visited[node] = stops_left
for nxt, price in graph[node]:
heapq.heappush(heap, (cost + price, nxt, stops_left - 1))
return -1Bài 8: Graph Valid Tree (Kiểm Tra Tree Hợp Lệ)
Cho
nnodes (0 đến n-1) và danh sách edges, xác định liệu chúng có tạo thành valid tree không.
Một valid tree có hai tính chất:
- Đúng
n - 1edges (tree với n nodes có n-1 edges) - Tất cả nodes đều connected (không có disconnected components)
// TypeScript - Graph Valid Tree
function validTree(n: number, edges: number[][]): boolean {
// Phải có đúng n-1 edges
if (edges.length !== n - 1) return false;
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);
}
// BFS từ node 0 — phải visit tất cả n nodes
const visited = new Set<number>([0]);
const queue = [0];
while (queue.length > 0) {
const node = queue.shift()!;
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return visited.size === n; // tất cả nodes có thể đến → valid tree
}# Python - Graph Valid Tree
def valid_tree(n: int, edges: list[list[int]]) -> bool:
if len(edges) != n - 1:
return False
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = {0}
queue = deque([0])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return len(visited) == n12. Hướng Dẫn Nhận Dạng Pattern
Khi thấy một bài toán graph, hãy xác định pattern trước:
| Pattern | Từ khóa nhận dạng | Giải thuật | Ví dụ |
|---|---|---|---|
| Grid traversal | "lưới 2D", "đảo", "matrix", "ô" | BFS hoặc DFS trên grid | Number of Islands |
| Đường đi ngắn nhất (unweighted) | "số bước tối thiểu", "số hop tối thiểu", "ít nước đi nhất" | BFS | Word Ladder |
| Đường đi ngắn nhất (weighted) | "chi phí tối thiểu", "giá rẻ nhất", "khoảng cách tối thiểu" | Dijkstra | Cheapest Flights |
| Phát hiện cycle | "có thể hoàn thành", "deadlock", "dependency vòng tròn" | DFS với màu (directed) / Union-Find | Course Schedule |
| Thứ tự topological | "prerequisites", "build order", "task dependencies" | Kahn BFS hoặc DFS post-order | Course Schedule II |
| Connected components | "đếm vùng", "nhóm độc lập", "số components" | DFS/BFS từ mỗi unvisited | Number of Islands |
| Union-Find | "merge nhóm", "redundant connection", "dynamic connectivity" | Union-Find | Redundant Connection |
| Clone/copy graph | "deep copy", "nhân bản" | DFS với visited map | Clone Graph |
Decision tree cho các bài graph:
Heuristic phỏng vấn:
- "Số bước / hop tối thiểu" → BFS (khám phá level by level)
- "Tất cả paths / bất kỳ path nào" → DFS (khám phá depth first)
- "Course schedule / prerequisites" → Topological sort + phát hiện cycle
- "Kết nối vùng / merge nhóm" → Union-Find
- "Bài toán lưới 2D" → Coi như graph với 4 neighbors theo hướng
- "Clone / copy structure" → DFS với visited hashmap
13. Tham Chiếu Độ Phức Tạp
Time Complexity theo Giải Thuật
| Giải thuật | Time | Space | Ghi chú |
|---|---|---|---|
| BFS | O(V + E) | O(V) | Queue-based, level-by-level |
| DFS | O(V + E) | O(V) | Stack/đệ quy, depth-first |
| Topological Sort (DFS) | O(V + E) | O(V) | Post-order + đảo ngược |
| Topological Sort (Kahn) | O(V + E) | O(V) | BFS với in-degree |
| Dijkstra (binary heap) | O((V + E) log V) | O(V) | Chỉ với non-negative weights |
| Union-Find (có tối ưu) | O(α(n)) mỗi op | O(V) | Gần O(1) amortized |
| Phát hiện Cycle (undirected) | O(V + E) | O(V) | DFS với parent tracking |
| Phát hiện Cycle (directed) | O(V + E) | O(V) | DFS với 3 trạng thái màu |
V = vertices, E = edges, α = hàm inverse Ackermann
Space Complexity để Lưu Graph
| Biểu diễn | Space | Phù hợp nhất cho |
|---|---|---|
| Adjacency Matrix | O(V²) | Dense graphs, kiểm tra edge O(1) |
| Adjacency List | O(V + E) | Sparse graphs (hầu hết bài toán) |
| Edge List | O(E) | Thuật toán Kruskal MST |
14. Bài Tập Thực Hành
Easy (Bắt Đầu Từ Đây)
| # | Bài Toán | Pattern | Cách Tiếp Cận |
|---|---|---|---|
| 1 | Number of Islands | Grid DFS/BFS | DFS, "chìm" đất đã visited |
| 2 | Flood Fill | Grid DFS | DFS từ ô nguồn |
| 3 | Find if Path Exists in Graph | Traversal cơ bản | BFS/DFS hoặc Union-Find |
| 4 | Connected Components in Undirected Graph | Components | DFS từ mỗi unvisited |
| 5 | Clone Graph | DFS + hashmap | DFS đệ quy với clone map |
Medium (Xây Dựng Tự Tin)
| # | Bài Toán | Pattern | Cách Tiếp Cận |
|---|---|---|---|
| 6 | Course Schedule | Phát hiện cycle | DFS với 3 màu |
| 7 | Course Schedule II | Topological sort | Kahn hoặc DFS post-order |
| 8 | Word Ladder | BFS shortest path | BFS với biến đổi từ |
| 9 | Pacific Atlantic Water Flow | Multi-source BFS | BFS ngược từ biên |
| 10 | Rotting Oranges | Multi-source BFS | BFS từ tất cả ô cam thối đồng thời |
| 11 | Redundant Connection | Union-Find | Edge đầu tiên tạo cycle |
| 12 | Graph Valid Tree | Kiểm tra tree | n-1 edges + tất cả connected |
Hard (Mức Thành Thạo)
| # | Bài Toán | Pattern | Cách Tiếp Cận |
|---|---|---|---|
| 13 | Cheapest Flights Within K Stops | Dijkstra biến thể | Dijkstra có ràng buộc số dừng |
| 14 | Alien Dictionary | Topological sort | Build graph từ các từ liền kề |
| 15 | Serialize and Deserialize Binary Tree | BFS encoding | Level-order với null markers |
Tổng Kết
Graph là cấu trúc dữ liệu mạnh mẽ và linh hoạt nhất — chúng model gần như bất kỳ mối quan hệ thực tế nào.
Các giải thuật nền tảng:
- BFS: Đường đi ngắn nhất (unweighted), khám phá level-by-level
- DFS: Phát hiện cycle, topological sort, bài toán all-paths
- Dijkstra: Đường đi ngắn nhất với non-negative weights
- Union-Find: Dynamic connectivity, phát hiện cycle, merge components
Các pattern chính:
- Bài toán lưới → Coi như implicit graph với 4-directional neighbors
- Prerequisites / ordering → Topological sort (Kahn hoặc DFS)
- Phát hiện cycle → DFS màu (directed) hoặc parent tracking (undirected)
- Dynamic merging → Union-Find
- Minimum cost path → Dijkstra
Tiếp theo trong series DSA? Bài viết tiếp theo đề cập Heaps & Priority Queues — cấu trúc dữ liệu hỗ trợ giải thuật Dijkstra và các bài toán "top-K". Bạn sẽ xây dựng MinHeap từ đầu và nắm vững các pattern để tìm K phần tử lớn nhất/nhỏ nhất hiệu quả.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.