Chương 6·Bài 6.5

Matching

Có một nhóm học sinh và một nhóm bài tập; mỗi bạn chỉ làm được vài bài nhất định, và mỗi bài chỉ giao cho một bạn. Giao thế nào để nhiều bạn có bài nhất?

Đó là bài cặp ghép (tập cạnh không có hai cạnh chung đầu mút), thường trên đồ thị hai phía: người với việc, hàng với cột, ô đen với ô trắng. Cặp ghép lớn nhất tìm bằng đường tăng. Định lý König cho thêm phủ đỉnh nhỏ nhất; Hopcroft-Karp cho đồ thị lớn; thuật toán Hungary cho ghép hoàn hảo chi phí nhỏ nhất; Gale-Shapley cho ghép ổn định.

Đường tăng và Kuhn

Với một cặp ghép MM, đường tăng là đường đi xen kẽ cạnh ngoài MM, cạnh trong MM, bắt đầu và kết thúc ở hai đỉnh chưa ghép. Đảo trạng thái mọi cạnh trên đường tăng làm MM tăng thêm 1. Định lý Berge: MM lớn nhất khi và chỉ khi không còn đường tăng.

Kuhn: lần lượt với mỗi đỉnh trái uu, tìm đường tăng bằng DFS. Thử từng đỉnh phải vv kề uu chưa thăm trong lượt này: nếu vv trống, hoặc đỉnh trái đang ghép với vv tìm được chỗ khác, thì ghép uu với vv. Mỗi lượt O(E)O(E), tổng O(VE)O(VE); thực tế chạy nhanh hơn nhiều. Đánh dấu thăm bằng số hiệu lượt để khỏi xóa mảng.

Trong hình, cạnh xanh đậm đang ghép, cạnh cam là đường tăng vừa tìm. Đảo trạng thái các cạnh trên đường làm số cặp tăng một.

Mỗi lượt tìm một đường tăng.

Ví dụCác Cặp Tương Thích

Đề. n,m≤500n, m \le 500 người hai nhóm, k≤1000k \le 1000 cặp tương thích. In số cặp lớn nhất và các cặp.

Lời giải. Thuật toán Kuhn: lần lượt với từng người nhóm trái, tìm một đường tăng bắt đầu từ người đó bằng DFS. Đường tăng đi từ trái sang phải bằng cạnh bất kỳ, từ phải về trái bằng cạnh đang ghép, và kết thúc ở một người phải còn tự do. Đảo trạng thái các cạnh trên đường làm số cặp tăng 1. Theo định lý Berge, khi không còn đường tăng thì cặp ghép là lớn nhất.

Bước 1: tìm đường tăng. mt[v] là người trái đang ghép với người phải vv. Thử từng vv kề uu: vv còn tự do thì ghép luôn; vv đã ghép thì thử tìm cho mt[v] một người khác bằng đệ quy. Thành công thì gán mt[v] = u: trên đường về, mọi cạnh của đường tăng được đảo. vis[v] = stamp để mỗi đỉnh phải chỉ được thử một lần trong một lần tìm; dùng số hiệu lần tìm làm dấu thì khỏi xóa mảng vis.

C++
bool kuhn(int u, int stamp) {
    for (int v : g[u])
        if (vis[v] != stamp) {
            vis[v] = stamp;
            if (!mt[v] || kuhn(mt[v], stamp)) {
                mt[v] = u;
                return 1;
            }
        }
    return 0;
}

Bước 2: in. ml[u] dựng lại từ mt để in theo thứ tự người trái.

C++
for (int u = 1; u <= n; u++) res += kuhn(u, u);
for (int v = 1; v <= m; v++)
    if (mt[v]) ml[mt[v]] = v;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 505;
int n, m, k, mt[N], ml[N], vis[N];
vector<int> g[N];

bool kuhn(int u, int stamp) {
    for (int v : g[u])
        if (vis[v] != stamp) {
            vis[v] = stamp;
            if (!mt[v] || kuhn(mt[v], stamp)) {
                mt[v] = u;
                return 1;
            }
        }
    return 0;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m >> k;
    for (int i = 1; i <= k; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
    }
    int res = 0;
    for (int u = 1; u <= n; u++) res += kuhn(u, u);
    for (int v = 1; v <= m; v++)
        if (mt[v]) ml[mt[v]] = v;
    cout << res << '\n';
    for (int u = 1; u <= n; u++)
        if (ml[u]) cout << u << ' ' << ml[u] << '\n';
}
4 4 5
1 1
1 2
2 1
3 3
4 3

