Buổi 08 — Weighted Graphs (Dijkstra & Bellman-Ford)
Navigation
← 07-Union-Find | → 09-Dynamic-Programming Giai đoạn: FAANG Mastery | Độ khó: ⭐⭐⭐⭐
1. Analogy & Context
Dijkstra = GPS điều hướng theo giao thông thực tế
Hình dung bạn là một GPS navigator. Bạn muốn tìm đường rẻ nhất từ nhà đến sân bay. Cách tiếp cận của Dijkstra: luôn khám phá thành phố “rẻ nhất để đến” tiếp theo. Giống như một du khách thận trọng luôn chọn chuyến bay giá rẻ nhất có thể book ngay lúc đó.
Một khi đã “finalize” một thành phố (đặt vé xong), bạn không bao giờ quay lại — vì không thể rẻ hơn được nữa (giả sử giá vé luôn dương).
Bellman-Ford = Du khách hoang mang, kiểm tra lại MỌI tuyến đường N-1 lần
Bellman-Ford hoạt động như người mắc OCD: mỗi “round”, kiểm tra lại TẤT CẢ các chuyến bay, xem có đường nào rẻ hơn không. Sau N-1 vòng, chắc chắn đã tìm ra đường rẻ nhất.
Điểm mạnh: chịu được negative edges (discount coupons làm giảm chi phí) và phát hiện negative cycle (chu trình có thể giảm cost vô hạn — không có shortest path).
Khi nào dùng thuật toán nào?
| Tình huống | Thuật toán |
|---|---|
| Tất cả edge weight ≥ 0 | Dijkstra (nhanh hơn) |
| Có negative weights | Bellman-Ford |
| Phát hiện negative cycle | Bellman-Ford (round thứ N) |
| All-pairs shortest path | Floyd-Warshall |
| Unweighted graph | BFS () |
| Dense graph, small V | Floyd-Warshall |
2. The FAANG Standard
Weighted Graph trong phỏng vấn FAANG L5+
- Google Maps team: Network delay optimization, route planning
- Amazon Logistics: Warehouse routing, delivery cost minimization
- Uber/Lyft: Surge pricing paths, driver dispatch
- Finance: Currency arbitrage detection (negative cycle = arbitrage opportunity!)
Taxonomy bài toán graph có trọng số:
Shortest path từ single source:
├── Non-negative weights → Dijkstra O(E log V)
├── Negative weights → Bellman-Ford O(VE)
└── DAG (directed acyclic) → Topological Sort + Relax O(V+E)
Shortest path all-pairs:
└── Floyd-Warshall O(V³)
Minimum Spanning Tree:
├── Kruskal (sort edges) → Union-Find O(E log E)
└── Prim (grow from node) → Priority Queue O(E log V)
Bridge/Articulation:
└── Tarjan's Algorithm O(V+E)
Priority Queue trong TypeScript
JavaScript không có built-in min-heap. Trong interview, thường được chấp nhận implement MinHeap đơn giản hoặc dùng sorted array (kém hiệu quả hơn nhưng OK nếu nói rõ). Một số interviewer cho phép assume có sẵn priority queue.
3. Visual Thinking (Mermaid)
3.1 Dijkstra — Step-by-step trên example graph
graph LR A((A,0)) -->|4| B((B,4)) A -->|2| C((C,2)) C -->|1| B B -->|3| D((D,7)) C -->|5| D style A fill:#4CAF50,color:#fff style C fill:#FF9800,color:#fff
Bảng distances sau mỗi bước:
| Step | Process | A | B | C | D |
|---|---|---|---|---|---|
| Init | — | 0 | ∞ | ∞ | ∞ |
| 1 | Process A (dist=0) | 0 | 4 | 2 | ∞ |
| 2 | Process C (dist=2) | 0 | 3 | 2 | 7 |
| 3 | Process B (dist=3) | 0 | 3 | 2 | 6 |
| 4 | Process D (dist=6) | 0 | 3 | 2 | 6 |
3.2 Bellman-Ford — Relaxation Rounds
graph LR S((S,0)) -->|"-1"| A((A)) S -->|4| B((B)) A -->|3| B B -->|"-2"| C((C)) A -->|2| C style S fill:#4CAF50,color:#fff
Round 0 (init): S=0, A=∞, B=∞, C=∞
Round 1 (relax all edges):
S→A: A = min(∞, 0 + (-1)) = -1
S→B: B = min(∞, 0 + 4) = 4
A→B: B = min(4, -1 + 3) = 2
B→C: C = min(∞, 2 + (-2)) = 0
A→C: C = min(0, -1 + 2) = 0 (no change)
Round 2: No changes → DONE (converged early)
Final: S=0, A=-1, B=2, C=0
3.3 Negative Cycle Detection
graph LR X((X)) -->|1| Y((Y)) Y -->|"-3"| Z((Z)) Z -->|1| X Note["Cycle weight: 1 + (-3) + 1 = -1 < 0<br/>NEGATIVE CYCLE!"] style Note fill:#FF5252,color:#fff
Negative cycle = vô hạn vòng lặp
Nếu có negative cycle, “shortest path” không tồn tại — bạn có thể đi vòng mãi mãi để giảm cost. Bellman-Ford phát hiện bằng cách: nếu Round thứ V vẫn còn relaxation → negative cycle tồn tại.
4. TypeScript Template
4.1 Graph Representation — Adjacency List với Weights
// Graph type: adjacency list, mỗi entry = [neighbor, weight]
type WeightedGraph = Map<number, [number, number][]>;
function buildGraph(n: number, edges: number[][]): WeightedGraph {
const graph: WeightedGraph = new Map();
for (let i = 0; i < n; i++) graph.set(i, []);
for (const [u, v, w] of edges) {
graph.get(u)!.push([v, w]);
graph.get(v)!.push([u, w]); // undirected: add both directions
}
return graph;
}4.2 Dijkstra — Min-Heap Priority Queue
// MinHeap implementation (đơn giản cho interview)
class MinHeap {
private heap: [number, number][] = []; // [distance, node]
push(item: [number, number]): void {
this.heap.push(item);
this._bubbleUp(this.heap.length - 1);
}
pop(): [number, number] | undefined {
if (this.heap.length === 0) return undefined;
const top = this.heap[0];
const last = this.heap.pop()!;
if (this.heap.length > 0) {
this.heap[0] = last;
this._sinkDown(0);
}
return top;
}
get size(): number { return this.heap.length; }
private _bubbleUp(i: number): void {
while (i > 0) {
const parent = Math.floor((i - 1) / 2);
if (this.heap[parent][0] <= this.heap[i][0]) break;
[this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
i = parent;
}
}
private _sinkDown(i: number): void {
const n = this.heap.length;
while (true) {
let smallest = i;
const l = 2 * i + 1, r = 2 * i + 2;
if (l < n && this.heap[l][0] < this.heap[smallest][0]) smallest = l;
if (r < n && this.heap[r][0] < this.heap[smallest][0]) smallest = r;
if (smallest === i) break;
[this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
i = smallest;
}
}
}
// Dijkstra — O(E log V)
function dijkstra(graph: WeightedGraph, start: number, n: number): number[] {
const dist = new Array(n).fill(Infinity);
dist[start] = 0;
const pq = new MinHeap();
pq.push([0, start]); // [distance, node]
while (pq.size > 0) {
const [d, u] = pq.pop()!;
// Stale entry check: if we've found a better path already, skip
if (d > dist[u]) continue;
for (const [v, w] of (graph.get(u) ?? [])) {
const newDist = dist[u] + w;
if (newDist < dist[v]) {
dist[v] = newDist;
pq.push([newDist, v]);
}
}
}
return dist;
}4.3 Bellman-Ford — Handles Negative Weights
// Bellman-Ford — O(VE)
// Returns distances array, or null if negative cycle detected
function bellmanFord(
n: number,
edges: [number, number, number][], // [u, v, weight]
start: number
): number[] | null {
const dist = new Array(n).fill(Infinity);
dist[start] = 0;
// Relax ALL edges exactly V-1 times (not V!)
for (let round = 0; round < n - 1; round++) {
let updated = false;
for (const [u, v, w] of edges) {
if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
updated = true;
}
}
if (!updated) break; // early termination optimization
}
// Round V: if any edge can still be relaxed → negative cycle
for (const [u, v, w] of edges) {
if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
return null; // NEGATIVE CYCLE DETECTED
}
}
return dist;
}4.4 Network Delay Time — Dijkstra Application
// LeetCode #743 — Network Delay Time
// Tìm thời gian để signal reach tất cả nodes từ source k
function networkDelayTime(times: number[][], n: number, k: number): number {
// Build graph
const graph = new Map<number, [number, number][]>();
for (let i = 1; i <= n; i++) graph.set(i, []);
for (const [u, v, w] of times) graph.get(u)!.push([v, w]);
// Dijkstra from k
const dist = new Array(n + 1).fill(Infinity);
dist[k] = 0;
const pq = new MinHeap();
pq.push([0, k]);
while (pq.size > 0) {
const [d, u] = pq.pop()!;
if (d > dist[u]) continue;
for (const [v, w] of graph.get(u)!) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push([dist[v], v]);
}
}
}
// Max distance among all nodes (excluding index 0 which is unused)
const maxDist = Math.max(...dist.slice(1));
return maxDist === Infinity ? -1 : maxDist;
}4.5 Cheapest Flights Within K Stops — Bellman-Ford Variant
// LeetCode #787 — Full solution in Section 84.6 Critical Connections — Tarjan’s Bridge Finding
// LeetCode #1192 — Find Critical Connections in a Network
// Bridge = edge mà nếu xóa đi, graph bị ngắt kết nối
function criticalConnections(n: number, connections: number[][]): number[][] {
const graph = new Map<number, number[]>();
for (let i = 0; i < n; i++) graph.set(i, []);
for (const [u, v] of connections) {
graph.get(u)!.push(v);
graph.get(v)!.push(u);
}
const disc = new Array(n).fill(-1); // discovery time
const low = new Array(n).fill(-1); // lowest reachable disc time
const result: number[][] = [];
let timer = 0;
function dfs(node: number, parent: number): void {
disc[node] = low[node] = timer++;
for (const neighbor of graph.get(node)!) {
if (neighbor === parent) continue; // don't go back to parent
if (disc[neighbor] === -1) {
// Tree edge: unvisited neighbor
dfs(neighbor, node);
low[node] = Math.min(low[node], low[neighbor]);
// Bridge condition: low[neighbor] > disc[node]
// means neighbor cannot reach node or earlier without this edge
if (low[neighbor] > disc[node]) {
result.push([node, neighbor]);
}
} else {
// Back edge: update low with discovered time of neighbor
low[node] = Math.min(low[node], disc[neighbor]);
}
}
}
dfs(0, -1);
return result;
}5. Complexity Deep-dive
Khi nào dùng thuật toán nào — Decision framework
Dijkstra:
- Binary heap:
- Fibonacci heap (theoretical):
- Điều kiện: TẤT CẢ edge weight phải ≥ 0
- Best for: Dense graphs where E >> V
Bellman-Ford:
- Simple but slower
- Điều kiện: Chịu được negative weights
- Detect negative cycle:
- Relaxation rounds: chính xác (không phải V)
Floyd-Warshall: , Space
function floydWarshall(dist: number[][]): number[][] {
const n = dist.length;
// For each intermediate node k
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
return dist;
}- Good for: V ≤ 500, all-pairs shortest path
- Cũng detect negative cycle: nếu
dist[i][i] < 0sau khi chạy
Comparison table:
| Dijkstra | Bellman-Ford | Floyd-Warshall | |
|---|---|---|---|
| Time | |||
| Space | |||
| Negative weights | NO | YES | YES |
| Negative cycle | Cannot detect | YES | YES |
| Single source | YES | YES | All-pairs |
| Dense graph | OK | Slow | OK if V small |
6. Edge Cases & Pitfalls
Dijkstra và negative edges — counterexample
Tại sao Dijkstra FAIL với negative edges?
Graph: A --(1)--> B --((-3))--> C
A -----(2)-----------> C
Dijkstra từ A:
dist = [A:0, B:∞, C:∞]
Process A: dist[B] = 1, dist[C] = 2
Process B (dist=1): dist[C] = min(2, 1+(-3)) = -2 ???
VẤN ĐỀ: C đã bị "finalized" với dist=2 trước khi B được process!
Dijkstra greedy assumption (once finalized, always optimal) bị vi phạm.
Lỗi phổ biến với khoảng cách infinity
// BUG: Dùng Number.MAX_SAFE_INTEGER gây overflow khi cộng weight
const dist = new Array(n).fill(Number.MAX_SAFE_INTEGER);
// dist[u] + w có thể overflow (wrap around thành số âm)!
// FIX: Dùng Infinity — không overflow trong JavaScript
const dist = new Array(n).fill(Infinity);
// Infinity + 5 = Infinity (safe)
// Infinity > 100 = true (safe comparison)Stale entries trong Priority Queue
// Dijkstra PQ có thể chứa stale entries (cũ hơn)
// Khi pop [d, u], có thể d > dist[u] (đã tìm đường tốt hơn từ trước)
// PHẢI check và skip:
const [d, u] = pq.pop()!;
if (d > dist[u]) continue; // CRITICAL: skip stale entry
// Không có dòng này → revisit nodes, O(E) thay vì O(E log V)Bellman-Ford edge cases:
// BUG: Relax V lần thay vì V-1 lần
for (let round = 0; round < n; round++) { ... } // WRONG
// FIX: Chính xác n-1 lần để shortest path (V nodes → max V-1 edges)
for (let round = 0; round < n - 1; round++) { ... }
// NOTE: Sau n-1 rounds, dùng round thứ n để DETECT negative cycle
// Đừng nhầm: detect bằng round thứ n, không phải V-1Directed vs Undirected:
// Directed graph: chỉ add one direction
graph.get(u)!.push([v, w]);
// Undirected graph: add cả hai chiều
graph.get(u)!.push([v, w]);
graph.get(v)!.push([u, w]); // MUST add this for undirectedDisconnected graph:
const maxDist = Math.max(...dist.slice(1));
// Nếu maxDist === Infinity → có node không reachable
return maxDist === Infinity ? -1 : maxDist;7. Pattern Recognition
Nhận diện bài Weighted Graph trong 30 giây
Keywords → Dijkstra:
- “shortest path” + “non-negative weights”
- “minimum cost/time to reach from source”
- “network delay”, “signal propagation time”
- “path with minimum maximum weight” (binary search + Dijkstra)
- “minimum effort” (BFS/Dijkstra hybrid)
Keywords → Bellman-Ford:
- “shortest path” + “negative weights possible”
- “at most K edges/stops” (K-limited Bellman-Ford)
- “detect negative cycle” / “currency arbitrage”
- “minimum cost flights with at most K stops”
Keywords → Floyd-Warshall:
- “all pairs shortest path”
- “find city with smallest number of neighbors within threshold”
- Graph nhỏ (V ≤ 500) + need all-pairs distances
Keywords → Tarjan’s Bridge:
- “critical connections”, “bridges in network”
- “if we remove this connection, network splits”
- “single points of failure”
Modified Dijkstra patterns
- K stops limit: Dùng state
(node, stops_remaining)— thêm dimension vào Dijkstra- Min-max path: Binary search trên answer + BFS/Dijkstra để check feasibility
- Multiple sources: Add virtual source node với weight-0 edges to all real sources
8. Top LeetCode Problems
| # | Tên bài | Độ khó | Algorithm |
|---|---|---|---|
| #743 | Network Delay Time | Medium | Dijkstra |
| #787 | Cheapest Flights K Stops | Medium | Bellman-Ford |
| #1631 | Path With Minimum Effort | Medium | Binary Search + Dijkstra |
| #1334 | City Smallest Threshold | Medium | Floyd-Warshall |
| #1192 | Critical Connections | Hard | Tarjan’s Bridge |
| #332 | Reconstruct Itinerary | Hard | Eulerian Path (Hierholzer) |
| #1168 | Water Distribution | Hard | MST (Kruskal) |
| #778 | Swim in Rising Water | Hard | Binary Search + BFS |
Complete Solution: #787 Cheapest Flights Within K Stops (Bellman-Ford Variant)
Bài #787 là bài Bellman-Ford quan trọng nhất. Modified Bellman-Ford với giới hạn K stops.
Key insight: Standard Bellman-Ford chạy V-1 rounds (shortest path dùng tối đa V-1 edges). Bài này giới hạn tối đa K stops = K+1 edges → chạy đúng K+1 rounds.
// LeetCode #787 — Cheapest Flights Within K Stops
// Time: O(K * E) where E = number of flights
// Space: O(N) for distance arrays
function findCheapestPrice(
n: number,
flights: number[][], // [from, to, price]
src: number,
dst: number,
k: number
): number {
// dist[i] = minimum cost to reach city i with at most (round) edges used
let dist = new Array(n).fill(Infinity);
dist[src] = 0;
// Run K+1 rounds (K stops = K+1 edges)
for (let round = 0; round <= k; round++) {
// CRITICAL: Use a COPY of current dist to avoid using updates from same round
// Without this, one flight might use updates from the same round (more than 1 edge per round)
const tempDist = [...dist];
for (const [from, to, price] of flights) {
if (dist[from] !== Infinity && dist[from] + price < tempDist[to]) {
tempDist[to] = dist[from] + price;
}
}
dist = tempDist;
}
return dist[dst] === Infinity ? -1 : dist[dst];
}
// Test case 1
console.log(findCheapestPrice(
4,
[[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]],
0, 3, 1
)); // Expected: 700 (0→1→3 = 700)
// Test case 2
console.log(findCheapestPrice(
3,
[[0,1,100],[1,2,100],[0,2,500]],
0, 2, 1
)); // Expected: 200 (0→1→2 = 200)
// Test case 3 — tại sao cần tempDist
console.log(findCheapestPrice(
4,
[[0,1,1],[0,2,5],[1,2,1],[2,3,1]],
0, 3, 1
)); // Expected: 6 (0→2→3, 1 stop), NOT 3 (0→1→2→3, 2 stops)Tại sao phải dùng
tempDist(copy)?Nếu update
disttrực tiếp trong cùng một round:
- Round 1, process flight [0→1]:
dist[1] = 0 + 1 = 1- Cùng round 1, process flight [1→2]:
dist[2] = dist[1] + 1 = 2- Cùng round 1, process flight [2→3]:
dist[3] = dist[2] + 1 = 3Kết quả sai! Chỉ có 1 round nhưng đã dùng 3 edges (qua 2 stops thêm chứ không phải K=1).
Với
tempDist: luôn relax từdist(trạng thái đầu round), write vàotempDist. Đảm bảo mỗi round chỉ dùng thêm đúng 1 edge.
Alternative: Dijkstra với state (node, stops_remaining)
// Modified Dijkstra — O(E log(V*K)) — tốt hơn với dense graphs
function findCheapestPriceDijkstra(
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]);
// State: [cost, node, stopsRemaining]
// dist[node][stops] = min cost to reach node with stops remaining
const dist: number[][] = Array.from({length: n}, () => new Array(k + 2).fill(Infinity));
dist[src][k + 1] = 0;
// Min heap: [cost, node, stopsRemaining]
// CHÚ Ý: MinHeap class ở § 4.2 được định nghĩa cho [number, number].
// Để dùng cho tuple 3 phần tử, generalize thành number[] (key là phần tử đầu).
// Dùng sort+shift LẶP TRONG VÒNG WHILE là O(N² log N) — sai complexity!
const pq = new MinHeapGeneric();
pq.push([0, src, k + 1]);
while (pq.size > 0) {
const [cost, node, stops] = pq.pop()!;
if (node === dst) return cost;
if (stops === 0) continue; // no more stops allowed
for (const [neighbor, price] of (graph.get(node) ?? [])) {
const newCost = cost + price;
if (newCost < dist[neighbor][stops - 1]) {
dist[neighbor][stops - 1] = newCost;
pq.push([newCost, neighbor, stops - 1]);
}
}
}
return -1;
}
// Generic min-heap — key = item[0]. Dùng cho tuple bất kỳ độ dài.
class MinHeapGeneric {
private heap: number[][] = [];
push(item: number[]): void {
this.heap.push(item);
this._bubbleUp(this.heap.length - 1);
}
pop(): number[] | undefined {
if (this.heap.length === 0) return undefined;
const top = this.heap[0];
const last = this.heap.pop()!;
if (this.heap.length > 0) { this.heap[0] = last; this._sinkDown(0); }
return top;
}
get size(): number { return this.heap.length; }
private _bubbleUp(i: number): void {
while (i > 0) {
const p = (i - 1) >> 1;
if (this.heap[p][0] <= this.heap[i][0]) break;
[this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
i = p;
}
}
private _sinkDown(i: number): void {
const n = this.heap.length;
while (true) {
const l = 2*i + 1, r = 2*i + 2;
let smallest = i;
if (l < n && this.heap[l][0] < this.heap[smallest][0]) smallest = l;
if (r < n && this.heap[r][0] < this.heap[smallest][0]) smallest = r;
if (smallest === i) break;
[this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
i = smallest;
}
}
}Bellman-Ford vs Dijkstra cho bài #787
- Bellman-Ford: — đơn giản hơn, đủ cho interview
- Modified Dijkstra: — tốt hơn khi K lớn, E nhỏ
- Trong interview: Bellman-Ford được ưu tiên vì code ngắn gọn, dễ giải thích. Đề xuất Dijkstra variant như optimization nếu có thêm thời gian.
9. Self-Assessment Checklist
Kiểm tra bản thân trước khi interview
- Giải thích tại sao Dijkstra FAIL với negative edges (có counterexample)
- Implement Dijkstra với MinHeap và stale-entry check trong < 10 phút
- Nhớ dùng
Infinitythay vìNumber.MAX_SAFE_INTEGER(tránh overflow) - Implement Bellman-Ford và giải thích tại sao chạy V-1 lần (không phải V)
- Phát hiện negative cycle: chạy round thứ V, nếu còn relax được → negative cycle
- Giải bài #787: tại sao cần
tempDistcopy trong mỗi round - Biết Floyd-Warshall: 3 vòng lặp lồng nhau, khởi tạo
dist[i][i] = 0,dist[i][j] = edge weight or Infinity - Biết khi nào dùng Dijkstra vs Bellman-Ford vs Floyd-Warshall
- Xử lý disconnected graph: kiểm tra
dist[dst] === Infinity→ return -1 - Tarjan’s bridge: giải thích
discvslowarray, điều kiệnlow[v] > disc[u]
Quick revision — 20 phút
Tập trung: Dijkstra cơ bản (#743 networkDelayTime), sau đó giải #787 bằng Bellman-Ford với tempDist copy. Đây là hai bài quan trọng nhất của chủ đề này.
Về Priority Queue trong interview thực tế
Nếu interviewer dùng JavaScript/TypeScript, họ biết không có built-in MinHeap. Bạn có thể: (1) implement nhanh MinHeap đơn giản, (2) dùng array sorted (nói rõ đây là O(N log N) per push, không optimal), hoặc (3) hỏi interviewer có thể assume có sẵn PQ không. Lựa chọn 3 thường được chấp nhận tại Google, Meta.