Algorithms: Graph & Tree
1. Representations
Adjacency List (sparse graph — thường dùng):
graph[0] = [1, 2]
graph[1] = [3]
graph[2] = [3, 4]
Adjacency Matrix (dense graph):
0 1 2 3 4
0 [ 0 1 1 0 0 ]
1 [ 0 0 0 1 0 ]
2 [ 0 0 0 1 1 ]
2. BFS Template
Dùng khi: shortest path (unweighted), level-order traversal, minimum steps.
func bfs(graph [][]int, start int) []int {
visited := make([]bool, len(graph))
queue := []int{start}
visited[start] = true
order := []int{}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
order = append(order, node)
for _, neighbor := range graph[node] {
if !visited[neighbor] {
visited[neighbor] = true
queue = append(queue, neighbor)
}
}
}
return order
}
// BFS với distance tracking
func shortestPath(graph [][]int, start, end int) int {
visited := make([]bool, len(graph))
queue := []int{start}
visited[start] = true
dist := 0
for len(queue) > 0 {
size := len(queue) // process level by level
for i := 0; i < size; i++ {
node := queue[0]
queue = queue[1:]
if node == end { return dist }
for _, nb := range graph[node] {
if !visited[nb] {
visited[nb] = true
queue = append(queue, nb)
}
}
}
dist++
}
return -1 // not reachable
}
3. DFS Template
Dùng khi: cycle detection, connected components, path finding, topological sort.
// Iterative DFS
func dfsIterative(graph [][]int, start int) {
visited := make([]bool, len(graph))
stack := []int{start}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if visited[node] { continue }
visited[node] = true
for _, nb := range graph[node] {
if !visited[nb] {
stack = append(stack, nb)
}
}
}
}
// Recursive DFS
func dfs(graph [][]int, node int, visited []bool) {
visited[node] = true
for _, nb := range graph[node] {
if !visited[nb] {
dfs(graph, nb, visited)
}
}
}
4. Topological Sort
Dùng khi: dependency ordering, course schedule, build order.
Điều kiện: DAG (Directed Acyclic Graph). Nếu có cycle → không có topological order.
Kahn's Algorithm (BFS-based)
func topoSort(numNodes int, edges [][]int) []int {
inDegree := make([]int, numNodes)
adj := make([][]int, numNodes)
for _, e := range edges {
adj[e[0]] = append(adj[e[0]], e[1])
inDegree[e[1]]++
}
queue := []int{}
for i := 0; i < numNodes; i++ {
if inDegree[i] == 0 {
queue = append(queue, i)
}
}
result := []int{}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
for _, nb := range adj[node] {
inDegree[nb]--
if inDegree[nb] == 0 {
queue = append(queue, nb)
}
}
}
if len(result) != numNodes {
return nil // cycle detected
}
return result
}
DFS-based Topological Sort
func topoSortDFS(numNodes int, adj [][]int) []int {
visited := make([]int, numNodes) // 0=unvisited, 1=visiting, 2=done
result := []int{}
hasCycle := false
var dfs func(node int)
dfs = func(node int) {
if hasCycle { return }
visited[node] = 1 // mark as visiting
for _, nb := range adj[node] {
if visited[nb] == 1 {
hasCycle = true
return
}
if visited[nb] == 0 {
dfs(nb)
}
}
visited[node] = 2
result = append([]int{node}, result...) // prepend
}
for i := 0; i < numNodes; i++ {
if visited[i] == 0 {
dfs(i)
}
}
if hasCycle { return nil }
return result
}
5. Union-Find (Disjoint Set Union)
Dùng khi: connected components, cycle detection in undirected graph, Kruskal's MST.
type UnionFind struct {
parent []int
rank []int
count int // number of connected components
}
func NewUF(n int) *UnionFind {
uf := &UnionFind{
parent: make([]int, n),
rank: make([]int, n),
count: n,
}
for i := range uf.parent {
uf.parent[i] = i
}
return uf
}
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x]) // path compression
}
return uf.parent[x]
}
func (uf *UnionFind) Union(x, y int) bool {
px, py := uf.Find(x), uf.Find(y)
if px == py { return false } // already connected → cycle!
// Union by rank
if uf.rank[px] < uf.rank[py] {
px, py = py, px
}
uf.parent[py] = px
if uf.rank[px] == uf.rank[py] {
uf.rank[px]++
}
uf.count--
return true
}
Complexity: O(α(n)) ≈ O(1) per operation (Inverse Ackermann function)
6. Shortest Path
Dijkstra (Non-negative weights)
Graph: A→B(1), A→C(4), B→C(2), B→D(5), C→D(1)
Dijkstra từ A:
dist = {A:0, B:∞, C:∞, D:∞}
Process A: dist[B]=1, dist[C]=4
Process B: dist[C]=min(4,3)=3, dist[D]=6
Process C: dist[D]=min(6,4)=4
Process D: done
Result: {A:0, B:1, C:3, D:4}
func dijkstra(graph [][]int, weights [][]int, src int) []int {
n := len(graph)
dist := make([]int, n)
for i := range dist { dist[i] = math.MaxInt32 }
dist[src] = 0
// Min-heap: [dist, node]
pq := &MinHeap{{0, src}}
heap.Init(pq)
for pq.Len() > 0 {
curr := heap.Pop(pq).([2]int)
d, u := curr[0], curr[1]
if d > dist[u] { continue } // stale entry
for i, v := range graph[u] {
newDist := dist[u] + weights[u][i]
if newDist < dist[v] {
dist[v] = newDist
heap.Push(pq, [2]int{newDist, v})
}
}
}
return dist
}
Complexity: O((V + E) log V)
Bellman-Ford (Negative weights)
func bellmanFord(n int, edges [][]int, src int) []int {
dist := make([]int, n)
for i := range dist { dist[i] = math.MaxInt32 }
dist[src] = 0
// Relax edges n-1 times
for i := 0; i < n-1; i++ {
for _, e := range edges {
u, v, w := e[0], e[1], e[2]
if dist[u] != math.MaxInt32 && dist[u]+w < dist[v] {
dist[v] = dist[u] + w
}
}
}
// Check negative cycle
for _, e := range edges {
u, v, w := e[0], e[1], e[2]
if dist[u] != math.MaxInt32 && dist[u]+w < dist[v] {
return nil // negative cycle exists
}
}
return dist
}
Complexity: O(VE) — chậm hơn Dijkstra nhưng handle negative weights
7. Tree traversals
// In-order (Left → Root → Right) — sorted order for BST
func inorder(root *TreeNode) []int {
if root == nil { return nil }
result := inorder(root.Left)
result = append(result, root.Val)
result = append(result, inorder(root.Right)...)
return result
}
// Level-order — BFS
func levelOrder(root *TreeNode) [][]int {
if root == nil { return nil }
result := [][]int{}
queue := []*TreeNode{root}
for len(queue) > 0 {
level := []int{}
size := len(queue)
for i := 0; i < size; i++ {
node := queue[0]; queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil { queue = append(queue, node.Left) }
if node.Right != nil { queue = append(queue, node.Right) }
}
result = append(result, level)
}
return result
}
8. Khi nào dùng gì?
| Bài toán | Algorithm |
|---|---|
| Shortest path, unweighted | BFS |
| Shortest path, non-negative weights | Dijkstra |
| Shortest path, negative weights | Bellman-Ford |
| All-pairs shortest path | Floyd-Warshall O(V³) |
| Dependency ordering | Topological Sort (Kahn's) |
| Connected components | Union-Find hoặc DFS |
| Cycle detection (undirected) | Union-Find |
| Cycle detection (directed) | DFS với 3-state visited |
| Minimum Spanning Tree | Kruskal (Union-Find) hoặc Prim |
💡 Interview: Khi gặp grid problem (islands, walls, paths), model nó thành graph với mỗi cell là node. BFS cho shortest path trong grid. DFS cho flood fill / connected components. "Number of Islands" = đếm số lần DFS được gọi từ unvisited land cell.