图论专题-学习笔记:匈牙利算法

1. 前言

本篇博文将会专门讲述匈牙利算法的具体思路,实现过程以及正确性证明。

匈牙利算法是在 O(n×e+m) 内的时间内寻找二分图的最大匹配的一种算法,其中 n 为左部点个数,m 为右部点个数。

在学习匈牙利算法之前,请先确保掌握以下名词:

  1. 二分图
  2. 匹配与最大匹配
  3. 增广路

如果对上述部分名词没有掌握,请先掌握后再来学习。

懒得百度?传送门:图论专题-学习笔记:二分图基础

2. 例题

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

由于自然语言描述匈牙利算法难懂且难表述,直接采用图示方法讲解。

在这里插入图片描述

(绘图工具:Windows 画图软件)

为了理解方便,我们假设左边的 A1A4 表示 4 个人,这 4 个人去吃饭,B1B4 表示 4 道菜,每个人都有自己喜欢的菜,而连线就表示喜欢的菜。

没错 B2 因为太难吃了以至于没人喜欢

每个人至多只能选择一道菜。

先给 A1 分菜。A1 喜欢吃 B1,那么就将 B1 分配给 A1

那么分配如下所示(红色边就是匹配):

在这里插入图片描述

再给 A2 分菜。A2 也喜欢吃 B1,于是就有了这样一段对话:

A2:“我说 A1 呀,我也想吃 B1,您就不能换一道菜吗?”

A1:“啊这?好吧,B1 给你,我吃 B3。”

于是当前分配如下:

在这里插入图片描述

现在考虑 A3。糟糕,A3 也想吃 B1

A3 :“A2 在吗?您可不可以换一道菜啊,我也想吃 B1。”

A2 :“啊这?但是我只喜欢这一道菜,根据先到先得原则,我不能让给您。”

于是 A3 没有吃到 B1,但是 A3 还喜欢 B4 啊!于是 A3 选择了 B4 这道菜。

那么当前分配如下:

在这里插入图片描述

最后是 A4A4 也想吃 B1,而且也只喜欢 B1。很遗憾的是,B1 已经被 A2 选走了,且 A2 不愿意换。

最后,A4 谁都没选到。

因此结果为 3。

你会发现,实际上上面的所有过程就是寻找最大匹配的过程。

简单总结一下:

  1. 如果新来的人想选择一道菜且这道菜没有被选,那么就选上。
  2. 如果想选的菜冲突了,以前的换一道菜。
  3. 但是如果以前的菜不能换,那么新来的人只能换一道菜。
  4. 如果新来的人想选的菜都选不了,那么就别吃了。

有关该算法的系统语言描述以及正确性证明见代码后面。

代码(推荐先将后面理论看完再看代码):

/*
========= Plozia =========
	Author:Plozia
	Problem:P3386 【模板】二分图最大匹配
	Date:2021/3/14
========= Plozia =========
*/

#include <bits/stdc++.h>
using std::vector;

typedef long long LL;
const int MAXN = 500 + 10;
int n, m, e, Match[MAXN], ans;
vector <int> Next[MAXN];
bool book[MAXN];

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return (fh == 1) ? sum : -sum;
}

bool dfs(int k)
{
	for (int i = 0; i < Next[k].size(); ++i)
	{
		int u = Next[k][i];
		if (book[u]) continue;
		book[u] = 1;
		if (!Match[u] || dfs(Match[u])) {Match[u] = k; return 1;}//寻找增广路
	}
	return 0;
}

void Hungary()
{
	memset(Match, 0, sizeof(Match));
	for (int i = 1; i <= n; ++i)
	{
		memset(book, 0, sizeof(book));//注意重置
		if (dfs(i)) ++ans;
	}
}

int main()
{
	n = read(), m = read(), e = read();
	for (int i = 1; i <= e; ++i)
	{
		int u = read(), v = read();
		Next[u].push_back(v);
	}
	Hungary();
	printf("%d\n", ans);
	return 0;
}

那么上面算法的本质是什么呢?

考虑一下 A1A2 的对话。

我们发现,在 A2 还没有匹配之前,A2非匹配点

(A2,B1) 为一条 非匹配边

(A1,B1) 为一条 匹配边

(A1,B3) 为一条 非匹配边B3非匹配点

于是:

路径 A2>B1>A1>B3 为一条 增广路

增广路!

在二分图#1(link)中作者提到过增广路有一条重要性质:

  • 当增广路上非匹配边比匹配边数量大一,那么将非匹配边改为匹配边,匹配边改为非匹配边,那么该路径依然是增广路而且匹配数加一。

于是我们再结合上面的图示来理解,正确性就显然了。

根据这个性质,在保证路径不变的情况下我们能够尽可能的增加匹配数,而最大匹配一定在若干条增广路上,且增广路上匹配数达到最大。

于是正确性证完了。

因此,匈牙利算法的本质就是不断寻找增广路来扩大匹配。

而代码中的 Match 数组就是表示匹配,Matchi 表示 (i,Matchi) 是一条匹配边。

3. 总结

匈牙利算法的本质就是不断寻找增广路来扩大匹配。

posted @   Plozia  阅读(701)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示