【学习笔记】二分图匹配

前言

二粉兔集合,全体起立!

一些定义

  • 二分图:将能将原图点集 \(V\) 分成两个集合 \(A,B\),且 \(A∩B=∅,A∪B=V\),使得所有边的端点一个在 \(A\) 中,一个在 \(B\) 中的图
  • 匹配:一个边的集合,集合内的任意两个边都没有公共端点,那么集合内的边是匹配边,不在集合内但在原图边集内的边是非匹配边,匹配边的端点是匹配点,其它点是非匹配点
  • 最大匹配:一个图所有的匹配中,包含匹配边最多的匹配的匹配边数
  • 交替路:从非匹配点开始,走非匹配边,匹配边,非匹配边......的路径
  • 增广路:以非匹配点结束的交替路
  • 完美匹配:所有点都是匹配点的匹配
  • 完备匹配:使得 \(A \textup{ or } B\) 集合的所有点都是匹配点的匹配
  • 最佳匹配:带权二分图的权值最大的完备匹配
  • 独立集:点的集合,集合内任意两个点没有边相连

一些性质

  • 二分图等价于无奇环
  • 二分图中,最大独立集点数加最大匹配点数等于总点数

匈牙利算法

等着 ctrl + c

KM 算法

  • 交替路:DFS 过程中,所有访问过的节点以及这些节点形成的边构成的树(即寻找增广路过程中形成的树)
  • 顶标:每个顶点赋予的顶点标记值。满足:\(a_i+b_j\ge w(i,j)\)
  • 相等子图:满足 \(a_i+b_j=w(i,j)\) 的边构成的子图
  • 定理:当相等子图中存在完美匹配时,这个完美匹配就是二分图的带权最大匹配
  • 证明:显而易见
  1. 此处我们设结点较少的集合为 \(A\),初始时对 \(A\) 的每一个顶点设置顶标,顶标的值为该点关联的最大边的权值,\(B\) 的顶点顶标为 \(0\)
  2. 对于 \(A\) 中的每个顶点,在相等子图中利用匈牙利算法找一条增广路径,如果没有找到,则修改顶标,扩大相等子图,继续找增广路径。

如果从 \(A\) 中的某个点 \(A_i\) 出发在相等子图中没有找到增广路径,我们是如何修改顶标的呢?如果我们没有找到增广路径,则我们一定找到了许多条从 \(A_i\) 出发并结束于 \(A\) 的匹配边与未匹配边交替出现的路径,即交替路。我们将交替路中 \(A\) 的顶点顶标减去一个值 \(d\),交替路中属于 \(B\) 的顶点顶标加上一个值 \(d\)。其中 \(d=a_i+b_j-w(i,j)\)

  • 两端都在交替路中的边 \((i,j)\),其顶标和没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。

  • 两端都不在交替路中的边 \((i,j)\),其顶标也没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。

  • \(x\) 不在交替路中,\(y\) 在交替路中的边 \((i,j)\),它的顶标和会增大。它原来不属于相等子图,现在仍不属于相等子图。

  • \(x\) 在交替路中,\(y\) 不在交替路中的边 \((i,j)\),它的顶标和会减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。

我们修改顶标的目的就是要扩大相等子图。为了保证至少有一条边进入相等子图,我们可以在交错树的边中寻找顶标和与边权之差最小的边,这就是前面说的 \(d\)。将交错树中属于 A 的顶点减去 \(d\),交错树中属于 B 的顶点加上 \(d\)。则可以保证至少有一条边扩充进入相等子图。

  1. 当每个点都找到增广路径时,此时意味着每个点都在匹配中,即找到了二分图的完备匹配。该完备匹配即为二分图的带权最大匹配

关于相等子图的一些性质:

  1. 在任意时刻,相等子图的顶标和即为所有顶点的顶标和
  2. 扩充相等子图后,相等子图的顶标和将会减小
  3. 当相等子图的最大匹配为原图的完备匹配时,匹配边的权值和等于所有顶点的顶标和,此匹配即为二分图的带权最大匹配

模板题:给定一个邻接矩阵,然后求二分图的最大权匹配。

#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 310;
const int inf = 0x3f3f3f3f;

int gcd(int a, int b) {
	if (b == 0) return a;
	return gcd(b, a % b);
}

int n, match[N]; // 匈牙利算法必备 
bool va[N], vb[N]; // 记录 va 与 vb 是否遍历过 
ll upd[N], w[N][N], delta; // 计算顶标修改值 
ll la[N], lb[N]; // KM 算法的顶标

bool dfs(int u) {
    va[u] = 1;
    for(int v = 1; v <= n; v++) {
        if (vb[v]) continue;
        if (la[u] + lb[v] - w[u][v] == 0) {
            vb[v] = 1;
            if (!match[v] || dfs(match[v])) {
                match[v] = u;
                return 1;
            }
        } else upd[v] = min(upd[v], la[u] + lb[v] - w[u][v]);
    }
    return 0;
}

ll km() {
    for (int i = 1; i <= n; i++) {
        la[i] = -inf;
        lb[i] = 0;
        for (int j = 1; j <= n; j++) {
            la[i] = max(la[i], w[i][j]);
            match[j] = 0;
        }
    }
    
    for (int i = 1; i <= n; i++) {
        while (1) {
            memset(va, 0, sizeof(va));
            memset(vb, 0, sizeof(vb));
            memset(upd, inf, sizeof(upd));
            
            if (dfs(i)) break;
            delta = inf;
            for (int j = 1; j <= n; j++)
                if (!vb[j]) delta = min(delta, upd[j]);
            
            for (int j = 1; j <= n; j++) {
                if (va[j]) la[j] -= delta;
                if (vb[j]) lb[j] += delta;
            }
        }
    }
    
    ll ans = 0;
    for(int i = 1; i <= n; i++)
		ans += w[match[i]][i];
    return ans;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

    while (cin >> n && n != -1) {
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                cin >> w[i][j];
        cout << km() << "\n";
    }
    
    return 0;
}
posted @ 2025-12-18 21:18  chrispang  阅读(2)  评论(0)    收藏  举报