zoukankan      html  css  js  c++  java
  • 二分图的最大匹配、完美匹配和匈牙利算法

    这篇文章讲无权二分图的最大匹配和完美匹配,以及用于求解匹配的匈牙利算法

    二分图:

    简单来说,如果图中的点可以被分为两组,并且使得所有边都跨越组的边界,那么这就是一个二分图。准确地说:把一个图的顶点划分为两个不相交集合(U)(V),使得每一条边都分别连接(U、V)中的顶点,如果存在这样的划分,那么这个图为一个二分图。二分图的一个等价定义是:不含有「含奇数条边的环」的图。

    下图中,图一为一个二分图,为了清晰,画成图二这种形式。

    匹配:

    在图论中,一个"匹配"是一个边的集合,其中任意两条边都没有公共点。

    下图中图三,图四中红色的边就是图二的匹配。


    我们定义匹配点、匹配边、未匹配点、非匹配边,他们的含义非常容易理解。

    上图中,图三中1、4、5、7为匹配点,其他点为未匹配点;1-5、4-7为匹配边,其他边为非匹配边。

    最大匹配:

    一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。上图中,图四是一个最大匹配,它包含四条匹配边。

    完美匹配:

    如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。上图中,图四是一个完美匹配。那么显然完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条线的匹配边一定会以已有的匹配边冲突)。

    并非每个图都存在完美匹配。

    举个栗子:

    下图中,如果在某一对男孩和女孩之间存在相连的边,就意味着他们彼此喜欢。问题一:是否可能让所以男孩和女孩两两配对,使得所有人都互相喜欢呢?

    这就是完美匹配问题。

    问题二:最多有多少互相喜欢的男女生可以配对?

    这就是最大匹配问题。

    最大匹配数:

    最大匹配的匹配边的数目。

    最小点覆盖数:

    选取最少的点,是任意一条边至少有一个端点被选择。

    最大独立数:

    选取最多的点,使任意所选两点均不相连。

    最小路径覆盖数:

    对于一个DAG,选取最少条路径,是的每个顶点属于且仅属于一条路径。路经长可以为0(即单个点)。

    几个定理:

    • 最大匹配数 = 虽小点覆盖数
    • 最大匹配数 = 最大独立数
    • 最小路径覆盖数 = 顶点数 - 最大匹配数

    以上为基本概念,下面引入求解最大匹配问题的一个算法匈牙利算法,先引入一些概念为算法服务。

    交替路:

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

    增广路:

    从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),那么这条交替路称为增广路。上图中,图五的一条增广路如下图所示(匹配点为红色)

    增广路有一个重要的特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多一条。

    我们通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(增广路定理)。匈牙利算法就是这么做的。

    同时说一下什么是匈牙利树。

    匈牙利树一般由BFS构造(类似于BFS树)。从一个未匹配点出发进行BFS(唯一的限制是,必须走交替路),直到不能再拓展为止。下图中,图七可以得到图八的一棵BFS树。


    但是这可树存在一个叶子节点为非匹配点(7号),但是匈牙利树要求所有叶子节点均为匹配点,因此这不是一棵匈牙利树。如果原图中根本不含7号点,那么2号节点出发就会得到一棵匈牙利树。这种情况如图九所示。

    下面给出匈牙利算法的DFS和BFS

    // 顶点、边的编号均从 0 开始
    // 邻接表储存
    // c++11
    
    struct Edge{
        int from;
        int to;
        int weight;
    
        Edge(int f, int t, int w):from(f), to(t), weight(w) {}
    };
    
    vector<int> G[__maxNodes]; /* G[i] 存储顶点 i 出发的边的编号 */
    vector<Edge> edges;
    int num_nodes;
    int num_left;
    int num_right;
    int num_edges;
    
    int matching[__maxNodes]; /* 存储求解结果 */
    int check[__maxNodes];
    
    bool dfs(int u){
        for (auto i = G[u].begin(); i != G[u].end(); ++i) { // 对 u 的每个邻接点
            int v = edges[*i].to;
            if (!check[v]) {     // 要求不在交替路中
                check[v] = true; // 放入交替路
                if (matching[v] == -1 || dfs(matching[v])) {
                    // 如果是未盖点,说明交替路为增广路,则交换路径,并返回成功
                    matching[v] = u;
                    matching[u] = v;
                    return true;
                }
            }
        }
        return false; // 不存在增广路,返回失败
    }
    
    int hungarian(){
        int ans = 0;
        memset(matching, -1, sizeof(matching));
        for (int u = 0; u < num_left; ++u) {
            if (matching[u] == -1) {
                memset(check, 0, sizeof(check));
                if (dfs(u))
                    ++ans;
            }
        }
        return ans;
    }
    
    ---------------------下面是BFS----------------------------
    
    queue<int> Q;
    int prev[__maxNodes];
    int Hungarian(){
        int ans = 0;
        memset(matching, -1, sizeof(matching));
        memset(check, -1, sizeof(check));
        for (int i = 0; i < num_left; ++i) {
            if (matching[i] == -1) {
                while (!Q.empty()) Q.pop();
                Q.push(i);
                prev[i] = -1; // 设 i 为路径起点
                bool flag = false; // 尚未找到增广路
                while (!Q.empty() && !flag) {
                    int u = Q.front();
                    for (auto ix = G[u].begin(); ix != G[u].end() && !flag; ++ix) {
                        int v = edges[*ix].to;
                        if (check[v] != i) {
                            check[v] = i;
                            Q.push(matching[v]);
                            if (matching[v] >= 0) { // 此点为匹配点
                                prev[matching[v]] = u;
                            } else { // 找到未匹配点,交替路变为增广路
                                flag = true;
                                int d = u, e = v;
                                while (d != -1) {
                                    int t = matching[d];
                                    matching[d] = e;
                                    matching[e] = d;
                                    d = prev[d];
                                    e = t;
                                }
                            }
                        }
                    }
                    Q.pop();
                }
                if (matching[i] != -1) ++ans;
            }
        }
        return ans;
    }
    

    匈牙利算法要点

    1、从左边第一个顶点开始,挑选未匹配点进行搜索,寻找增广路。

    • 如果经过一个未匹配点,说明寻找成功。跟新路径信息,匹配边数+1,停止搜索。
    • 如果一直没有找到增广路,则不再从这个点开始搜索。事实上,此时搜索后会形成一个匈牙利树。我么可以永久性地把它从图中删去,而不影响结果。

    2、由于找到增光路之后需要眼路径更新匹配,所以我么需要一个结构来记录路径上的点。DFS版本通过函数调用使用一个栈,而BFS版本使用(prev)数组。

    性能对比

    两个版本的时间复杂度都是(O(V*E))。DFS优点是思路清晰、代码量少,但是性能不如BFS。

    经过测试两种算法性能之后得出如下结论。

    对于稀疏图,BFS版本明显快于DFS版本;

    对于稠密图两者不相上下。

    在完全随机数据 9000 个顶点 4,0000 条边时BFS领先DFS大约 97.6%,9000 个顶点 100,0000 条边时BFS领先DFS 8.6%, 而达到 500,0000 条边时 BFS 仅领先 0.85%

  • 相关阅读:
    poj 3321 Apple Tree
    hdu 1520 Anniversary party
    Light OJ 1089 Points in Segments (II)
    Timus 1018 Binary Apple Tree
    zoj 3299 Fall the Brick
    HFUT 1287 法默尔的农场
    Codeforces 159C String Manipulation 1.0
    GraphQL + React Apollo + React Hook 大型项目实战(32 个视频)
    使用 TypeScript & mocha & chai 写测试代码实战(17 个视频)
    GraphQL + React Apollo + React Hook + Express + Mongodb 大型前后端分离项目实战之后端(19 个视频)
  • 原文地址:https://www.cnblogs.com/excellent-zzy/p/12288617.html
Copyright © 2011-2022 走看看