Độ phức tạp: mỗi lần tìm O(k)O(k), có nn lần: O(nk)O(nk).

Định lý König

Trên đồ thị hai phía, phủ đỉnh nhỏ nhất (tập đỉnh chạm mọi cạnh) có kích thước bằng cặp ghép lớn nhất. Dựng: từ mọi đỉnh trái chưa ghép, đi theo đường xen kẽ (trái sang phải bằng cạnh bất kỳ, phải sang trái bằng cạnh ghép), đánh dấu các đỉnh tới được. Phủ đỉnh là đỉnh trái không được đánh dấu cùng đỉnh phải được đánh dấu.

Hệ quả hay dùng: tập độc lập lớn nhất $= V - $ cặp ghép lớn nhất; số đường đi rời đỉnh ít nhất phủ một DAG == số đỉnh −- cặp ghép lớn nhất trên đồ thị tách đỉnh ra/vào.

Hỏi nhanh

Bảng có các dấu tại (1,1),(1,3),(2,2),(3,2)(1, 1), (1, 3), (2, 2), (3, 2). Mỗi thao tác xóa mọi dấu trên một hàng hoặc một cột. Cần ít nhất bao nhiêu thao tác?

Ví dụPhủ Các Ô Được Đánh Dấu

Đề. Bảng N×NN \times N, N≤100N \le 100, có các ô đánh dấu o. Chọn ít hàng và cột nhất sao cho mọi ô đánh dấu nằm trên một hàng hoặc cột được chọn, in các hàng, cột đó.

Lời giải. Mỗi ô đánh dấu (i,j)(i, j) là một cạnh nối hàng ii với cột jj. Chọn hàng, cột để mọi ô đánh dấu nằm trên một hàng hoặc cột được chọn chính là phủ đỉnh của đồ thị hai phía này. Theo định lý König, phủ đỉnh nhỏ nhất bằng cặp ghép lớn nhất. Tìm cặp ghép bằng Kuhn như ví dụ trước, rồi dựng phủ: đánh dấu mọi đỉnh tới được từ các hàng chưa ghép theo đường xen kẽ, lấy hàng không được đánh dấu và cột được đánh dấu.

Bước 1: đi theo đường xen kẽ. Từ hàng uu sang mọi cột kề, từ cột đi tiếp về hàng đang ghép với nó.

C++
void alt(int u) {
    visL[u] = 1;
    for (int v : g[u])
        if (!visR[v]) {
            visR[v] = 1;
            if (mt[v] && !visL[mt[v]]) alt(mt[v]);
        }
}
C++
for (int u = 1; u <= n; u++)
    if (!ml[u]) alt(u);

Bước 2: vì sao là một phủ. Lấy một ô (i,j)(i, j). Nếu hàng ii không được đánh dấu thì nó được chọn. Nếu hàng ii được đánh dấu thì alt đã đi qua cạnh (i,j)(i, j) nên cột jj được đánh dấu và được chọn.

Bước 3: vì sao đúng bằng số cặp. Hàng chưa ghép đều là điểm xuất phát nên đều được đánh dấu, tức hàng được chọn đều đã ghép. Cột được đánh dấu cũng đã ghép, nếu không thì có đường tăng. Một cặp không thể góp cả hai đầu: cột của nó được đánh dấu thì alt đi tiếp về hàng của nó và hàng ấy được đánh dấu. Mỗi cặp góp đúng một đỉnh.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 105;
int n, mt[N], ml[N], vis[N];
bool visL[N], visR[N];
vector<int> g[N];

bool kuhn(int u, int stamp) {
    for (int v : g[u])
        if (vis[v] != stamp) {
            vis[v] = stamp;
            if (!mt[v] || kuhn(mt[v], stamp)) {
                mt[v] = u;
                return 1;
            }
        }
    return 0;
}

void alt(int u) {
    visL[u] = 1;
    for (int v : g[u])
        if (!visR[v]) {
            visR[v] = 1;
            if (mt[v] && !visL[mt[v]]) alt(mt[v]);
        }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= n; j++)
            if (s[j - 1] == 'o') g[i].push_back(j);
    }
    int res = 0;
    for (int u = 1; u <= n; u++) res += kuhn(u, u);
    for (int v = 1; v <= n; v++)
        if (mt[v]) ml[mt[v]] = v;
    for (int u = 1; u <= n; u++)
        if (!ml[u]) alt(u);
    cout << res << '\n';
    for (int u = 1; u <= n; u++)
        if (!visL[u]) cout << 1 << ' ' << u << '\n';
    for (int v = 1; v <= n; v++)
        if (visR[v]) cout << 2 << ' ' << v << '\n';
}
4
.o..
.o.o
...o
.o..

