二分图匹配和匈牙利算法

内容概要:

  1. 最大流算法解决二分图最大匹配
  2. 匈牙利算法
  3. LeetCode上一个困难问题:覆盖

匹配问题相关概念

该类问题的前提是图为二分图。关于二分图包括二分图检测在前面的文章已经讨论过了。
匹配:给定一个二分图G,在G的一个子图M中, M的边集{E}中的任意两条边都不交汇于同一个结点,则称M是一个匹配(Matching)。 简言之,在关注边的情况下,二分图G的一个匹配就是满足任意两条边之间都没有公共顶点的边的子集。
极大匹配:极大匹配(Maximal Matching)是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。
最大匹配:最大匹配(Maximum Matching)是所有极大匹配当中边数最多的一个匹配。
完全匹配:如果二分图G的一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配(Perfect Matching),也称作完备匹配。

最大流算法解决最大匹配问题

算法思路
最大匹配问题可以用最大流算法解决,关于最大流问题,在上篇文章已经讨论过了,最大流问题和Edmonds-Karp算法
最大流解决匹配问题的思路是这样的,首先建立一张新图:

构建图

该图在原图的基础上,增加一个源点s和汇点t,原二分图G的两部分分别连接s和t,方向如上图所示,每个边上的容量均为1,该图的最大流就是原二分图G的最大匹配数。这是因为源点发出的边的数目为二分图左侧顶点数,汇点的入边数为二分图右侧顶点数,由于每个边容量为1,在流量平衡限制下,源点进入左侧每个顶点的流量最多为1,左侧每个顶点进入右侧顶点流量最多为1,每个右侧顶点进入汇点的流量最多为1,这就意味着二分图G左侧的每个顶点最多和右侧的一个匹配上,没有匹配的不会构成可行流。
算法实现

public class BipartiteMatching {
    private Graph G;
    private int maxMatching;
    public BipartiteMatching(Graph G){
        BipartitionDetection bd = new BipartitionDetection(G);
        if(!bd.isBipartite())
            throw new IllegalArgumentException("Only Works On BipartitionGraph!");
        this.G = G;

        int []colors = bd.colors();
        // 源点编号 V, 汇点编号 V + 1
        WeightedGraph network = new WeightedGraph(G.V() + 2, true);
        for(int v = 0; v < G.V(); v ++){

            if(colors[v] == 0)// 颜色为0设为左侧顶点
                network.addEdge(G.V(), v, 1);
            else network.addEdge(v, G.V() + 1, 1);
            for(int w: G.adj(v))
                if(v < w) { // 这样方向不同的边只遍历一次
                    if(colors[v] == 0) network.addEdge(v, w, 1);
                    else network.addEdge(w, v, 1);
                }
        }

        MaxFlow mf = new MaxFlow(network, G.V(), G.V() + 1);
        maxMatching = mf.result();
    }

    public int maxMatching(){
        return maxMatching;
    }

    public static void main(String args[]){
        Graph g = new Graph("g.txt", false);
        BipartiteMatching bm = new BipartiteMatching(g);
        System.out.println(bm.maxMatching());
    }
}

匈牙利算法

匈牙利算法是另一个求解最大匹配的算法,它比使用最大流算法解决最大匹配效率要高,而且该算法的思想非常简单。
算法描述
从左侧的非匹配点出发,如果来到的右边点是一个匹配过的点,从右向左走只能走匹配过的边,这样走过的路径一定是匹配边和未匹配边交替出现的路径。如果上述过程最终终止于另外一个未匹配点(这个点只可能在右侧),此时称找到的路径为增广路径。
增广路径一定是包含偶数个顶点,奇数条边的路径,且起点和终点一定是未匹配状态。这样将匹配边和未匹配边互换就能多出一个匹配来,且原来匹配状态的点一定还是匹配状态,而多出的两个匹配点就是起点和终点。

以下图所示二分图为例:

匈牙利算法1

