二分图完全匹配算法之匈牙利算法

最近在参加了一个小项目,里面用到二分图匹配算法,因为之前并没有接触过相关算法,于是找到一些博客进行了一番学习,但学习之后,发现部分博客或多或少存在一些另我疑惑的地方,于是,我打算写此博客以巩固对算法的理解。

一、二分图基本概念

二分图(Bipartite graph)又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。

通俗一点,二分图是一类特殊的图,它可以被划分为两个部分,每个部分内的点互不相连。

如下图所示,可以将【2】【4】结点视为一个集合,【1】【3】【5】【6】结点视为一个集合,集合内部的结点互不相连,因此该图为一个二分图。

在这里插入图片描述

重要定理:G为二分图的充要条件是G中的每一个圈的长度都是偶数。

二、二分图匹配相关概念

1、什么是匹配?

注意,本文只讨论二分图的最大匹配,不讨论最优匹配。

给定一个二分图G=(V,E),在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配;具有边数量最多的匹配称为最大匹配;若|V|=2|M|,则称M为完美匹配

如下所示为一个二分图的完美匹配:

二分图完美匹配一定是最大匹配,最大匹配不一定是完美匹配。

2、什么是交替路与增广路?

交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。

增广路(Augmenting Path):从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发点不算),则这条【交替路】称为增广路。

增广路的三个性质:

  1. M为G的最大匹配当且仅当不存在M的增广路径。
  2. 增广路P的路径长度必定为奇数,第一条边和最后一条边都不属于M,不匹配的边比匹配的边多一。
  3. 不断寻找增广路可以得到一个更大的匹配M’,直到找不到更多的增广路。

增广路的这三个性质可以用来寻找匹配,在接下来会说明。

如下图所示:

路径2->A->1->B为一条交替路,因为出发点2途径点B均为未匹配点,因此该路径也为增广路

三、匈牙利算法

1、基本概念

匈牙利算法的核心就是寻找增广路。具体而言,匈牙利算法从二分图中没有匹配的点开始寻找增广路径(增广路径性质3)。找到增广路后,根据增广路径的性质,显然路径里没被匹配的连线比已经匹配了的连线多一条(增广路径性质2),于是对增广路径中的边进行取反操作,这样匹配数就比原来多1个。不断执行上述操作,直到找不到增广路径为止,无法找到增广路径说明已达到最大匹配(增广路径性质1)。

取反操作:将增广路的匹配边变成不匹配边,不匹配边变成匹配边。

如上图所示:

  1. 首先从未匹配点1开始寻找增广路,找到了另一个未匹配点A说明找到了增广路,停止寻找,进行取反操作,将非匹配边变为匹配边,于是1-A为匹配边。
  2. 未匹配点2开始寻找增广路,显然2->A->1->B为增广路,进行取反,于是此时匹配边为2-A、1-B
  3. 未匹配点3开始寻找增广路,显然3->A->2->C为增广路,进行取反,于是匹配边变为3-A、2-C、1-B。因为已经不存在未匹配点,此时已经找不到更多增广路径,因此达到了此二分图的最大匹配。

注:显然,二分图匹配结果并不唯一。

以上就是匈牙利算法的过程,很明显,搜寻增广路径这个步骤是基于深度优先搜索的。

匈牙利算法寻找增广路的伪代码描述:

//以下为寻找增广路并进行取反操作的伪代码,取反操作通过递归调用完成。  
while(找到Xi的关联顶点Yj){
      if(顶点Yj不在增广路径上){Yj加入增广路;
           //如果Yj是未匹配点说明成功找到增广路,从“Yj处继续寻找能够找到增广路”属于递归调用本函数过程,进行取反操作
           if(Yj是未匹配点||Yj处继续寻找能够找到增广路){Yj的匹配点改为Xi;
                  return true;
           }
      }
      return false;
}

2、匈牙利算法的实现

网上搜索匈牙利算法,从思想上看都是基于dfs的,另外,我注意到也有一些博客提出了基于广度优先(bfs)的匈牙利算法,我不确定所谓“基于bfs的匈牙利算法”能否称为匈牙利算法,我个人的理解是匈牙利算法就是基于dfs的来寻找增广路径而实现的。