Độ phức tạp: O(N⋅N2)O(N \cdot N^2) cho Kuhn trên tối đa N2N^2 cạnh.

Hopcroft-Karp

Mỗi pha, BFS từ mọi đỉnh trái chưa ghép để chia lớp theo độ dài đường xen kẽ ngắn nhất. Rồi DFS chỉ đi theo các lớp tăng dần, tìm một tập cực đại các đường tăng ngắn nhất rời nhau và đảo hết. Sau O(V)O(\sqrt V) pha không còn đường tăng; tổng O(EV)O(E \sqrt V). Trong DFS, đỉnh nào thất bại thì đặt lớp của nó thành vô cùng để pha này không thử lại.

Khoảng cách so với Kuhn thấy rõ khi đồ thị lớn:

Cặp ghép lớn nhất, 100 nghìn đỉnh mỗi phía, 400 nghìn cạnh.Kuhn452 ms · gấp 11 lầnKuhn khởi tạo tham lam234 ms · gấp 5,7 lầnHopcroft-Karp41 ms
Cặp ghép lớn nhất với 100 nghìn đỉnh mỗi phía và 400 nghìn cạnh ngẫu nhiên, ba cách làm.

Hopcroft-Karp nhanh hơn Kuhn khoảng 11 lần. Nếu vẫn dùng Kuhn thì ghép tham lam trước (mỗi đỉnh trái lấy ngay một đỉnh phải còn trống) bớt được chừng một nửa thời gian. Quy tắc: từ 10410^4 đỉnh trở lên dùng Hopcroft-Karp.

Bảng ô vuông là đồ thị hai phía tự nhiên: tô màu như bàn cờ theo tính chẵn lẻ của i+ji + j, mọi cạnh nối hai ô khác màu. Lát domino nhiều nhất là cặp ghép lớn nhất giữa ô đen và ô trắng.

Ví dụLát Domino Tối Đa

Đề. Bảng H×WH \times W (≤100×100\le 100 \times 100) có ô trống và ô chặn. Đặt nhiều domino nhất trên ô trống, in bảng với domino ngang là ><, dọc là v trên ^.

Lời giải. Tô bảng như bàn cờ theo tính chẵn lẻ của i+ji + j; mỗi domino phủ đúng một ô mỗi màu, nên số domino lớn nhất là cặp ghép lớn nhất giữa ô trống chẵn và ô trống lẻ kề nhau. Đồ thị tới 10410^4 đỉnh, nên dùng Hopcroft-Karp thay cho Kuhn. Ô (i,j)(i, j) có số hiệu iW+j+1iW + j + 1.

Bước 1: dựng đồ thị. lefts là các ô trống có i+ji + j chẵn; mỗi ô nối với các ô trống kề, đều là ô lẻ.

C++
if (a[i][j] != '.' || (i + j) % 2) continue;
int u = i * w + j + 1;
lefts.push_back(u);
for (int d = 0; d < 4; d++) {
    int x = i + dx[d], y = j + dy[d];
    if (x >= 0 && x < h && y >= 0 && y < w && a[x][y] == '.') g[u].push_back(x * w + y + 1);
}

Bước 2: BFS chia lớp. Ô trái tự do có dist = 0; từ ô trái u sang ô phải v, rồi theo cạnh ghép sang ô trái mr[v] ở lớp kế. Gặp ô phải tự do nghĩa là pha này còn đường tăng.

C++
for (int v : g[u]) {
    int x = mr[v];
    if (!x) found = 1;
    else if (dist[x] == INF) dist[x] = dist[u] + 1, q.push(x);
}

Bước 3: DFS theo lớp. Chỉ đi sang ô trái ở đúng lớp dist[u] + 1, nên mọi đường tìm được trong một pha đều là đường tăng ngắn nhất. Thất bại thì đặt dist[u] = INF để pha này không thử lại uu. Mỗi pha tìm một tập đường tăng rời nhau; sau O(V)O(\sqrt V) pha không còn đường tăng.

C++
bool dfs(int u) {
    for (int v : g[u]) {
        int x = mr[v];
        if (!x || (dist[x] == dist[u] + 1 && dfs(x))) {
            ml[u] = v, mr[v] = u;
            return 1;
        }
    }
    dist[u] = INF;
    return 0;
}

