算法学习笔记:二分图#2——匈牙利算法

算法学习笔记:二分图#2——匈牙利算法

1. 前言

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

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

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

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

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

懒得百度?传送门:算法学习笔记:二分图#1——定义+性质+判定

2. 例题

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

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

在这里插入图片描述

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

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

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

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

先给 A 1 A_1 A1 分菜。 A 1 A_1 A1 喜欢吃 B 1 B_1 B1,那么就将 B 1 B_1 B1 分配给 A 1 A_1 A1

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

在这里插入图片描述

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

A 2 A_2 A2:“我说 A 1 A_1 A1 呀,我也想吃 B 1 B_1 B1,您就不能换一道菜吗?”

A 1 A_1 A1:“啊这?好吧, B 1 B_1 B1 给你,我吃 B 3 B_3 B3。”

于是当前分配如下:

在这里插入图片描述

现在考虑 A 3 A_3 A3。糟糕, A 3 A_3 A3 也想吃 B 1 B_1 B1

A 3 A_3 A3 :“ A 2 A_2 A2 在吗?您可不可以换一道菜啊,我也想吃 B 1 B_1 B1。”

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

于是 A 3 A_3 A3 没有吃到 B 1 B_1 B1,但是 A 3 A_3 A3 还喜欢 B 4 B_4 B4 啊!于是 A 3 A_3 A3 选择了 B 4 B_4 B4 这道菜。

那么当前分配如下:

在这里插入图片描述

最后是 A 4 A_4 A4 A 4 A_4 A4 也想吃 B 1 B_1 B1,而且也只喜欢 B 1 B_1 B1。很遗憾的是, B 1 B_1 B1 已经被 A 2 A_2 A2 选走了,且 A 2 A_2 A2 不愿意换。

最后, A 4 A_4 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;
}

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

考虑一下 A 1 A_1 A1 A 2 A_2 A2 的对话。

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

( A 2 , B 1 ) (A_2,B_1) (A2,B1) 为一条 非匹配边

( A 1 , B 1 ) (A_1,B_1) (A1,B1) 为一条 匹配边

( A 1 , B 3 ) (A_1,B_3) (A1,B3) 为一条 非匹配边 B 3 B_3 B3非匹配点

于是:

路径 A 2 − > B 1 − > A 1 − > B 3 A_2->B_1->A_1->B_3 A2>B1>A1>B3 为一条 增广路

增广路!

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

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

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

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

于是正确性证完了。

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

而代码中的 M a t c h Match Match 数组就是表示匹配, M a t c h i Match_i Matchi 表示 ( i , M a t c h i ) (i,Match_i) (i,Matchi) 是一条匹配边。

3. 总结

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值