题解:洛谷 P3386 二分图最大匹配

【题目来源】

洛谷:P3386 【模板】二分图最大匹配 - 洛谷(luogu.com.cn)

【题目描述】

给定一个二分图,其左部点的个数为 \(n\),右部点的个数为 \(m\),边数为 \(e\),求其最大匹配的边数。

左部点从 \(1\)\(n\) 编号,右部点从 \(1\)\(m\) 编号。

【输入】

输入的第一行是三个整数,分别代表 \(n\)\(m\)\(e\)

接下来 \(e\) 行,每行两个整数 \(u,v\),表示存在一条连接左部点 \(u\) 和右部点 \(v\) 的边。

【输出】

输出一行一个整数,代表二分图最大匹配的边数。

【输入样例】

1 1 1
1 1

【输出样例】

1

【核心思想】

  1. 问题分析:给定一个二分图,左部有 \(n\) 个点,右部有 \(m\) 个点,以及 \(e\) 条边。需要求出二分图的最大匹配,即选择尽可能多的边,使得任意两条边没有公共端点。这是二分图匹配的经典问题,可通过匈牙利算法(增广路)或转化为网络最大流求解。

  2. 算法选择

    • 匈牙利算法(增广路思想):对每个左部点尝试寻找增广路,若找到则匹配数加 1。利用 match[v] 记录右部点 \(v\) 当前匹配的左部点,vis[v] 标记本轮搜索中右部点是否被访问,避免重复搜索。算法核心是递归尝试为冲突的匹配点重新找匹配。
    • 网络流转化(Dinic):将二分图匹配建模为最大流:源点 \(s\) 连接所有左部点(容量 1),左部点连接右部点(容量 1),右部点连接汇点 \(t\)(容量 1),则最大流的值即为最大匹配数。
    • 匈牙利算法实现简单,复杂度 \(O(n \times e)\),适合 \(n, e \le 10^5\) 的情形;网络流方法通用性强,但代码稍长。
  3. 关键步骤(以匈牙利算法为例):

    • 读入:读取 \(n, m, e\),建立左部点 \(u\) 到右部点 \(v\) 的有向边邻接表。
    • 初始化match[1..m] = 0,表示右部点未匹配。
    • 对每个左部点 \(i\)(1 到 \(n\)
      • 清空 vis[1..m](本轮访问标记)。
      • 调用 dfs(i)
        • 遍历 \(i\) 的所有邻接右部点 \(v\)
          • vis[v] 为真则跳过。
          • 标记 vis[v] = true
          • match[v] == 0dfs(match[v]) 为真(即能为 \(v\) 当前匹配的左部点找到新的匹配),则将 match[v] = i,返回 true
        • 若所有邻接点均失败,返回 false
      • dfs(i) 为真,则匹配数 ans++
    • 输出:最大匹配数 ans
  4. 时间/空间复杂度

    • 匈牙利算法:时间复杂度 \(O(n \times e)\),空间复杂度 \(O(n + m + e)\)
    • Dinic 最大流:时间复杂度 \(O(E \sqrt{V})\)(二分图特殊性质),空间复杂度 \(O(V + E)\)
  5. 二分图匹配的核心思想

    • 增广路径:从一个未匹配的左部点出发,交替经过非匹配边和匹配边,最终到达一个未匹配的右部点,则找到一条增广路,沿此路反转匹配状态可使匹配数增加 1。
    • 匈牙利算法的递归性:当尝试匹配一个右部点时,若它已被匹配,则尝试为它的当前匹配点寻找新的匹配,若成功则当前点可以匹配,否则继续尝试其他点。
    • 复杂度保证:每个左部点最多尝试一次增广,每次尝试遍历其所有出边,总复杂度线性于 \(n \times e\)
    • 适用场景:任务分配、人员安排、二分图最大匹配问题。

【算法标签】

普及+ #二分图

【代码详解】

// 匈牙利算法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 505, M = 100010;
int n, m, k, a, b, ans;  // n: 男生数, m: 女生数, k: 边数, ans: 最大匹配数
struct edge
{
    int v, ne;  // v: 目标节点, ne: 下一条边
} e[M];
int h[N], idx;  // 邻接表
int vis[N], match[N];  // vis: 访问标记, match: 女生匹配的男生

void add(int a, int b)  // 添加有向边 a->b
{
    e[++idx] = {b, h[a]};
    h[a] = idx;
}
bool dfs(int u)  // 匈牙利算法
{
    for (int i = h[u]; i; i = e[i].ne)  // 遍历u的所有邻接点
    {
        int v = e[i].v;  // 妹子
        if (vis[v])  // 这个妹子已经被考虑过
        {
            continue;
        }
        vis[v] = 1;  // 先标记这个妹子
        if (!match[v] || dfs(match[v]))  // 妹子空闲或能腾出位置
        {
            match[v] = u;  // 配成对
            return 1;
        }
    }
    return 0;
}
int main()
{
    cin >> n >> m >> k;  // 输入男生数, 女生数, 边数
    for (int i = 0; i < k; i++)  // 输入每条边
    {
        cin >> a >> b;
        add(a, b);  // 建立有向边
    }
    for (int i = 1; i <= n; i++)  // 为每个男生找匹配
    {
        memset(vis, 0, sizeof vis);  // 重置访问标记
        if (dfs(i))  // 如果找到匹配
        {
            ans++;
        }
    }
    cout << ans;  // 输出最大匹配数
    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
const int N = 510, M = 100005;
int n1, n2, m;  // n1: 左部点数, n2: 右部点数, m: 边数
int h[N], e[M], ne[M], idx;  // 邻接表
int match[N];  // 匹配数组: match[j]=x表示右部点j匹配左部点x
bool st[N];  // 访问标记: 在本次find中右部点是否被访问

void add(int a, int b)  // 添加有向边 a->b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x)  // 匈牙利算法, 为左部点x找匹配
{
    for (int i = h[x]; i != -1; i = ne[i])  // 遍历x的所有邻接点
    {
        int j = e[i];  // 右部点j
        if (!st[j])  // 如果j在本轮未被访问
        {
            st[j] = true;  // 标记已访问
            if (match[j] == 0 || find(match[j]))  // 如果j未匹配或能为j的原匹配找到新匹配
            {
                match[j] = x;  // 匹配x和j
                return true;  // 找到匹配
            }
        }
    }
    return false;  // 未找到匹配
}
int main()
{
    cin >> n1 >> n2 >> m;  // 输入左部点数, 右部点数, 边数
    memset(h, -1, sizeof(h));  // 初始化邻接表
    while (m--)  // 输入边
    {
        int a, b;
        cin >> a >> b;
        add(a, b);  // 添加有向边
    }
    int res = 0;  // 最大匹配数
    for (int i = 1; i <= n1; i++)  // 为每个左部点找匹配
    {
        memset(st, false, sizeof(st));  // 清空访问标记
        if (find(i))  // 如果找到匹配
        {
            res++;
        }
    }
    cout << res << endl;  // 输出最大匹配数
    return 0;
}
#include <bits/stdc++.h>
using namespace std;
#define int long long           // 将 int 定义为 long long,防止流量溢出
const int N = 1005, M = 100005 * 2; // N: 最大点数, M: 最大边数(双向边,乘2)
int n, m, k, s, t;              // n: 左部点数, m: 右部点数, k: 边数, s: 源点, t: 汇点
int h[N], e[M], w[M], ne[M], idx;   // 链式前向星:h表头, e终点, w容量, ne下一条, idx当前边数
int d[N];                       // d[u]: 节点u在层次图中的层数(BFS构建)
int cur[N];                     // cur[u]: 当前弧优化,记录u当前遍历到的边

// 链式前向星:添加有向边,同时添加反向边(残量网络)
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// BFS构建层次图:从源点s出发,给每个节点分配层数
bool bfs()
{
    memset(d, 0, sizeof(d));    // 清空层数
    queue<int> q;
    q.push(s);
    d[s] = 1;                   // 源点层数为1

    while (q.size())
    {
        int u = q.front();
        q.pop();

        // 遍历u的所有出边
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int v = e[i];
            // v未访问且该边还有剩余容量
            if (d[v] == 0 && w[i])
            {
                d[v] = d[u] + 1;    // v的层数为u的层数+1
                q.push(v);          // v入队
                if (v == t)
                {
                    return true;    // 找到汇点,层次图构建成功
                }
            }
        }
    }
    return false;                   // 无法到达汇点,不存在增广路径
}

// DFS进行多路增广:从u出发,最多增广mf的流量
int dfs(int u, int mf)
{
    if (u == t)
    {
        return mf;                  // 到达汇点,返回增广流量
    }

    int sum = 0;                    // 从u出发总共增广的流量

    // 从当前弧开始遍历(当前弧优化:避免重复遍历已无效的边)
    for (int i = cur[u]; i != -1; i = ne[i])
    {
        cur[u] = i;                 // 更新当前弧

        int v = e[i];
        // 满足层次关系(v在u的下一层)且还有剩余容量
        if (d[v] == d[u] + 1 && w[i])
        {
            // 递归DFS,增广流量为mf和w[i]的较小值
            int f = dfs(v, min(mf, w[i]));

            // 更新残量网络
            w[i] -= f;              // 正向边容量减少
            w[i ^ 1] += f;          // 反向边容量增加

            sum += f;               // 累加增广流量
            mf -= f;                // 剩余可增广流量减少

            if (mf == 0)
            {
                break;              // 流量已用完,退出
            }
        }
    }

    // 如果从u出发无法增广任何流量,将u的层数置0(剪枝,后续不再访问)
    if (sum == 0)
    {
        d[u] = 0;
    }

    return sum;                     // 返回从u出发增广的总流量
}

// Dinic算法主函数:不断BFS建层次图,然后DFS多路增广
int dinic()
{
    int flow = 0;                   // 总流量

    while (bfs())                   // 当还存在增广路径时
    {
        memcpy(cur, h, sizeof(h));  // 重置当前弧为表头
        flow += dfs(s, 1e18);       // 从源点出发,尝试增广无穷大流量
    }

    return flow;                    // 返回最大流
}

signed main()                       // 使用 signed 替代 int,因为 #define int long long
{
    cin >> n >> m >> k;             // 读入左部点数、右部点数、边数

    // 初始化链式前向星
    memset(h, -1, sizeof(h));

    // 读入k条边,建立二分图的网络流模型
    while (k--)
    {
        int u, v;
        cin >> u >> v;
        // 左部点u连向右部点v(右部点编号偏移+n,避免冲突)
        // 容量为1(每条边最多匹配一次)
        add(u, v + n, 1);
        add(v + n, u, 0);           // 反向边
    }

    // 设置源点和汇点
    s = 0;                          // 源点编号0
    t = n + m + 1;                  // 汇点编号n+m+1

    // 源点s向所有左部点连边,容量为1(每个左部点最多匹配一次)
    for (int i = 1; i <= n; i++)
    {
        add(s, i, 1);
        add(i, s, 0);               // 反向边
    }

    // 所有右部点向汇点t连边,容量为1(每个右部点最多匹配一次)
    for (int i = 1; i <= m; i++)
    {
        add(i + n, t, 1);
        add(t, i + n, 0);            // 反向边
    }

    // 输出最大匹配数(即最大流)
    cout << dinic() << endl;

    return 0;
}

【运行结果】

1 1 1
1 1
1
posted @ 2026-09-01 22:40  团爸讲算法  阅读(19)  评论(0)    收藏  举报