Back to blog

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

algorithmsdata-structuresgraphsinterview-prepdsabfs-dfs
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


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
EdgeKết nối giữa hai vertices
AdjacentHai vertices được nối bởi một edge
DegreeSố edges kết nối với một vertex
In-degree / Out-degreeVới directed graph: edges đi vào / đi ra khỏi một vertex
PathChuỗi vertices được kết nối bởi các edges
CyclePath bắt đầu và kết thúc tại cùng một vertex
ConnectedMọi vertex đều có thể đến từ mọi vertex khác
ComponentSubgraph connected tối đại
DAGDirected Acyclic Graph — directed graph không có cycle

1.3 Các Loại Graph

LoạiMô tảVí dụ
UndirectedEdge không có hướngTình bạn (mutual)
Directed (Digraph)Edge có hướng (A → B ≠ B → A)Follow trên Twitter, dependencies
WeightedEdge có giá trị sốKhoảng cách đường, độ trễ mạng
UnweightedTất cả edges đều bằng nhauKết nối mạng xã hội
CyclicChứa ít nhất một cycleMạng đường bộ
AcyclicKhông có cycle (DAG = Directed Acyclic Graph)Điều kiện tiên quyết môn học, build systems
DenseNhiề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ộiUsers là vertices, friendship là edges (Facebook, LinkedIn)
NavigationThành phố là vertices, đường là weighted edges (Google Maps)
Web CrawlingWeb pages là vertices, hyperlinks là edges (PageRank)
Dependency ResolutionPackages là vertices, dependencies là directed edges (npm, Maven)
Network RoutingRouters là vertices, kết nối là weighted edges
Recommendation SystemsUsers/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ễnSpaceThêm EdgeKiểm tra EdgeDuyệt Neighbors
Adjacency MatrixO(V²)O(1)O(1)O(V)
Adjacency ListO(V + E)O(1)O(degree)O(degree)
Edge ListO(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))  # undirected

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

  1. Khởi tạo queue với vertex bắt đầu
  2. Đánh dấu vertex bắt đầu là đã visited
  3. 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 result

BFS 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 path

Khi 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)

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 result

DFS — 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 result

Khi 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 components

Time: 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 False

7.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 False

8. 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ược

8.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ậnPhát hiện CycleThứ tựĐộ phức tạp
DFS (post-order + đảo ngược)Cần bước riêngBấ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-heapO(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): x thuộc component nào? (trả về root/đại diện)
  • union(x, y): Merge các components của x và 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 count

Time: 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ó n mô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ừ beginWord và endWord, và danh sách từ, tìm độ dài chuỗi chuyển đổi ngắn nhất từ beginWord đến endWord, 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 0

Time: 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ó n nodes và n edges (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à k số điểm dừng tối đa, tìm giá rẻ nhất từ nguồn đến đích với tối đa k điểm dừng. Trả về -1 nế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 -1

Bài 8: Graph Valid Tree (Kiểm Tra Tree Hợp Lệ)

Cho n nodes (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:

  1. Đúng n - 1 edges (tree với n nodes có n-1 edges)
  2. 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) == n

12. 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:

PatternTừ khóa nhận dạngGiải thuậtVí dụ
Grid traversal"lưới 2D", "đảo", "matrix", "ô"BFS hoặc DFS trên gridNumber 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"BFSWord 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"DijkstraCheapest 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-FindCourse Schedule
Thứ tự topological"prerequisites", "build order", "task dependencies"Kahn BFS hoặc DFS post-orderCourse Schedule II
Connected components"đếm vùng", "nhóm độc lập", "số components"DFS/BFS từ mỗi unvisitedNumber of Islands
Union-Find"merge nhóm", "redundant connection", "dynamic connectivity"Union-FindRedundant Connection
Clone/copy graph"deep copy", "nhân bản"DFS với visited mapClone 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ậtTimeSpaceGhi chú
BFSO(V + E)O(V)Queue-based, level-by-level
DFSO(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 opO(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ễnSpacePhù hợp nhất cho
Adjacency MatrixO(V²)Dense graphs, kiểm tra edge O(1)
Adjacency ListO(V + E)Sparse graphs (hầu hết bài toán)
Edge ListO(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ánPatternCách Tiếp Cận
1Number of IslandsGrid DFS/BFSDFS, "chìm" đất đã visited
2Flood FillGrid DFSDFS từ ô nguồn
3Find if Path Exists in GraphTraversal cơ bảnBFS/DFS hoặc Union-Find
4Connected Components in Undirected GraphComponentsDFS từ mỗi unvisited
5Clone GraphDFS + hashmapDFS đệ quy với clone map

Medium (Xây Dựng Tự Tin)

#Bài ToánPatternCách Tiếp Cận
6Course SchedulePhát hiện cycleDFS với 3 màu
7Course Schedule IITopological sortKahn hoặc DFS post-order
8Word LadderBFS shortest pathBFS với biến đổi từ
9Pacific Atlantic Water FlowMulti-source BFSBFS ngược từ biên
10Rotting OrangesMulti-source BFSBFS từ tất cả ô cam thối đồng thời
11Redundant ConnectionUnion-FindEdge đầu tiên tạo cycle
12Graph Valid TreeKiểm tra treen-1 edges + tất cả connected

Hard (Mức Thành Thạo)

#Bài ToánPatternCách Tiếp Cận
13Cheapest Flights Within K StopsDijkstra biến thểDijkstra có ràng buộc số dừng
14Alien DictionaryTopological sortBuild graph từ các từ liền kề
15Serialize and Deserialize Binary TreeBFS encodingLevel-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.