Bước 4: vẽ. Hai ô cùng hàng là domino ngang, ô trái nhận >, ô phải <; cùng cột là domino dọc, ô trên v, ô dưới ^.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 1e4 + 5, INF = 1e9;
int h, w, ml[N], mr[N], dist[N];
vector<int> g[N], lefts;
string a[105];

bool bfs() {
    queue<int> q;
    for (int u : lefts) {
        if (!ml[u]) dist[u] = 0, q.push(u);
        else dist[u] = INF;
    }
    bool found = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : g[u]) {
            int x = mr[v];
            if (!x) found = 1;
            else if (dist[x] == INF) dist[x] = dist[u] + 1, q.push(x);
        }
    }
    return found;
}

bool dfs(int u) {
    for (int v : g[u]) {
        int x = mr[v];
        if (!x || (dist[x] == dist[u] + 1 && dfs(x))) {
            ml[u] = v, mr[v] = u;
            return 1;
        }
    }
    dist[u] = INF;
    return 0;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> h >> w;
    for (int i = 0; i < h; i++) cin >> a[i];
    int dx[] = {0, 0, 1, -1}, dy[] = {1, -1, 0, 0};
    for (int i = 0; i < h; i++)
        for (int j = 0; j < w; j++) {
            if (a[i][j] != '.' || (i + j) % 2) continue;
            int u = i * w + j + 1;
            lefts.push_back(u);
            for (int d = 0; d < 4; d++) {
                int x = i + dx[d], y = j + dy[d];
                if (x >= 0 && x < h && y >= 0 && y < w && a[x][y] == '.') g[u].push_back(x * w + y + 1);
            }
        }
    int res = 0;
    while (bfs())
        for (int u : lefts)
            if (!ml[u] && dfs(u)) res++;
    for (int u : lefts)
        if (ml[u]) {
            int i = (u - 1) / w, j = (u - 1) % w, x = (ml[u] - 1) / w, y = (ml[u] - 1) % w;
            if (i == x) {
                a[i][min(j, y)] = '>';
                a[i][max(j, y)] = '<';
            } else {
                a[min(i, x)][j] = 'v';
                a[max(i, x)][j] = '^';
            }
        }
    cout << res << '\n';
    for (int i = 0; i < h; i++) cout << a[i] << '\n';
}
2 2
..
..

Độ phức tạp: O(EV)O(E \sqrt V).

Hungary

Bài phân công: ma trận chi phí n×nn \times n, chọn một hoán vị có tổng nhỏ nhất. Thuật toán Hungary giữ thế vị uiu_i cho hàng, vjv_j cho cột với ui+vj≤ciju_i + v_j \le c_{ij}, chỉ ghép trên các cạnh “chặt” ui+vj=ciju_i + v_j = c_{ij}. Lần lượt thêm từng hàng; khi không tìm được đường tăng trên cạnh chặt, tăng thế vị một lượng Δ\Delta nhỏ nhất để có thêm cạnh chặt, như một bước Dijkstra. Mỗi hàng O(n2)O(n^2), tổng O(n3)O(n^3). Tổng chi phí tối ưu bằng −v0-v_0 trong cách cài với cột ảo 0.

Muốn cực đại thì đổi dấu chi phí. Ma trận chữ nhật n≤mn \le m chạy được ngay, mỗi hàng ghép một cột.

Hỏi nhanh

Vì sao không dùng tham lam “mỗi người lấy việc rẻ nhất còn lại” cho bài phân công?

Ví dụPhân Công Chi Phí Nhỏ Nhất

Đề. n≤200n \le 200 nhân viên, nn công việc, chi phí cij≤1000c_{ij} \le 1000. Giao mỗi người một việc khác nhau, tổng chi phí nhỏ nhất, in tổng và phương án.

Lời giải. Thuật toán Hungary giữ thế vị uiu_i cho hàng, vjv_j cho cột với ui+vj≤ciju_i + v_j \le c_{ij} mọi lúc. Tổng thế vị là cận dưới của mọi phương án, và khi mọi cặp ghép đều nằm trên cạnh chặt (ui+vj=ciju_i + v_j = c_{ij}) thì cặp ghép đạt đúng cận đó, nên tối ưu. Thêm lần lượt từng hàng: tìm đường tăng chỉ đi trên cạnh chặt; chưa tới được cột tự do thì tăng thế vị một lượng nhỏ nhất để có thêm cạnh chặt. Cột ảo 0 làm điểm xuất phát, p[j] là hàng ghép với cột jj, way[j] là cột đứng trước jj trên đường tăng.

