P3386 【模板】二分图最大匹配

这是一道经典的二分图最大匹配模板题。解决这类问题的标准算法是匈牙利算法(Hungarian Algorithm)。


1. 基本概念

  • 二分图:图中的顶点可以分为两个互不相交的集合(左部 \(L\) 和右部 \(R\)),图中的每条边所连接的两个顶点分别属于这两个不同的集合。
  • 匹配:在二分图中,一个边的集合,其中任意两条边都没有公共端点。
  • 最大匹配:边数最多的那个匹配。

2. 核心算法:匈牙利算法

匈牙利算法的核心思想是寻找增广路(Augmenting Path)。

算法步骤:

  1. 从左部点 \(1\) 到 \(n\) 依次尝试为每个点 \(u\) 寻找匹配。
  2. 对于点 \(u\),遍历它所有连接的右部点 \(v\):
    • 如果 \(v\) 还没有被匹配,则直接将 \(u\) 和 \(v\) 匹配。
    • 如果 \(v\) 已经和某个点 \(p\) 匹配了,我们就尝试为 \(p\) “腾位子”——即递归地看 \(p\) 能不能改选其他的右部点。
    • 如果成功“腾位子”或 \(v\) 原本就空闲,则匹配成功,总匹配数 \(+1\)。
  3. 注意点:为了防止在一次 DFS 中陷入死循环,需要使用 vis 数组标记当前路径中已经访问过的右部点。

3. 代码实现 (C++)

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

const int MAXN = 505;
vector<int> adj[MAXN]; // 邻接表,只存储从左部点指向右部点的边
int match[MAXN];       // match[v] 表示右部点 v 当前匹配的左部点编号
bool vis[MAXN];        // 标记右部点在单次寻找增广路中是否被访问过
int n, m, e;

bool dfs(int u) {
    for (int v : adj[u]) {
        if (!vis[v]) {
            vis[v] = true; // 标记该右部点已尝试
            // 如果 v 未匹配,或者能给 v 原配的左部点 match[v] 找到新欢
            if (match[v] == 0 || dfs(match[v])) {
                match[v] = u;
                return true;
            }
        }
    }
    return false;
}

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

    cin >> n >> m >> e;

    for (int i = 0; i < e; ++i) {
        int u, v;
        cin >> u >> v;
        // 题目给出的 v 是右部点编号,直接存储即可
        adj[u].push_back(v);
    }

    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        memset(vis, false, sizeof(vis)); // 每次为新点找匹配时,都要重置标记
        if (dfs(i)) {
            ans++;
        }
    }

    cout << ans << endl;

    return 0;
}

4. 关键细节解析

  • 重边处理:题目提示不保证没有重边。在匈牙利算法中,重边不影响结果的正确性,因为 dfs 会遍历所有边,重边只是多了一次尝试机会,不会导致错误。

  • 复杂度:

    • 时间复杂度为 \(O(n \cdot e)\)。在本题中 \(500 \times 50000 = 2.5 \times 10^7\),在 1s 的时限内绰绰有余。
    • 空间复杂度为 \(O(n + m + e)\),主要由邻接表占用。
  • 为什么 vis 数组要重置?

    每当我们尝试为一个新的左部点 \(i\) 寻找匹配时,vis 记录的是在“为 \(i\) 找匹配”的过程中,哪些右部点已经被考虑过了,防止 DFS 递归中出现死循环。

posted @ 2026-03-03 18:01  whos张一  阅读(59)  评论(0)    收藏  举报