Masalah matching muncul ketika kita harus memasangkan elemen dari dua himpunan tanpa konflik: pekerja ke pekerjaan, mahasiswa ke asrama, atau item ke pembeli. Untuk graph bipartit, ada tiga algoritma klasik dengan trade-off yang berbeda: Kuhn’s algorithm (augmenting path DFS, sederhana, O(V·E)), Hopcroft-Karp (BFS layering + DFS, O(E·√V)), dan Hungarian algorithm (weighted assignment, O(n³)). Pada Chapter ini kita membedah dua algoritma pertama secara penuh dengan kode C++ yang dapat diuji, lalu menutup dengan ringkasan Hungarian untuk masalah weighted assignment. Pemilihan algoritma bergantung pada ukuran graph dan apakah edge memiliki bobot.
Aturan: Bipartite Matching #
Diberikan graph bipartit G = (L ∪ R, E) dengan L ∩ R = ∅. Sebuah matching adalah subset M ⊆ E sehingga setiap node muncul paling banyak satu kali di M. Maximum matching adalah matching dengan kardinalitas terbesar.
Beberapa istilah kunci:
| Istilah | Definisi |
|---|---|
| Matched node | Node yang menjadi endpoint suatu edge di M |
| Free node | Node yang tidak matched |
| Alternating path | Path yang edge-nya bergantian di M dan tidak di M |
| Augmenting path | Alternating path dari free node ke free node |
| Perfect matching | Semua node matched; ` |
Theorem (Berge): Sebuah matching M maksimum iff tidak ada augmenting path terhadap M. Algoritma augmenting path memanfaatkan ini: cari augmenting path, lalu augment (toggle keanggotaan edge) untuk menambah |M| sebesar satu.
Mekanisme: Augmenting Path (Kuhn’s Algorithm) #
Kuhn’s algorithm melakukan iterasi untuk setiap node u ∈ L:
- Inisialisasi
match_R[v] = -1untuk semuav ∈ R(tidak ada yang matched). - Untuk setiap
u ∈ L: jalankan DFS dariuuntuk mencari augmenting path ke free node diR. - Saat DFS menemukan free node
vdiR, atau nodevyang matched keu'dan rekursi dariu'menemukan free node baru, augment dengan menetapkanmatch_R[v] = u. - Flag
usedmencegah DFS mengunjungiuyang sama dua kali dalam satu iterasi (mencegah infinite loop pada cycle bipartit).
Setiap iterasi berhasil menambah |M| sebesar satu atau menandai u sebagai dead (tidak bisa match). Total O(V·E) karena setiap DFS O(E) dan ada O(V) iterasi.
Kode: Kuhn’s Algorithm (Augmenting Path) #
#include <iostream>
#include <vector>
using namespace std;
class BipartiteMatching {
public:
BipartiteMatching(int n_left, int n_right)
: n_left_(n_left), n_right_(n_right), adj_(n_left) {}
void add_edge(int u, int v) {
adj_[u].push_back(v);
}
// DFS mencari augmenting path dari u.
bool try_kuhn(int u, vector<char>& used, vector<int>& match_r) {
if (used[u]) return false;
used[u] = 1;
for (int v : adj_[u]) {
if (match_r[v] == -1 || try_kuhn(match_r[v], used, match_r)) {
match_r[v] = u;
return true;
}
}
return false;
}
// Mengembalikan daftar pasangan (u, v) yang matched.
vector<pair<int,int>> max_matching() {
vector<int> match_r(n_right_, -1);
int size = 0;
for (int u = 0; u < n_left_; ++u) {
vector<char> used(n_left_, 0);
if (try_kuhn(u, used, match_r)) ++size;
}
vector<pair<int,int>> result;
for (int v = 0; v < n_right_; ++v) {
if (match_r[v] != -1) result.push_back({match_r[v], v});
}
result_size_ = size;
return result;
}
int size() const { return result_size_; }
private:
int n_left_, n_right_, result_size_ = 0;
vector<vector<int>> adj_;
};
int main() {
// L = {0,1,2,3}, R = {0,1,2,3}
BipartiteMatching bm(4, 4);
// Edges:
// 0 -> 0,1
// 1 -> 0,2
// 2 -> 1,3
// 3 -> 2
bm.add_edge(0, 0); bm.add_edge(0, 1);
bm.add_edge(1, 0); bm.add_edge(1, 2);
bm.add_edge(2, 1); bm.add_edge(2, 3);
bm.add_edge(3, 2);
auto m = bm.max_matching();
cout << "Ukuran matching maksimum: " << bm.size() << '\n';
cout << "Pasangan (u, v):\n";
for (auto [u, v] : m) {
cout << " L" << u << " -- R" << v << '\n';
}
return 0;
}Output yang Dihasilkan:
Ukuran matching maksimum: 4
Pasangan (u, v):
L1 -- R0
L0 -- R1
L3 -- R2
L2 -- R3Bedah Kode Baris-per-Baris #
adj_(n_left): adjacency list dari kiri ke kanan. Graph bipartit direpresentasikan satu arah karena algoritma hanya travers dariLkeRlalu kembali keLviamatch_r.add_edge(u, v): tambahkan edgeu → v. Edge otomatis dianggap tidak berarah dalam konteks matching.try_kuhn(u, used, match_r): DFS rekursif. Basis: jikausudah pernah dikunjungi di iterasi ini (used[u] == 1), kembalikanfalseuntuk mencegah loop.for (int v : adj_[u]): coba setiap kandidatvdiR. Dua kemungkinan:match_r[v] == -1(free, augment), ataumatch_r[v] = u'dan DFS rekursif dariu'menemukan jalan ke free node lain (augment dengan re-routingu').match_r[v] = u: augment berhasil — nodevsekarang matched keu. Returntrue.vector<char> used(n_left_, 0): di-reset untuk setiapubaru. Tipechar(bukanbool) sedikit lebih efisien karenavector<bool>di-pack bit-by-bit.for (int u = 0; u < n_left_; ++u): proses setiap node kiri. Order matters — algoritma greedy dengan urutan input; hasil akhir sama-sama maksimum, tetapi pasangan spesifik bisa berbeda.- Verifikasi: 4 pasangan
(0,1), (1,0), (2,3), (3,2)semuanya valid (edge ada) dan semua node matched.|M| = 4 = |L| = |R|→ perfect matching.
Mekanisme: Hopcroft-Karp (BFS Layering + DFS) #
Kuhn’s algorithm mencari satu augmenting path per iterasi. Hopcroft-Karp memproses banyak augmenting path sekaligus per fase:
- BFS layering: dari semua free node di
L, bangun layer jarak terpendek ke free node diRmelalui alternating path. Hanya edge yang menjaga jarak minimal yang dipakai. - DFS augmenting: jalankan DFS paralel dari semua free node di layer 0. Setiap DFS mengikuti edge ke layer berikutnya; augment bila menemukan free node di
R. - Setelah satu fase selesai, ulang BFS — bila tidak ada augmenting path, berhenti.
Setiap fase menambah |M| sebesar jumlah augmenting path yang ditemukan; jarak augmenting path terpendek monoton naik tiap fase. Karena jarak maksimal O(√V), jumlah fase O(√V); setiap fase O(E). Total O(E·√V) — jauh lebih cepat untuk graph besar.
Kode: Hopcroft-Karp #
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
class HopcroftKarp {
public:
HopcroftKarp(int n_left, int n_right)
: n_left_(n_left), n_right_(n_right), adj_(n_left),
match_l_(n_left, -1), match_r_(n_right, -1), dist_(n_left) {}
void add_edge(int u, int v) { adj_[u].push_back(v); }
int max_matching() {
int size = 0;
while (bfs()) {
for (int u = 0; u < n_left_; ++u) {
if (match_l_[u] == -1 && dfs(u)) ++size;
}
}
return size;
}
vector<pair<int,int>> pairs() {
vector<pair<int,int>> r;
for (int u = 0; u < n_left_; ++u)
if (match_l_[u] != -1) r.push_back({u, match_l_[u]});
return r;
}
private:
bool bfs() {
queue<int> q;
bool found = false;
for (int u = 0; u < n_left_; ++u) {
if (match_l_[u] == -1) { dist_[u] = 0; q.push(u); }
else dist_[u] = -1;
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : adj_[u]) {
int u2 = match_r_[v];
if (u2 != -1 && dist_[u2] == -1) {
dist_[u2] = dist_[u] + 1;
q.push(u2);
} else if (u2 == -1) {
found = true; // ada augmenting path di layer ini
}
}
}
return found;
}
bool dfs(int u) {
for (int v : adj_[u]) {
int u2 = match_r_[v];
if (u2 == -1 || (dist_[u2] == dist_[u] + 1 && dfs(u2))) {
match_l_[u] = v;
match_r_[v] = u;
return true;
}
}
dist_[u] = -1; // tandai u sebagai dead untuk fase ini
return false;
}
int n_left_, n_right_;
vector<vector<int>> adj_;
vector<int> match_l_, match_r_, dist_;
};
int main() {
HopcroftKarp hk(4, 4);
hk.add_edge(0, 0); hk.add_edge(0, 1);
hk.add_edge(1, 0); hk.add_edge(1, 2);
hk.add_edge(2, 1); hk.add_edge(2, 3);
hk.add_edge(3, 2);
int sz = hk.max_matching();
cout << "Hopcroft-Karp max matching: " << sz << '\n';
cout << "Pasangan (u, v):\n";
for (auto [u, v] : hk.pairs()) {
cout << " L" << u << " -- R" << v << '\n';
}
return 0;
}Output yang Dihasilkan:
Hopcroft-Karp max matching: 4
Pasangan (u, v):
L0 -- R1
L1 -- R0
L2 -- R3
L3 -- R2Bedah Kode Baris-per-Baris #
match_l_danmatch_r_: tracking dua arah —match_l_[u] = viffmatch_r_[v] = u. Mempermudah lookup dari kedua sisi. Berbeda dengan Kuhn yang hanya menyimpanmatch_r.bfs(): inisialisasidist_[u] = 0untuk semua free nodeu ∈ L(layer 0). Untuk setiap edgeu → v:- Jika
vfree (match_r_[v] == -1): tandaifound = true— ada augmenting path di fase ini. - Jika
vmatched keu2danu2belum dikunjungi: setdist_[u2] = dist_[u] + 1, pushu2ke queue (next layer).
- Jika
dfs(u): traverse hanya edge ke layerdist_[u] + 1. Bilavfree atauu2rekrusif menemukan augmenting path, augment dan returntrue.dist_[u] = -1setelah DFS gagal: tandaiusebagai dead di fase ini, agar DFS paralel lain tidak mencoba lagi — inilah trik yang membuat setiap edge diprosesO(1)per fase.while (bfs()): fase diulang selama masih ada augmenting path. Jarak augmenting path terpendek naik tiap fase, hinggaO(√V)fase.- Verifikasi: hasil sama dengan Kuhn — 4 pasangan perfect matching. Hopcroft-Karp menemukannya dalam 1-2 fase (tergantung urutan), bukan 4 iterasi seperti Kuhn.
Aplikasi: Assignment Problem #
Masalah assignment: n pekerja, n pekerjaan, matriks cost C[i][j]. Cari perfect matching dengan total cost minimum. Ini adalah weighted bipartite matching.
| Algoritma | Kompleksitas | Use case |
|---|---|---|
| Kuhn’s (augmenting path) | O(V·E) | Unweighted, sederhana |
| Hopcroft-Karp | O(E·√V) | Unweighted, graph besar |
| Hungarian | O(n³) | Weighted (assignment problem) |
| Min cost max flow | O(F·V·E) | Weighted, dengan kapasitas |
Hungarian algorithm mempertahankan potential u[i] untuk left dan v[j] untuk right sehingga u[i] + v[j] ≤ C[i][j] (dual feasible). Setiap iterasi menambah satu edge ke matching sambil menjaga dual feasibility — augmenting path + relabeling. Implementasi penuh memerlukan ~50 baris; untuk assignment problem dengan n ≤ 500, kode tersedia di banyak template kompetisi.
Kompleksitas dan Pemilihan #
| Algoritma | Time | Space | Cocok untuk |
|---|---|---|---|
| Brute force | O(n!) | O(n) | n ≤ 10 |
| Kuhn’s | O(V·E) | O(V+E) | Default, V ≤ 1000 |
| Hopcroft-Karp | O(E·√V) | O(V+E) | V ≥ 1000, dense graph |
| Hungarian | O(n³) | O(n²) | Weighted assignment |
| Min cost max flow | O(F·V·E) | O(V+E) | Weighted, kapasitas |
Ringkasan #
- Bipartite matching: cari maximum subset edge tanpa konflik.
- Augmenting path (Berge): matching maksimum iff tidak ada augmenting path.
- Kuhn’s algorithm: DFS per node kiri,
O(V·E), sederhana dan cepat untuk graph kecil. - Hopcroft-Karp: BFS layering + DFS paralel,
O(E·√V), optimal untuk graph besar. - Hungarian algorithm: weighted bipartite matching
O(n³), untuk masalah assignment. - Untuk skenario dengan cost dan kapasitas, reduksi ke min cost max flow (lihat Chapter network-flow).