2.1、实际问题

在这里插入图片描述

问题描述

RPG girls今天和大家一起去游乐场玩,终于可以坐上梦寐以求的过山车了。可是,过山车的每一排只有两个座位,而且还有条不成文的规矩,就是每个女生必须找个个男生做partner和她同坐。但是,每个女孩都有各自的想法,举个例子把,Rabbit只愿意和XHD或PQK做partner,Grass只愿意和linle或LL做partner,PrincessSnow愿意和水域浪子或伪酷儿做partner。考虑到经费问题,boss刘决定只让找到partner的人去坐过山车,其他的人,嘿嘿,就站在下面看着吧。聪明的Acmer,你可以帮忙算算最多有多少对组合可以坐上过山车吗?

输入

输入数据的第一行是三个整数K , M , N,分别表示可能的组合数目,女生的人数,男生的人数。0<K<=1000

1<=N 和M<=500.接下来的K行,每行有两个数,分别表示女生Ai愿意和男生Bj做partner。最后一个0结束输入。

输出:对于每组数据,输出一个整数,表示可以坐上过山车的最多组合数。

样例输入:6 3 3 1 1 1 2 1 3 2 1 2 3 3 1 0

样例输出:3

这道题本质上就是求二分图的最大匹配问题。

2.2、代码实现

Java实现

此代码转载自https://wmathor.com/index.php/archives/1344///***//表示是我添加的注释。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int[][] map;
    static int n, m;

    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        while (cin.hasNext()) {
            int t = cin.nextInt();
            if (t == 0)
                break;
            n = cin.nextInt();
            m = cin.nextInt();
            map = new int[n + 1][m + 1];
            for (int i = 0; i < t; i++)
                map[cin.nextInt()][cin.nextInt()] = 1; // 有向边
            int count = 0; // 最大匹配数
            int[] mc = new int[m + 1]; // mc[i] = j 表示i号男生所连的女生是j号
            Arrays.fill(mc, -1); // 初始时所有女生都没有连
            //***//以下for循环是核心部分,表示依次从未匹配点开始寻找增广路径
            for (int i = 1; i <= n; i++) {
                //***//将增广路径中的结点记录下来,以免重复,因此每一次循环都必须重置。
                boolean[] vis = new boolean[m + 1]; // vis[i] = true 表示i号男生已经被匹配了
                //***//根据增广路径性质2、3,每找到一条增广路径就说明找到了一条匹配边
                if (dfs(i, vis, mc))
                    count++;
            }
            System.out.println(count);
        }
    }

    //***//注:这个是寻找增广路径的代码,可以结合之前的伪代码,每找到一条增广路径就说明找到了一条匹配边。
    private static boolean dfs(int start, boolean[] vis, int[] mc) {
        for (int i = 1; i <= m; i++) { // 枚举男生集
            if (!vis[i] && map[start][i] == 1) { // 如果这个男生没被匹配并且和当前的start有边相连
                vis[i] = true; // 将这个男生标记为匹配过
                //***//以下判断语句中dfs递归调用,对应匈牙利算法中寻找增广路径并取反的操作,大家可以试着手动推理一下
                if (mc[i] == -1 || dfs(mc[i], vis, mc)) { // 这个男生没有和女生匹配 || 这个男生所连的女生还有别的选择,就把这个男生"腾"出来
                    mc[i] = start;
                    return true;
                }
            }
        }
        return false;
    }
}

Go:

有时间再把go的代码实现并添加过来,先画个饼。

四、总结

匈牙利算法的核心在于找增广路径,注意增广路径的三个性质。文中伪代码部分可以当成是解决这类问题的模板。

本文内容也是我自己对于算法的理解,可能存在错误,欢迎批评指正。

五、参考博客

https://blog.csdn.net/lemonxiaoxiao/article/details/108672039

https://zhuanlan.zhihu.com/p/208596378

https://www.cxyxiaowu.com/874.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值