题解:洛谷 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
【核心思想】
-
问题分析:给定一个二分图,左部有 \(n\) 个点,右部有 \(m\) 个点,以及 \(e\) 条边。需要求出二分图的最大匹配,即选择尽可能多的边,使得任意两条边没有公共端点。这是二分图匹配的经典问题,可通过匈牙利算法(增广路)或转化为网络最大流求解。
-
算法选择:
- 匈牙利算法(增广路思想):对每个左部点尝试寻找增广路,若找到则匹配数加 1。利用
match[v]记录右部点 \(v\) 当前匹配的左部点,vis[v]标记本轮搜索中右部点是否被访问,避免重复搜索。算法核心是递归尝试为冲突的匹配点重新找匹配。 - 网络流转化(Dinic):将二分图匹配建模为最大流:源点 \(s\) 连接所有左部点(容量 1),左部点连接右部点(容量 1),右部点连接汇点 \(t\)(容量 1),则最大流的值即为最大匹配数。
- 匈牙利算法实现简单,复杂度 \(O(n \times e)\),适合 \(n, e \le 10^5\) 的情形;网络流方法通用性强,但代码稍长。
- 匈牙利算法(增广路思想):对每个左部点尝试寻找增广路,若找到则匹配数加 1。利用
-
关键步骤(以匈牙利算法为例):
- 读入:读取 \(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] == 0或dfs(match[v])为真(即能为 \(v\) 当前匹配的左部点找到新的匹配),则将match[v] = i,返回true。
- 若
- 若所有邻接点均失败,返回
false。
- 遍历 \(i\) 的所有邻接右部点 \(v\):
- 若
dfs(i)为真,则匹配数ans++。
- 清空
- 输出:最大匹配数
ans。
-
时间/空间复杂度:
- 匈牙利算法:时间复杂度 \(O(n \times e)\),空间复杂度 \(O(n + m + e)\)。
- Dinic 最大流:时间复杂度 \(O(E \sqrt{V})\)(二分图特殊性质),空间复杂度 \(O(V + E)\)。
-
二分图匹配的核心思想:
- 增广路径:从一个未匹配的左部点出发,交替经过非匹配边和匹配边,最终到达一个未匹配的右部点,则找到一条增广路,沿此路反转匹配状态可使匹配数增加 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
浙公网安备 33010602011771号