Chương 6·Bài 64

Matching

Cặp ghép là tập cạnh không có hai cạnh chung đầu mút. Bài toán hay gặp nhất là 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à thuật toán 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.

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.

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;
using 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

Mỗi lần tìm O(k)O(k), có nn lần: O(nk)O(nk).

Định lý König: phủ đỉnh nhỏ nhất

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;
using 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..

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.

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;
using 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
..
..

O(EV)O(E \sqrt V).

Ghép có trọng số: thuật toán 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;
using 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

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;
using 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

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, ô màu đen trắng, số lẻ với số chẵn. Đồ thị nhỏ dùng Kuhn; tới 10410^4 đến 10510^5 đỉnh dùng Hopcroft-Karp.
  • “Ít hàng/cột nhất để phủ”, “xóa ít nhất để không còn cặp 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í và 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

1
Ghép Các Mức Gần Nhaucanuc80k6400Sắp hai nhóm theo mức kỹ năng rồi tham lam bằng hai con trỏ; Kuhn cũng đủ nhanh.
LQDOJ
2
Phân Ca Không Trùngcanuc80k6401Ghép ca với cộng tác viên bằng Kuhn; mỗi ca cần đúng một người khác nhau.
LQDOJ
3
Ghép Điểm Theo Hướng Đông Bắccanuc80k6403Xét điểm xanh theo xx tăng dần, ghép với điểm đỏ hợp lệ chưa dùng có yy lớn nhất; hoặc Kuhn trực tiếp.
LQDOJ
4
Ghép Cạnh Trên Câycanuc80k6406Cây là hai phía, nhưng tham lam từ lá lên (ghép đỉnh với cha nếu cả hai còn trống) đã tối ưu.
LQDOJ
5
Đổi Sách Cho Mọi Thành Viêncanuc80k6409Mỗi người phải nhận một cuốn sách khác của mình mà họ thích: cần ghép hoàn hảo người với sách.
LQDOJ
6
Ghép Đai Ốc Và Bu Lôngcanuc80k6411Kuhn trên ma trận vừa khớp giữa bu lông và đai ốc.
LQDOJ
7
Xóa Ít Phần Tử Nhấtcanuc80k6412Nối aa với bb khi bb là bội của aa; số phần tử ít nhất cần xóa là phủ đỉnh nhỏ nhất, tức cặp ghép lớn nhất.
LQDOJ
8
Xóa Dấu Theo Hàng Và Cộtcanuc80k6413Mỗi dấu là cạnh hàng và cột; số thao tác ít nhất bằng cặp ghép lớn nhất.
LQDOJ
9
Phủ Các Ô Bùncanuc80k6414Mỗi ô bùn thuộc một đoạn ngang tối đa và một đoạn dọc tối đa; phủ đỉnh nhỏ nhất trên đồ thị đoạn ngang với đoạn dọc.
LQDOJ
10
Phân Phát Theo Khoảngcanuc80k6417Ghép yêu cầu với thẻ trong đoạn của nó; tham lam theo đầu phải tăng dần cũng cho tối ưu.
LQDOJ
11
Mỗi Người Tối Đa Hai Việccanuc80k6418Mỗi người làm tối đa hai việc: nhân đôi đỉnh của người rồi ghép thường.
LQDOJ
12
Lập Lịch Đội Xecanuc80k6420Nối chuyến ii tới chuyến jj nếu một xe kịp làm cả hai; số xe ít nhất bằng số chuyến trừ cặp ghép lớn nhất.
LQDOJ
13
Bạn Ghép Cùng Số Đầu Tiêncanuc80k6436Tổng nguyên tố của hai số lớn hơn 2 thì một lẻ một chẵn; thử từng bạn ghép của số đầu, kiểm ghép hoàn hảo phần còn lại.
LQDOJ
14
Ghép Cặp Với Tổng Ưu Tiên Lớn Nhấtcanuc80k6444N≤16N \le 16: DP trên tập người phía BB đã dùng, hoặc Hungary cho cực đại bằng cách đổi dấu.
LQDOJ
15
Đếm Các Cách Ghép Hoàn Hảocanuc80k6448Đếm số ghép hoàn hảo: DP trên tập đỉnh phải đã dùng, người trái thứ ii ghép với một đỉnh trong tập.
LQDOJ

Đọc thêm