执行匈牙利算法,首先可以得到0-4一个匹配;接着左侧顶点1未匹配,从顶点1走到顶点4,顶点4已经是匹配状态,沿匹配边回到0,然后0走到6,本轮寻找终止,路径为1-4-0-6,这样得到1-4和0-6为匹配。(当然也可以走路径1-7,可以直接得到一个新的匹配,这里选1-4-0-6是为了讲述匈牙利算法的中间过程。)接着左侧顶点2未匹配,从顶点2走到顶点6,顶点6已经是匹配状态,沿匹配边回到0,然后0走到4,4沿匹配边回到1,1走到7,本轮寻找终止,路径为2-6-0-4-1-7,这样得到2-6、0-4、1-7为匹配。

匈牙利算法2

接着左侧顶点3未匹配,从顶点3走到顶点5,直接得到一个新的匹配,最大匹配寻找完成。(如果从顶点3走到顶点7,顶点7回到顶点1,顶点1走到4,4回到0,0走到6,6回到2,本轮寻找终止,路径为3-7-1-4-0-6-2,不是增广路径,不会有新的匹配产生。最终还是走3-5。)
算法实现(BFS实现)

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class Hungarian {
    private Graph G;
    private int maxMatching = 0;
    private int[] matching;// 记录匹配状态,-1 表示未匹配

    public Hungarian(Graph G){
        BipartitionDetection bd = new BipartitionDetection(G);
        if(!bd.isBipartite())
            throw new IllegalArgumentException("Only Works On BipartitionGraph!");
        this.G = G;

        int []colors = bd.colors();
        matching = new int[G.V()];
        Arrays.fill(matching, -1);

        for(int v = 0; v < G.V(); v ++)
            if(colors[v] == 0 && matching[v] == -1) // 左侧的点
                if(bfs(v)) maxMatching ++;
    }

    private boolean bfs(int v){
        Queue<Integer> q = new LinkedList<>();
        int pre[] = new int[G.V()];
        Arrays.fill(pre, -1);
        q.add(v);
        pre[v] = v;
        while(!q.isEmpty()){
            int cur = q.remove();
            for(int next: G.adj(cur))
                if(pre[next] == -1){
                    if(matching[next] != -1){
                        pre[next] = cur;
                        pre[matching[next]] = next;
                        q.add(matching[next]);
                    }else {
                        pre[next] = cur;
                        ArrayList<Integer> augPath = getAugPath(pre, v, next);
                        // 匹配边变换
                        for (int i = 0; i < augPath.size(); i += 2) {
                            matching[augPath.get(i)] = augPath.get(i + 1);
                            matching[augPath.get(i+1)] = augPath.get(i);
                        }
                        return true;
                    }
                }
        }
        return false;
    }

    private ArrayList<Integer> getAugPath(int[]pre, int start, int end){
        ArrayList<Integer> res = new ArrayList<>();
        int cur = end;
        while(cur != start){
            res.add(cur);
            cur = pre[cur];
        }
        res.add(cur);// 逆序没关系
        return res;
    }

    public int maxMatching(){
        return maxMatching;
    }

    public boolean isPerfectMacthing(){
        return maxMatching * 2 == G.V();
    }

    public static void main(String args[]){
        Graph g = new Graph("g.txt", false);
        Hungarian hg = new Hungarian(g);
        System.out.println(hg.maxMatching());
    }
}

当然匈牙利算法也可以用DFS来实现增广路径的寻找,由于使用了递归,代码会比BFS实现的要简洁。

import java.util.ArrayList;
import java.util.Arrays;

public class HungarianDFS {
    private Graph G;
    private int maxMatching = 0;
    private boolean[] visited;
    private int[] matching;// 记录匹配状态,-1 表示未匹配