Bước 1: mở rộng cây tìm kiếm. Đầu mỗi lượt p[0] = i đặt hàng mới vào cột ảo, used là các cột đã thuộc cây. Hàng i0 vừa được thêm cập nhật minv[j], chi phí rút gọn nhỏ nhất để đi tới cột jj chưa dùng, và ghi way[j]. delta là giá trị nhỏ nhất trong các minv, ứng với cột j1.

C++
for (int j = 1; j <= n; j++)
    if (!used[j]) {
        ll cur = a[i0][j] - u[i0] - v[j];
        if (cur < minv[j]) minv[j] = cur, way[j] = j0;
        if (minv[j] < delta) delta = minv[j], j1 = j;
    }

Bước 2: cập nhật thế vị. Cột đã dùng giảm v, hàng của chúng tăng u cùng một lượng, nên cạnh chặt trong cây vẫn chặt; minv của cột chưa dùng giảm delta, và cột j1 có cạnh chặt mới. Đi sang j1; nếu j1 tự do thì dừng.

C++
for (int j = 0; j <= n; j++) {
    if (used[j]) u[p[j]] += delta, v[j] -= delta;
    else minv[j] -= delta;
}
j0 = j1;

Bước 3: đảo đường tăng. Đi ngược theo way, dời mỗi hàng sang cột kế tiếp trên đường.

C++
do {
    int j1 = way[j0];
    p[j0] = p[j1];
    j0 = j1;
} while (j0);

Tổng chi phí là -v[0]: cột ảo gom toàn bộ lượng tăng thế vị của các lượt.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 205;
const ll INF = 1e18;
int n, p[N], way[N], ans[N];
ll a[N][N], u[N], v[N], minv[N];
bool used[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) cin >> a[i][j];
    for (int i = 1; i <= n; i++) {
        p[0] = i;
        int j0 = 0;
        for (int j = 0; j <= n; j++) minv[j] = INF, used[j] = 0;
        do {
            used[j0] = 1;
            int i0 = p[j0], j1 = 0;
            ll delta = INF;
            for (int j = 1; j <= n; j++)
                if (!used[j]) {
                    ll cur = a[i0][j] - u[i0] - v[j];
                    if (cur < minv[j]) minv[j] = cur, way[j] = j0;
                    if (minv[j] < delta) delta = minv[j], j1 = j;
                }
            for (int j = 0; j <= n; j++) {
                if (used[j]) u[p[j]] += delta, v[j] -= delta;
                else minv[j] -= delta;
            }
            j0 = j1;
        } while (p[j0]);
        do {
            int j1 = way[j0];
            p[j0] = p[j1];
            j0 = j1;
        } while (j0);
    }
    for (int j = 1; j <= n; j++) ans[p[j]] = j;
    cout << -v[0] << '\n';
    for (int i = 1; i <= n; i++) cout << i << ' ' << ans[i] << '\n';
}
3
9 2 7
6 4 3
5 8 1

Độ phức tạp: mỗi lượt O(n2)O(n^2), tổng O(n3)O(n^3).

Ghép ổn định

Mỗi người hai phía xếp hạng toàn bộ phía kia. Cặp ghép hoàn hảo là ổn định nếu không có hai người khác cặp cùng thích nhau hơn người đang ghép. Gale-Shapley: mỗi người phía AA còn tự do cầu hôn người kế tiếp trong danh sách của mình; người phía BB giữ lời cầu hôn tốt nhất từng nhận, trả lại người cũ khi có người tốt hơn. Mỗi người AA cầu hôn mỗi người BB tối đa một lần, nên tổng O(n2)O(n^2), và kết quả luôn ổn định: nếu AiA_i thích BjB_j hơn bạn ghép thì AiA_i đã từng cầu hôn BjB_j và bị từ chối vì BjB_j có người tốt hơn.

Kết quả là phương án tốt nhất cho phía chủ động cầu hôn trong mọi ghép ổn định.

Ví dụGhép Cặp Ổn Định

Đề. t≤100t \le 100 bộ, mỗi bộ n≤500n \le 500 người mỗi phía; danh sách ưu tiên của phía BB cho trước, rồi của phía AA, mỗi dòng mở đầu bằng số hiệu người. In một ghép ổn định.

