KM 二分图最大权匹配
以下没有证明。
这玩意儿比 dijkstra 费用流短,而且不太跑得满,内存连续性也相当好。缺点可能是对两侧点数有严格的限制,且不能求 \(i\) 匹配的最优方案,还有相当不直观。
先考虑二分图最大权匹配的对偶问题:最小化顶标和 \(\sum y_i\),满足对于每条边,\(y_u +y_v \ge w\)。(假设边权非负)。
称 \(y_u + y_v = w\) 的边构成的生成子图为相等子图,我们断言当相等子图的完美匹配是原图的最大权匹配,其他匹配权值一定更小(根据顶标的性质)。
所以我们先初始化一组顶标,通常取左侧顶标为邻边边权 max,右侧为 \(0\),然后不断扩大相等子图(减小顶标和)知道找到完美匹配。
暴力的过程就是每次加入一个左侧点,然后从该点开始 DFS 遍历图。如果有增广路就直接增广,否则使左侧遍历到的点顶标 \(+ \Delta\),右侧减 \(-\Delta\),使得一条边恰好被加入相等子图。这样可以保证遍历到的相等子图中的边仍然在相等子图中。(贪心的证明?)
但这样共要加入 \(n^2\) 条边,每次遍历边数也为 \(n^2\),每次只加一条边,比较浪费。
注意到找增广路不需要用 DFS。我们每次只需要维护一个可达的点集 \(R\),每次尝试匹配一个左侧点可达的右侧点 \(v\)(未访问过),如果未匹配就可以找到一条增广路,否则就将 \(v\) 匹配的点加入 \(R\) 继续做。
这样,我们初始化 \(R = \{s\}\),并维护每个 \(\not \in R\) 的右侧点 \(v\) 变得可达需要修改的最小顶标值 \(slack_v\)。每次将 \(slack_v\) 最小的 \(v\) 加入一条邻边到相等子图(注意邻边的另一侧 \(u \in R\) 是不固定的),即可找到 \(s\) 出发的增广路。
实际上 KM 维护了每加入一个左侧点的最大权匹配,所以也可以做左部点数小于等于右部点数的完备匹配。
胡写的代码:https://uoj.ac/submission/564953
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <numeric>
#include <vector>
#include <cassert>
#include <unistd.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
#define LOG(f...) fprintf(stderr, f)
// #define DBG(f...) printf(f)
#define DBG(f...) void()
#define all(cont) begin(cont), end(cont)
#ifdef __linux__
#define getchar getchar_unlocked
#define putchar putchar_unlocked
#endif
template <class T> void read(T &x) {
char ch; x = 0;
int f = 1;
while (isspace(ch = getchar()));
if (ch == '-') ch = getchar(), f = -1;
do x = x * 10 + (ch - '0'); while (isdigit(ch = getchar()));
x *= f;
}
template <class T, class ...A> void read(T &x, A&... args) { read(x); read(args...); }
const int N = 405;
const int INF = 0x3f3f3f3f;
int d[N][N], src[N], sl[N], mat1[N], mat2[N];
int yl[N], yr[N];
bool vis1[N], vis2[N];
int n1, n2, m;
bool exchanged = false;
void augment(int x) {
fill(vis1, vis1 + n1, false);
fill(vis2, vis2 + n2, false);
fill(sl, sl + n2, INF);
while (true) {
vis1[x] = true;
int piv = -1, dlt = INF;
for (int v = 0; v < n2; ++v) {
if (vis2[v]) continue;
int s = yl[x] + yr[v] - d[x][v];
if (s < sl[v]) { src[v] = x; sl[v] = s; }
if (sl[v] < dlt || (sl[v] == dlt && mat2[v] == -1)) { dlt = sl[v]; piv = v; }
}
for (int u = 0; u < n1; ++u)
if (vis1[u]) yl[u] -= dlt;
for (int v = 0; v < n2; ++v)
if (vis2[v]) yr[v] += dlt;
else sl[v] -= dlt;
vis2[piv] = true;
if (mat2[piv] == -1) { x = piv; break; }
x = mat2[piv];
}
while (true) {
mat2[x] = src[x];
int t = mat1[src[x]];
mat1[src[x]] = x;
if (t == -1) break;
x = t;
}
}
int main() {
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
read(n1, n2, m);
if (n1 <= n2)
for (int i = 0; i < m; ++i) {
int u, v, w;
read(u, v, w); --u; --v;
d[u][v] = max(d[u][v], w);
}
else {
for (int i = 0; i < m; ++i) {
int u, v, w;
read(u, v, w); --u; --v;
d[v][u] = max(d[v][u], w);
}
exchanged = true;
swap(n1, n2);
}
for (int i = 0; i < n1; ++i)
yl[i] = *max_element(d[i], d[i] + n2);
fill(yr, yr + n2, 0);
fill(mat1, mat1 + n1, -1);
fill(mat2, mat2 + n2, -1);
for (int i = 0; i < n1; ++i)
augment(i);
printf("%lld\n", accumulate(yl, yl + n1, 0LL) + accumulate(yr, yr + n2, 0LL));
if (exchanged)
for (int i = 0; i < n2; ++i)
printf("%d%c", d[mat2[i]][i] ? mat2[i] + 1 : 0, " \n"[i + 1 == n2]);
else
for (int i = 0; i < n1; ++i)
printf("%d%c", d[i][mat1[i]] ? mat1[i] + 1 : 0, " \n"[i + 1 == n1]);
return 0;
}