    public HungarianDFS(Graph G){
        BipartitionDetection bd = new BipartitionDetection(G);
        if(!bd.isBipartite())
            throw new IllegalArgumentException("Only Works On BipartitionGraph!");
        this.G = G;

        int []colors = bd.colors();
        matching = new int[G.V()];
        Arrays.fill(matching, -1);
        visited = new boolean[G.V()];

        for(int v = 0; v < G.V(); v ++)
            if(colors[v] == 0 && matching[v] == -1) { // 左侧的点
                Arrays.fill(visited, false);
                if (dfs(v)) maxMatching++;
            }
    }

    private boolean dfs(int v){
        visited[v] = true;
        for(int u: G.adj(v))
            if(!visited[u]){
                visited[u] = true;
                if(matching[u] == -1 || dfs(matching[u])){
                    matching[v] = u;
                    matching[u] = v;
                    return true;
                }
            }
        return false;
    }

    public int maxMatching(){
        return maxMatching;
    }

    public boolean isPerfectMacthing(){
        return maxMatching * 2 == G.V();
    }

    public static void main(String args[]){
        Graph g = new Graph("g.txt", false);
        HungarianDFS hg = new HungarianDFS(g);
        System.out.println(hg.maxMatching());
    }
}

最大匹配的一个应用实例:覆盖

LCP4
例子

问题分析
如果将棋盘格按照黑白进行染色,相邻的格子不同色,则一个多米诺骨牌必然占据相邻的一个白格子和一个黑格子。这样就可以把问题看做一个二分图的最大匹配,黑白分别对应二分图的两部分。在问题所抽象到的二分图中,每个格子是一个顶点,每个顶点和其上下左右的四个顶点相连。
问题解答
将问题抽象出的图的邻接集合建立出来,然后使用匈牙利算法求最大匹配。


class Solution {
    TreeSet<Integer> g[];// 用于构建图
    int colors[]; // 图 g 二分后的各个顶点颜色 0 或者 1
    boolean visited[];
    int matching[]; // 已经匹配的
    int maxMatch = 0; // 最大匹配数
    
    public int domino(int n, int m, int[][] broken) {

        int[][] board = new int[n][m];
        g = new TreeSet[m*n];
        colors = new int[m*n];
        Arrays.fill(colors, -1); // 没有被染色

        visited = new boolean[m*n];
        matching = new int[m*n];
        Arrays.fill(matching, -1);

        for(int i = 0; i < m * n; i ++)
            g[i] = new TreeSet<>();
        for(int[] p: broken)  board[p[0]][p[1]] = 1; // 为 1 表示不可放骨牌,为 0 可以

        for(int i = 0; i < n; i ++)
            for(int j = 0; j < m; j ++){
                if(j + 1 < m && board[i][j] == 0 && board[i][j+1] == 0) {
                    g[i * m + j].add(i * m + j + 1);
                    g[i * m + j + 1].add(i * m + j);
                }
                if(i + 1 < n && board[i][j] == 0 && board[i+1][j] == 0) {
                    g[i * m + j].add((i + 1) * m + j);
                    g[(i + 1) * m + j].add(i * m + j);
                }
            }
        for(int v = 0; v < m * n; v ++)
            if(!visited[v])
                dfs(v, 0);

        for(int v = 0; v < m*n; v ++)
            if(colors[v] == 0 && matching[v] == -1) { // 左侧的点
                Arrays.fill(visited, false);
                if (Hungar(v)) maxMatch ++;
            }

        return maxMatch;
    }
    private void dfs(int v, int color){
        // 预处理出colors数组,即二分图的两个部分
        colors[v] = color;
        visited[v] = true;
        for(int w: g[v]){
            if(!visited[w]) dfs(w, 1 - color);
        }
    }

    private boolean Hungar(int v){
        visited[v] = true;
        for(int u: g[v])
            if(!visited[u]){
                visited[u] = true;
                if(matching[u] == -1 || Hungar(matching[u])){
                    matching[v] = u;
                    matching[u] = v;
                    return true;
                }
            }
        return false;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342