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

posted @ 2022-07-19 22:23  RiverHamster  阅读(95)  评论(0编辑  收藏  举报
\