861 二分图的最大匹配(匈牙利算法找二分图的最大匹配)

1. 问题描述:

给定一个二分图,其中左半部包含 n1 个点(编号 1~n1),右半部包含 n2 个点(编号 1~n2),二分图共包含 m 条边。数据保证任意一条边的两个端点都不可能在同一部分中。请你求出二分图的最大匹配数。二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

输入格式

第一行包含三个整数 n1、 n2 和 m。接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。

输出格式

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

数据范围

1 ≤ n1,n2 ≤ 500,
1 ≤ u ≤ n1,
1 ≤ v ≤ n2,
1 ≤ m ≤ 10 ^ 5

输入样例:

2 2 4
1 1
1 2
2 1
2 2

输出样例:

扫描二维码关注公众号,回复: 13380806 查看本文章

2
来源:https://www.acwing.com/problem/content/description/863/

2. 思路分析:

分析题目可以知道这是二分图最大匹配的裸题,二分图的最大匹配属于特殊的最大流问题,可以使用最大流来解决,也可以使用匈牙利算法来解决,一般可以使用二分图来解决的问题都可以使用最大流来解决,下面使用匈牙利算法解决二分图的最大匹配问题,其中涉及到匈牙利算法的某些概念:

  • 匹配:在图论中,一个匹配是一个边的集合,其中任意两条边都没有公共顶点。
  • 最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
  • 完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。显然,完美匹配一定是最大匹配,完美匹配的任何一个点都已经匹配
  • 交替路径:从一个非匹配点出发,依次经过非匹配边、匹配边、非匹配边...形成的路径叫交替路
  • 增广路径:从一个非匹配点出发,依次经过非匹配边,匹配边,非匹配边,匹配边...非匹配点形成的路径

其中有一个比较重要的结论是:最大匹配 <==> 不存在增广路径,这个结论的充分性比较好证明,必要性比较复杂,我们在用的时候记住这个结论即可。我们可以将找匹配的过程看成是匹配情侣的过程,我们的目标是尽可能匹配更多的情侣,下面举一个例子就比较清楚了,我们需要找出下面二分图的最大匹配,左图为原图,右图为最大匹配图:

首先从左-1开始匹配,发现左-1喜欢的妹子右-1还没有匹配那么将右-2分配给左-1,接下来继续看左-2,发现左-2喜欢的右-2的妹子也还没匹配,此时将右-2的妹子发给左-2,接下来看左-3,此时有点尴尬,发现喜欢的右-1与右-2妹子都已经和左-1和左-2匹配上了,为了尽可能匹配更多的情侣,我们尝试将左-1匹配的妹子右-1分配给左-3,看左-1还喜欢哪个妹子并且还没有匹配的情况下将其分配给左-1,可以发现左-1喜欢右-2的妹子,但是这个时候右-2已经和别人匹配了,我们可以使用同样的方法看能否重新给左-2分配一个未分配并且是左-2喜欢的妹子,可以发现左-2还喜欢右-3的妹子并且还没有匹配,此时将右-3分配给左-2,右-2分配给左-1,右-1分配给左-3,这样就完成了3个匹配,对于左-4来说,喜欢的右-3妹子已经和别人匹配了此时可以发现重新分配之后也不能够满足将右-3的妹子分配给左-4了,此时左-4落单了,那么最大匹配数为3。可以发现在重新修改匹配对象的过程其实是一个递归的过程,使用相同的方法对已经分配的对象进行修改,所以匈牙利算法也是基于递归来实现的,使用一个match数组来存储右边的点集对应的是左边哪一个点,比如match[2] = 3表示右边2号点与左边3号点匹配,另外一个数组st标记右边的点集是否被访问,调用匈牙利算法的时候基于这样一个原则,若a<-->b而且b未匹配那么直接将b分配给左边的a,否则看当前右边b的匹配是谁,看是否可以重新分配对象给b的左边的匹配,这其实是一个递归的过程,如果可以重新分配对象那么就可以将b分配给a。

3. 代码如下:

python:

from typing import List


class Solution:
    # 本质上可以看成是尽力匹配更多的情侣
    def find(self, u: int, st: List[int], match: List[int], g: List[List[int]]):
        for next in g[u]:
            # 右边的点集未访问
            if st[next] == 0:
                # 标记已访问
                st[next] = 1
                # 右边的点未匹配那么直接匹配或者重新分配之前已经分配的点成功那么就将右边的点next分配给u这个点(需要调整的是之前已经分配好对象的进行调整)
                if match[next] == 0 or self.find(match[next], st, match, g):
                    # 将右边的next分配给u, 递归的时候会重新分配对象
                    match[next] = u
                    return True
        return False

    def process(self):
        n1, n2, m = map(int, input().split())
        g = [list() for i in range(n1 + 10)]
        for i in range(m):
            a, b = map(int, input().split())
            # 匈牙利算法一般连的是单向边
            g[a].append(b)
        match = [0] * (n2 + 10)
        res = 0
        for i in range(1, n1 + 1):
            # 因为每一次的分配的情况可能改变所以需要再初始化一遍
            st = [0] * (n2 + 10)
            # 每一次对于当前的点找匹配, 判断是否可以分配一个对象给它
            if self.find(i, st, match, g):
                res += 1
        return res


if __name__ == "__main__":
    print(Solution().process())

猜你喜欢

转载自blog.csdn.net/qq_39445165/article/details/121444589