Bài 42: Bipartite Matching - Ghép cặp trên đồ thị hai phía¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: CP-Algorithms, USACO Guide
Bạn sẽ học được gì?¶
- Hiểu bài toán ghép cặp trên đồ thị hai phía (bipartite matching)
- Triển khai thuật toán Kuhn (Augmenting Path) với độ phức tạp \(O(VE)\)
- Triển khai thuật toán Hopcroft-Karp với độ phức tạp \(O(E\sqrt{V})\)
- Áp dụng định lý König để tìm tập đỉnh bao nhỏ nhất
- Giải bài toán phân công (Assignment Problem) với thuật toán Hungarian
- Nhận diện và giải các bài toán thực tế quy về bipartite matching
1. Bản chất vấn đề¶
Đồ thị hai phía (Bipartite Graph)¶
Đồ thị hai phía là đồ thị mà tập đỉnh có thể chia thành hai tập \(L\) và \(R\) sao cho mọi cạnh đều nối một đỉnh trong \(L\) với một đỉnh trong \(R\). Không có cạnh nào nối hai đỉnh cùng bên.
Ghép cặp (Matching)¶
Matching là một tập các cạnh sao cho không hai cạnh nào chia sẻ đỉnh chung (tức là mỗi đỉnh chỉ được ghép tối đa một lần).
Maximum Matching là matching có số cạnh lớn nhất.
Perfect Matching là matching mà mọi đỉnh đều thuộc đúng một cạnh trong matching.
Ẩn dụ: Ghép cặp học sinh với dự án¶
Giả sử có \(N\) học sinh và \(M\) dự án. Mỗi học sinh chỉ quan tâm đến một số dự án nhất định. Ta muốn gán mỗi học sinh vào đúng một dự án (mỗi dự án chỉ do một nhóm thực hiện) sao cho số học sinh được gán là lớn nhất.
Đây chính là bài toán Maximum Bipartite Matching: bên trái là học sinh, bên phải là dự án, cạnh thể hiện sự quan tâm.
Một maximum matching có thể là: Học sinh 1 ghép Dự án B, Học sinh 2 ghép Dự án C, Học sinh 3 ghép Dự án A (ghép được 3 cặp).
Các thuật ngữ quan trọng¶
| Thuật ngữ | Ý nghĩa |
|---|---|
| Matching | Tập cạnh không chia sẻ đỉnh |
| Maximum Matching | Matching có nhiều cạnh nhất |
| Perfect Matching | Mọi đỉnh đều thuộc đúng một cạnh trong matching |
| Alternating Path | Đường đi xen kẽ cạnh trong và ngoài matching |
| Augmenting Path | Alternating path mà hai đầu đều là đỉnh chưa ghép |
2. Thuật toán Kuhn (Augmenting Path)¶
Bản chất vấn đề¶
Cho đồ thị hai phía có \(|L|\) đỉnh bên trái và \(|R|\) đỉnh bên phải, tìm maximum matching — tập cạnh kề đôi lớn nhất.
Tư duy cốt lõi¶
Định lý Berge: Một matching là maximum nếu và chỉ nếu không tồn tại augmenting path.
Vậy thuật toán rất đơn giản: lặp lại tìm augmenting path, nếu tìm được thì "đảo" trạng thái (cạnh trong matching ra ngoài, cạnh ngoài vào trong), tăng kích thước matching lên 1.
Trên đồ thị hai phía với matching \(M\):
- Alternating path: đường đi xen kẽ giữa cạnh thuộc \(M\) và cạnh không thuộc \(M\).
- Augmenting path: alternating path mà hai đầu đều là đỉnh chưa được ghép.
Khi tìm được augmenting path, ta "lật" trạng thái: tất cả cạnh trên path đổi từ trong matching ra ngoài và ngược lại. Kết quả là matching tăng kích thước đúng 1.
Pseudocode¶
Thuật toán Kuhn hoạt động như sau:
- Khởi tạo matching rỗng.
- Với mỗi đỉnh \(u\) bên trái (chưa ghép), chạy DFS tìm augmenting path bắt đầu từ \(u\).
- Hàm DFS(\(u\)): duyệt qua các đỉnh \(v\) kề \(u\). Nếu \(v\) chưa ghép hoặc DFS từ đỉnh đang ghép với \(v\) thành công thì ghép \(u\) với \(v\).
- Lặp lại cho đến khi duyệt hết đỉnh bên trái.
Trace từng bước¶
Ví dụ: 3 đỉnh bên trái \(\{1, 2, 3\}\), 3 đỉnh bên phải \(\{a, b, c\}\).
Cạnh: \(1\)-\(a\), \(1\)-\(b\), \(2\)-\(b\), \(2\)-\(c\), \(3\)-\(a\).
Bước 1: DFS(1) — thử ghép đỉnh 1.
- Thử \(a\): \(a\) chưa ghép → ghép \(1\)-\(a\). Matching = \(\{1\text{-}a\}\).
Bước 2: DFS(2) — thử ghép đỉnh 2.
- Thử \(b\): \(b\) chưa ghép → ghép \(2\)-\(b\). Matching = \(\{1\text{-}a, 2\text{-}b\}\).
Bước 3: DFS(3) — thử ghép đỉnh 3.
- Thử \(a\): \(a\) đã ghép với \(1\) → thử DFS(1).
- DFS(1): thử \(b\): \(b\) đã ghép với \(2\) → thử DFS(2).
- DFS(2): thử \(c\): \(c\) chưa ghép → ghép \(2\)-\(c\). Return true.
- Ghép \(1\)-\(b\). Return true.
- DFS(1): thử \(b\): \(b\) đã ghép với \(2\) → thử DFS(2).
- Ghép \(3\)-\(a\). Return true. Matching = \(\{3\text{-}a, 1\text{-}b, 2\text{-}c\}\).
Kết quả: maximum matching = 3 (perfect matching).
Phân tích tính đúng đắn¶
Bảo toàn tính hợp lệ: Mỗi bước augment chỉ đảo trạng thái các cạnh trên augmenting path. Vì augmenting path bắt đầu và kết thúc ở đỉnh chưa ghép, việc đảo không vi phạm tính chất mỗi đỉnh chỉ thuộc tối đa một cạnh.
Tính tối ưu: Theo định lý Berge, matching là maximum khi không còn augmenting path. Thuật toán dừng đúng khi DFS từ mọi đỉnh bên trái đều thất bại — tức là không còn augmenting path.
Tính dừng: Mỗi lần augment tăng kích thước matching đúng 1. Matching tối đa có \(|L|\) cạnh nên thuật toán dừng sau tối đa \(|L|\) lần augment.
Đánh giá độ phức tạp¶
- Thời gian: \(O(VE)\) — mỗi đỉnh bên trái gọi DFS một lần, mỗi DFS tối đa duyệt qua \(O(E)\) cạnh.
- Bộ nhớ: \(O(V + E)\) — lưu đồ thị và mảng visited.
Code¶
Heuristic tăng tốc cho Kuhn¶
- Xáo trộn thứ tự đỉnh: Tránh worst-case khi đỉnh được cho theo thứ tự xấu.
- Khởi tạo tham lam (Greedy init): Trước khi chạy Kuhn, duyệt qua tất cả cạnh và ghép ngay nếu có thể. Điều này giúp matching ban đầu gần maximum, giảm số lần DFS.
- Thứ tự ưu tiên đỉnh bậc nhỏ trước: Đỉnh bậc nhỏ khó tìm augmenting path hơn, nên xử lý trước.
3. Thuật toán Hopcroft-Karp¶
Bản chất vấn đề¶
Giống thuật toán Kuhn: tìm maximum bipartite matching, nhưng với độ phức tạp tốt hơn khi đồ thị lớn.
Tư duy cốt lõi¶
Kuhn tìm một augmenting path mỗi lần → \(O(VE)\). Hopcroft-Karp tìm đồng thời tất cả các augmenting path ngắn nhất bằng BFS, sau đó augment tất cả bằng DFS → chỉ cần \(O(\sqrt{V})\) phases.
Cấu trúc mỗi phase:
- BFS: Tính khoảng cách từ tất cả đỉnh chưa ghép bên trái. Chỉ đi theo alternating paths. Dừng khi gặp đỉnh chưa ghép bên phải. Mục đích: tìm tất cả augmenting paths có độ dài ngắn nhất.
- DFS: Từ mỗi đỉnh chưa ghép bên trái, chạy DFS chỉ đi qua các cạnh thỏa mãn khoảng cách BFS (\(dist[u] + 1 = dist[v]\)). Mỗi DFS tìm được một augmenting path và augment.
- Lặp lại cho đến khi không còn augmenting path.
Phân tích tính đúng đắn¶
Tính đúng của BFS: BFS xây dựng lớp khoảng cách sao cho mọi augmenting path ngắn nhất đều đi qua các cạnh thỏa mãn \(dist[u] + 1 = dist[v]\). Điều này đảm bảo DFS chỉ tìm đúng các augmenting path ngắn nhất.
Tối ưu theo phase: Sau mỗi phase, tất cả augmenting paths ngắn nhất đều được augment. Điều này đảm bảo độ dài augmenting path tăng dần qua các phase.
Số phase tối đa: Có tối đa \(2\sqrt{V} + 1\) phases. Bởi vì: - Sau \(\sqrt{V}\) phases, độ dài augmenting path tăng lên ít nhất \(\sqrt{V}\). - Đường đi ngắn nhất từ đỉnh chưa ghép bên trái sang đỉnh chưa ghép bên phải có độ dài tối đa \(2\sqrt{V} + 1\) (vì mỗi augmenting path tăng matching lên 1).
Đánh giá độ phức tạp¶
- Thời gian: \(O(E\sqrt{V})\) — mỗi phase BFS + DFS chạy \(O(E)\), có tối đa \(O(\sqrt{V})\) phases.
- Bộ nhớ: \(O(V + E)\) — lưu đồ thị, mảng matching, mảng khoảng cách.
Code¶
So sánh Kuhn và Hopcroft-Karp¶
| Tiêu chí | Kuhn | Hopcroft-Karp |
|---|---|---|
| Độ phức tạp | \(O(VE)\) | \(O(E\sqrt{V})\) |
| Độ khó code | Rất dễ | Trung bình |
| Khi nào dùng | \(V \leq 1000\), \(E \leq 10^4\) | \(V\) lớn, \(E\) lớn |
| Thực tế | Thường đủ cho contest | Cần khi \(V > 10^4\) |
Mẹo thi đấu
Trong contest, hãy ưu tiên Kuhn trước vì code ngắn, dễ debug. Chỉ chuyển sang Hopcroft-Karp khi Kuhn bị TLE.
4. Định lý König (Min Vertex Cover)¶
Bản chất vấn đề¶
Vertex cover là tập đỉnh sao cho mỗi cạnh đều có ít nhất một đầu mút nằm trong tập đó. Bài toán: tìm minimum vertex cover — tập đỉnh bao nhỏ nhất.
Tư duy cốt lõi¶
Định lý König: Trong đồ thị hai phía, kích thước maximum matching = kích thước minimum vertex cover.
Đây là kết quả lý thuyết mạnh mẽ: hai bài toán tưởng chừng khác nhau lại có lời giải liên hệ trực tiếp.
Phân tích tính đúng đắn¶
Sau khi chạy thuật toán matching, ta tìm min vertex cover bằng BFS trên alternating paths:
- Đánh dấu tất cả đỉnh chưa ghép bên trái.
- Chạy BFS từ các đỉnh đã đánh dấu theo alternating paths:
- Từ đỉnh bên trái: đi theo cạnh không trong matching sang bên phải.
- Từ đỉnh bên phải: đi theo cạnh trong matching sang bên trái.
- Min vertex cover = (đỉnh bên trái KHÔNG bị đánh dấu) \(\cup\) (đỉnh bên phải BỊ đánh dấu).
Trong ví dụ trên, đỉnh xanh lá nằm trong vertex cover, đỉnh đỏ không nằm trong.
Đánh giá độ phức tạp¶
- Thời gian: \(O(V + E)\) — BFS trên đồ thị đã có matching.
- Bộ nhớ: \(O(V + E)\).
Code¶
Ứng dụng: Maximum Independent Set¶
Trong đồ thị hai phía: Maximum Independent Set = Tổng số đỉnh \(-\) Maximum Matching.
Independent set là tập đỉnh sao cho không hai đỉnh nào kề nhau. Đây là bổ sung của vertex cover.
5. Thuật toán Hungarian (Bài toán phân công)¶
Bản chất vấn đề¶
Cho \(N\) người và \(N\) việc. Chi phí để người \(i\) làm việc \(j\) là \(cost[i][j]\). Tìm cách phân công mỗi người đúng một việc sao cho tổng chi phí nhỏ nhất.
Đây là bài toán Minimum Weight Perfect Matching trên đồ thị hai phía đầy đủ.
Tư duy cốt lõi¶
Thuật toán Hungarian (Kuhn-Munkres) hoạt động trên nguyên lý đối ngẫu:
- Duy trì nhãn (label) cho mỗi đỉnh: \(lx[u]\) cho bên trái, \(ly[v]\) cho bên phải.
- Biên \((u,v)\) là tight nếu \(lx[u] + ly[v] = cost[u][v]\).
- Chỉ xem xét đồ thị con gồm các cạnh tight. Nếu tìm được perfect matching trên đồ thị con này → đó là lời giải tối ưu.
- Nếu chưa có perfect matching, điều chỉnh nhãn để thêm cạnh tight mới.
Phân tích tính đúng đắn¶
Invariant: Luôn duy trì \(lx[u] + ly[v] \leq cost[u][v]\) cho mọi cạnh \((u,v)\). Mọi cạnh tight đều thỏa mãn đẳng thức.
Tối ưu: Khi tìm được perfect matching trên đồ thị con tight, tổng chi phí bằng \(\sum lx[u] + \sum ly[v]\). Vì mọi cạnh trong matching đều tight, đây là nghiệm tối ưu theo nguyên lý đối ngẫu tuyến tính.
Hội tụ: Mỗi lần điều chỉnh nhãn,至少 một cạnh mới trở thành tight, mở rộng đồ thị con. Sau tối đa \(N^2\) lần điều chỉnh, đồ thị con chứa perfect matching.
Đánh giá độ phức tạp¶
- Thời gian: \(O(N^3)\).
- Bộ nhớ: \(O(N^2)\).
Code¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
6. Ứng dụng¶
6.1. Phân công công việc (Job Assignment)¶
Bài toán cổ điển: \(N\) người, \(M\) việc (\(N \leq M\)), mỗi người có thể làm một số việc. Tìm maximum number of việc được gán.
Giải pháp: Chạy trực tiếp maximum bipartite matching.
6.2. Maximum Independent Set trên đồ thị hai phía¶
Maximum Independent Set = Tổng số đỉnh \(-\) Maximum Matching.
Áp dụng: chọn nhiều khóa học nhất sao cho không có hai khóa học nào trùng lịch thi.
6.3. Minimum Path Cover trong DAG¶
Cho DAG có \(N\) đỉnh. Tìm số đường đi ít nhất sao cho mỗi đỉnh thuộc đúng một đường đi.
Định lý Dilworth: Min path cover = \(N\) \(-\) Maximum matching trên đồ thị liên kết.
Biến đổi: Mỗi đỉnh \(u\) trong DAG tách thành \(u_{out}\) và \(u_{in}\). Nếu có cạnh \(u \rightarrow v\) trong DAG, thêm cạnh \(u_{out} \rightarrow v_{in}\). Chạy maximum bipartite matching.
6.4. Ghép cặp ổn định (Stable Marriage)¶
Bài toán: \(N\) người nam và \(N\) người nữ, mỗi người có danh sách ưu tiên về bên kia. Tìm perfect matching sao cho không tồn tại cặp \((m, w)\) mà \(m\) thích \(w\) hơn vợ hiện tại VÀ \(w\) thích \(m\) hơn chồng hiện tại.
Giải pháp: Thuật toán Gale-Shapley, \(O(N^2)\). Đây là biến thể khác của matching, không phải bipartite matching thông thường.
6.5. Bài toán liên quan đến số nguyên tố¶
Nhiều bài toán yêu cầu: cho mảng, tìm maximum matching trên đồ thị mà hai phần tử kề nhau nếu tổng của chúng là số nguyên tố.
Giải pháp: Tách mảng thành hai tập (chẵn/lẻ), xây đồ thị hai phía, chạy matching.
7. Lưu ý và Cạm bẫy¶
Phải reset visited mỗi lần gọi DFS trong Kuhn¶
Kuhn có thể rất chậm nếu không dùng heuristic¶
Ví dụ worst-case: đồ thị "ladder" → Kuhn chạy \(O(VE)\) chậm. Hãy dùng:
- Greedy initialization trước khi chạy Kuhn.
- Xáo trộn thứ tự đỉnh.
- Thứ tự đỉnh theo bậc tăng dần.
Không phải mọi đồ thị đều là bipartite¶
Đồ thị hai phía KHÔNG có chu trình lẻ. Trước khi áp dụng, cần kiểm tra:
Chỉ số đỉnh (0-indexed vs 1-indexed)¶
Luôn ghi nhớ chuyển về 0-indexed trước khi xử lý. Nhiều bài trên SPOJ/CF dùng 1-indexed.
Hopcroft-Karp cần hai mảng matchL và matchR¶
Khác với Kuhn chỉ cần \(matchR\), Hopcroft-Karp cần cả \(matchL\) và \(matchR\) để tra cứu nhanh trong BFS.
Bài toán Maximum Weight Bipartite Matching¶
Nếu mỗi cạnh có trọng số và cần maximize tổng trọng số → dùng thuật toán Hungarian (Kuhn-Munkres) hoặc min-cost max-flow. Không thể dùng Kuhn/Hopcroft-Karp trực tiếp.
8. Bài tập luyện tập¶
| Bài | Nền tảng | Độ khó | Chủ đề |
|---|---|---|---|
| CSES - Building Teams | CSES | Trung bình | Kiểm tra đồ thị hai phía |
| CSES - School Dance | CSES | Trung bình | Matching cơ bản |
| CSES - Distinct Routes II | CSES | Khó | Matching + đường đi |
| SPOJ - MATCHING | SPOJ | Khó | Hopcroft-Karp bắt buộc |
| SPOJ - ADABLOOM | SPOJ | Khó | Matching nâng cao |
| CF 498C - Array and Operations | CF | Khó | Matching + số học |
| CF 1045I - Palindrome Pairs | CF | Trung bình | Bitmask + matching |
| CF 1139E - Maximize Mex | CF | Khó | Matching online |
| UVa 10080 - Gopher II | UVA | Trung bình | Matching cơ bản |
| UVa 11159 - Factors and Multiples | UVA | Trung bình | Bipartite check + matching |
Tóm tắt thuật toán¶
Tham khảo¶
- CP-Algorithms - Bipartite Matching
- USACO Guide - Bipartite Matching
- CP-Algorithms - Hungarian Algorithm
- KACTL - Hopcroft-Karp