Lời giải. Gale-Shapley với phía AA cầu hôn. Mỗi người AA còn tự do cầu hôn người kế tiếp trong danh sách của mình; người BB giữ lời cầu hôn tốt nhất từng nhận và trả lại người cũ khi có người tốt hơn.

Bước 1: bảng hạng. Để BB so hai người trong O(1)O(1), danh sách của BB ghi thẳng thành hạng rk[b][a]; danh sách của AA giữ nguyên thứ tự trong pa. Mỗi dòng mở đầu bằng số hiệu người.

C++
for (int r = 0; r < n; r++) {
    int a;
    cin >> a;
    rk[b][a] = r;
}

Bước 2: vòng cầu hôn. Hàng đợi fr chứa người AA đang tự do; nxt[a] là vị trí kế tiếp trong danh sách của aa. bb chưa có ai thì nhận aa; có người cũ mà thích aa hơn thì đổi và đẩy người cũ vào lại hàng đợi; ngược lại aa bị từ chối, quay lại hàng đợi để cầu hôn người kế. Mỗi người AA cầu hôn mỗi người BB tối đa một lần, nên vòng lặp chạy không quá n2n^2 lần.

C++
while (!fr.empty()) {
    int a = fr.front();
    fr.pop();
    int b = pa[a][nxt[a]++];
    if (!husband[b]) husband[b] = a;
    else if (rk[b][a] < rk[b][husband[b]]) fr.push(husband[b]), husband[b] = a;
    else fr.push(a);
}

Vì sao ổn định: nếu aa thích bb hơn vợ hiện tại thì aa đã cầu hôn bb trước, và bị từ chối hoặc bị trả lại vì bb có người tốt hơn. Lựa chọn của bb chỉ tốt lên theo thời gian, nên bb không thích aa hơn chồng hiện tại. Mỗi bộ dữ liệu đặt lại nxt và husband.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 505;
int n, pa[N][N], rk[N][N], nxt[N], husband[N], wife[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int t;
    cin >> t;
    while (t--) {
        cin >> n;
        for (int k = 0; k < n; k++) {
            int b;
            cin >> b;
            for (int r = 0; r < n; r++) {
                int a;
                cin >> a;
                rk[b][a] = r;
            }
        }
        for (int k = 0; k < n; k++) {
            int a;
            cin >> a;
            for (int r = 0; r < n; r++) cin >> pa[a][r];
        }
        queue<int> fr;
        for (int a = 1; a <= n; a++) nxt[a] = 0, husband[a] = 0, fr.push(a);
        while (!fr.empty()) {
            int a = fr.front();
            fr.pop();
            int b = pa[a][nxt[a]++];
            if (!husband[b]) husband[b] = a;
            else if (rk[b][a] < rk[b][husband[b]]) fr.push(husband[b]), husband[b] = a;
            else fr.push(a);
        }
        for (int b = 1; b <= n; b++) wife[husband[b]] = b;
        for (int a = 1; a <= n; a++) cout << a << ' ' << wife[a] << '\n';
    }
}
1
3
1 2 1 3
2 1 2 3
3 1 2 3
1 1 2 3
2 1 2 3
3 2 3 1

Độ phức tạp: mỗi bộ O(n2)O(n^2).

Khi đi thi

  • Nhận ra hai phía: người với việc, hàng và cột, ô đen với ô trắng, số lẻ với số chẵn. Đồ thị nhỏ dùng Kuhn; từ 10410^4 đến 10510^5 đỉnh dùng Hopcroft-Karp.
  • “Ít hàng/cột nhất để phủ”, “xóa ít nhất để hết xung đột”, “tập độc lập lớn nhất”, “phủ DAG bằng ít đường nhất”: đều quy về cặp ghép lớn nhất qua König.
  • Có chi phí, mỗi bên đúng một: Hungary O(n3)O(n^3). Đề nói “ổn định” với danh sách ưu tiên: Gale-Shapley.

Tóm tắt

  • Cặp ghép lớn nhất khi và chỉ khi không còn đường tăng; Kuhn tìm từng đường tăng, O(VE)O(VE).
  • König: phủ đỉnh nhỏ nhất bằng cặp ghép lớn nhất, dựng từ đỉnh trái chưa ghép bằng đường xen kẽ.
  • Hopcroft-Karp: mỗi pha tăng theo lớp BFS, O(EV)O(E\sqrt V).
  • Hungary giữ thế vị và chỉ ghép trên cạnh chặt; Gale-Shapley cầu hôn lần lượt, O(n2)O(n^2).

Bài tập

Đọc thêm