Cấu trúc tổng quát nhất. BFS (Tuần 6) và DFS (Tuần 8) đều áp dụng được. Học thêm Topological Sort, Union-Find, và Dijkstra (kết hợp Heap từ Tuần 9) — bộ ba tools cho mọi bài graph.
u → v. Ví dụ: dependency graph, social media follows.u — v. Ví dụ: friendship, road map.| Aspect | Adjacency List | Adjacency Matrix |
|---|---|---|
| Bộ nhớ | O(V + E) | O(V²) |
| Kiểm tra cạnh u–v | O(deg(u)) | O(1) |
| Duyệt neighbors của u | O(deg(u)) | O(V) |
| Phù hợp với | Sparse (E << V²) | Dense, V nhỏ |
from collections import defaultdict
# Từ list edges
edges = [[0,1], [0,2], [1,2], [2,3]]
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # cho undirected; bỏ dòng này nếu directed
Đa số bài LeetCode có graph sparse → ưu tiên adjacency list.
Graph có thể có chu kỳ (khác cây). Không kiểm soát visited → vô tận.
from collections import deque
def bfs(graph, start):
visited = {start}
q = deque([start])
while q:
node = q.popleft()
process(node)
for nei in graph[node]:
if nei not in visited:
visited.add(nei)
q.append(nei)
def dfs(graph, node, visited):
if node in visited: return
visited.add(node)
process(node)
for nei in graph[node]:
dfs(graph, nei, visited)
| Tiêu chí | BFS | DFS |
|---|---|---|
| Tìm đường ngắn nhất (unweighted) | ✓ Phù hợp tự nhiên | ✗ Không đảm bảo |
| Khám phá toàn bộ component | OK | OK |
| Topological sort | Kahn's algorithm (BFS-based) | DFS post-order |
| Cycle detection | Khó hơn | Tự nhiên (3-color) |
| Bộ nhớ trong tree khổng lồ | O(branching^depth) | O(depth) |
| Code complexity | Iterative dễ | Recursive đẹp; iterative phức tạp |
Trong các bài lưới, không cần build adjacency list. Mỗi ô (r, c) là node, neighbors là 4 ô kế (hoặc 8 ô với chéo).
# Template duyệt 4 hướng
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)] # up, down, left, right
def neighbors(r, c, m, n):
for dr, dc in DIRS:
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n:
yield nr, nc
# Thay vì set visited, mark trực tiếp lên grid:
grid[r][c] = '0' # cho Number of Islands — mark land thành water
Đề (LeetCode #200): Lưới 2D với '1' = land, '0' = water. Đếm số đảo (cụm '1' liên thông qua 4 hướng).
def numIslands(grid):
if not grid: return 0
m, n = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] != '1':
return
grid[r][c] = '0' # in-place mark visited
dfs(r-1, c); dfs(r+1, c); dfs(r, c-1); dfs(r, c+1)
for r in range(m):
for c in range(n):
if grid[r][c] == '1':
dfs(r, c)
count += 1
return count
Time O(m × n), Space O(m × n) worst case (recursion depth).
from collections import deque
def numIslands(grid):
if not grid: return 0
m, n = len(grid), len(grid[0])
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
count = 0
for r in range(m):
for c in range(n):
if grid[r][c] == '1':
count += 1
q = deque([(r, c)])
grid[r][c] = '0' # mark KHI push (quan trọng!)
while q:
r0, c0 = q.popleft()
for dr, dc in DIRS:
nr, nc = r0 + dr, c0 + dc
if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == '1':
grid[nr][nc] = '0' # mark NGAY
q.append((nr, nc))
return count
Time và space giống DFS. Dùng BFS khi cần tránh stack overflow trên grid lớn.
Đề (LeetCode #695): Tương tự #200 nhưng trả về diện tích đảo lớn nhất.
def maxAreaOfIsland(grid):
m, n = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] != 1:
return 0
grid[r][c] = 0
return 1 + dfs(r-1, c) + dfs(r+1, c) + dfs(r, c-1) + dfs(r, c+1)
best = 0
for r in range(m):
for c in range(n):
if grid[r][c] == 1:
best = max(best, dfs(r, c))
return best
Time O(m × n).
DFS giờ trả về kích thước cây con (= số ô của đảo). Pattern này tận dụng đệ quy bottom-up từ Tuần 8 (đệ quy trả về thông tin tổng hợp).
Đề (LeetCode #994): Lưới có 0 (rỗng), 1 (cam tươi), 2 (cam thối). Mỗi phút, cam thối lây sang cam tươi 4 hướng. Trả về số phút để mọi cam thối, -1 nếu không thể.
Khởi tạo queue với tất cả cam thối ban đầu. BFS từ chúng đồng thời — mỗi level = 1 phút.
def orangesRotting(grid):
m, n = len(grid), len(grid[0])
q = deque()
fresh = 0
for r in range(m):
for c in range(n):
if grid[r][c] == 2: q.append((r, c, 0))
elif grid[r][c] == 1: fresh += 1
minutes = 0
DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
while q:
r, c, t = q.popleft()
minutes = max(minutes, t)
for dr, dc in DIRS:
nr, nc = r+dr, c+dc
if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh -= 1
q.append((nr, nc, t + 1))
return minutes if fresh == 0 else -1
Đề (LeetCode #286): Lưới với -1 (tường), 0 (cổng), INF (phòng trống). Điền mỗi phòng trống bằng khoảng cách đến cổng gần nhất.
INF = 2147483647
def wallsAndGates(rooms):
if not rooms: return
m, n = len(rooms), len(rooms[0])
q = deque()
for r in range(m):
for c in range(n):
if rooms[r][c] == 0:
q.append((r, c))
DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
while q:
r, c = q.popleft()
for dr, dc in DIRS:
nr, nc = r+dr, c+dc
if 0 <= nr < m and 0 <= nc < n and rooms[nr][nc] == INF:
rooms[nr][nc] = rooms[r][c] + 1
q.append((nr, nc))
Time O(m × n).
Quy nạp theo level. Level 0 = source. Level k+1 = neighbors mới của level k chưa visit. Mọi node trong level k cách source đúng k bước. Khi target được mark visited lần đầu, level đó = shortest distance.
Khi weight tổng quát → dùng Dijkstra (xem 6.1).
Đề (LeetCode #127, Hard): Cho beginWord, endWord, wordList. Mỗi bước thay 1 ký tự để tạo từ mới (phải có trong wordList). Trả về độ dài chuỗi ngắn nhất từ begin đến end (bao gồm cả hai), 0 nếu không thể.
Ví dụ: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] → 5 (hit→hot→dot→dog→cog).
Node = từ. Edge = hai từ khác nhau đúng 1 ký tự. Build adjacency list trực tiếp tốn O(N² × L) — quá đắt. Thay vào đó:
# Generic patterns: thay mỗi vị trí bằng '*'
# "hot" → ["*ot", "h*t", "ho*"]
# Hai từ là neighbors ⟺ chia sẻ ít nhất 1 generic pattern
from collections import defaultdict, deque
def ladderLength(beginWord, endWord, wordList):
if endWord not in wordList: return 0
L = len(beginWord)
# Build map: pattern → list of words
patterns = defaultdict(list)
for w in wordList:
for i in range(L):
patterns[w[:i] + '*' + w[i+1:]].append(w)
visited = {beginWord}
q = deque([(beginWord, 1)])
while q:
word, level = q.popleft()
for i in range(L):
p = word[:i] + '*' + word[i+1:]
for nei in patterns[p]:
if nei == endWord: return level + 1
if nei not in visited:
visited.add(nei)
q.append((nei, level + 1))
patterns[p] = [] # tối ưu: mỗi pattern dùng 1 lần
return 0
Time O(N × L²) với N = |wordList|, L = |word|.
Đề (LeetCode #1091): Lưới 0/1, 0 là đi được, 1 là tường. Tìm đường ngắn nhất từ (0,0) đến (n-1, n-1) qua 8 hướng.
def shortestPathBinaryMatrix(grid):
n = len(grid)
if grid[0][0] != 0 or grid[n-1][n-1] != 0: return -1
DIRS = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)] # 8 hướng
q = deque([(0, 0, 1)])
visited = {(0, 0)}
while q:
r, c, d = q.popleft()
if (r, c) == (n-1, n-1): return d
for dr, dc in DIRS:
nr, nc = r+dr, c+dc
if (0 <= nr < n and 0 <= nc < n
and grid[nr][nc] == 0 and (nr, nc) not in visited):
visited.add((nr, nc))
q.append((nr, nc, d + 1))
return -1
Time O(n²).
"h*t" để làm gì?def count_components(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
count = 0
def dfs(u):
if u in visited: return
visited.add(u)
for v in graph[u]: dfs(v)
for u in range(n):
if u not in visited:
dfs(u)
count += 1
return count
Time O(V + E).
Cùng tinh thần Number of Islands: lặp qua mọi node chưa visited, mỗi DFS "phủ" 1 component, tăng count.
Đề (LeetCode #323): Cho n nodes (0..n-1) và edges. Trả số connected components.
Đây chính là template ở 4.1. Đáng chú ý là có thể giải bằng Union-Find (Phần 5):
def countComponents(n, edges):
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]] # path compression
x = parent[x]
return x
def union(a, b):
ra, rb = find(a), find(b)
if ra != rb:
parent[ra] = rb
return True
return False
count = n
for u, v in edges:
if union(u, v):
count -= 1
return count
Time O(E × α(n)) với α = hàm Ackermann ngược (gần như hằng số). So với DFS O(V+E), Union-Find tốt hơn khi có nhiều query incrementally.
Ứng dụng: build dependency, course prerequisites, task scheduling.
def topo_dfs(graph, n):
visited = [0] * n # 0=chưa, 1=đang trong stack, 2=xong
order = []
def dfs(u):
if visited[u] == 1: raise Exception("Cycle detected")
if visited[u] == 2: return
visited[u] = 1
for v in graph[u]: dfs(v)
visited[u] = 2
order.append(u) # thêm KHI XONG (post-order)
for u in range(n):
if visited[u] == 0: dfs(u)
return order[::-1] # đảo lại
3-color marking: 0 = chưa thăm, 1 = đang trong stack hiện tại (gặp lại = chu kỳ), 2 = đã xử lý xong.
Ý tưởng: bắt đầu từ các node có in-degree = 0, "tháo gỡ" từng node và giảm in-degree của neighbors.
def topo_kahn(graph, n):
in_deg = [0] * n
for u in range(n):
for v in graph[u]:
in_deg[v] += 1
q = deque([u for u in range(n) if in_deg[u] == 0])
order = []
while q:
u = q.popleft()
order.append(u)
for v in graph[u]:
in_deg[v] -= 1
if in_deg[v] == 0:
q.append(v)
if len(order) != n: return [] # có chu kỳ
return order
| Aspect | DFS | Kahn's (BFS) |
|---|---|---|
| Code | Recursive ngắn | Iterative dài hơn |
| Cycle detection | 3-color trong process | Check len(order) cuối |
| Output thứ tự | Cần đảo | Tự nhiên |
| Phỏng vấn ưu tiên | — | Thường ưu tiên Kahn's |
Đề: n khoá học (0..n-1), prerequisites[i] = [a, b] nghĩa là phải học b trước a. Trả True nếu có thể học hết.
Bài này = "DAG có chu kỳ không?". Dùng Kahn's algorithm: nếu có chu kỳ, một số node sẽ không bao giờ có in-degree = 0 → order ngắn hơn n.
def canFinish(numCourses, prerequisites):
graph = defaultdict(list)
in_deg = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a) # b → a (học b trước)
in_deg[a] += 1
q = deque([u for u in range(numCourses) if in_deg[u] == 0])
completed = 0
while q:
u = q.popleft()
completed += 1
for v in graph[u]:
in_deg[v] -= 1
if in_deg[v] == 0:
q.append(v)
return completed == numCourses
Time O(V + E).
Đề: Tương tự #207 nhưng trả về thứ tự học hợp lệ. [] nếu không thể.
Đây chính là Topological Sort. Sửa nhỏ #207:
def findOrder(numCourses, prerequisites):
graph = defaultdict(list)
in_deg = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a)
in_deg[a] += 1
q = deque([u for u in range(numCourses) if in_deg[u] == 0])
order = []
while q:
u = q.popleft()
order.append(u)
for v in graph[u]:
in_deg[v] -= 1
if in_deg[v] == 0:
q.append(v)
return order if len(order) == numCourses else []
Cấu trúc cho 2 thao tác chính:
find(x) — trả root của tập chứa x.union(a, b) — gộp tập chứa a và b.class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb: return False
if self.rank[ra] < self.rank[rb]: ra, rb = rb, ra
self.parent[rb] = ra
if self.rank[ra] == self.rank[rb]: self.rank[ra] += 1
return True
Time O(α(n)) per op (gần như hằng số). Path compression + union by rank đảm bảo độ phức tạp này.
Đề (LeetCode #547): Ma trận isConnected[i][j] = 1 nếu thành phố i và j liên kết trực tiếp. Đếm số "tỉnh" (cluster các thành phố liên thông).
def findCircleNum(isConnected):
n = len(isConnected)
dsu = DSU(n)
for i in range(n):
for j in range(i+1, n):
if isConnected[i][j] == 1:
dsu.union(i, j)
return sum(1 for i in range(n) if dsu.find(i) == i)
Time O(n² × α(n)) — chi phối bởi việc duyệt ma trận.
Cho graph weighted với cạnh không âm. Tìm shortest path từ source.
BFS không hoạt động vì cạnh có trọng số khác nhau. Thay queue bằng min-heap — luôn xử lý node có distance nhỏ nhất hiện tại trước.
import heapq
def dijkstra(graph, source, n):
dist = [float('inf')] * n
dist[source] = 0
h = [(0, source)]
while h:
d, u = heapq.heappop(h)
if d > dist[u]: continue # outdated entry
for v, w in graph[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(h, (nd, v))
return dist
Time O((V + E) log V).
| Tín hiệu trong đề | Pattern |
|---|---|
| "Đếm đảo / cụm trên grid" | DFS/BFS in-place mark |
| "Diện tích / chu vi đảo lớn nhất" | DFS bottom-up trả về size |
| "Khoảng cách ngắn nhất từ tập sources" | Multi-source BFS |
| "Shortest path unweighted" | BFS đơn nguồn |
| "Shortest path weighted (≥ 0)" | Dijkstra (BFS + min-heap) |
| "Word Ladder, transform 1 ký tự" | BFS + generic pattern |
| "Đếm components" | DFS hoặc Union-Find |
| "Course schedule / dependency / order" | Topological Sort (Kahn's) |
| "Có chu kỳ trong directed graph?" | DFS 3-color hoặc Kahn's check len |
| "Edges đến dạng stream / nhiều query union" | Union-Find |
| "Bipartite graph?" | BFS/DFS với 2-color |
Backtracking — Permutations, Combinations, Subsets, N-Queens
Pattern then chốt cho bài "liệt kê mọi khả năng". Học template "chọn — đệ quy — bỏ chọn", áp dụng vào permutations, combinations, subsets. Bài "boss": N-Queens, Word Search, Sudoku Solver. Quan trọng: kỹ thuật pruning để giảm time exponential. Chuẩn bị cho Tuần 12 — DP.
Nhấn T hoặc Esc để đóng