• 学习:图论----二分图


    二分图是一类比较特殊的图,图内所有的环上节点的个数是偶数(二分图中只有偶环),属于图的一种定义,除以之外还有二分图匹配,最大匹配,最大权匹配等算法。

    二分图的定义


      二分图内的所有环都为偶环,即在一个有向图 G=(V, E) 中,所有的顶点可以分成两个集合,使得所有的边全部都满足两边的顶点分别位于不同的集合。

    下面的图都是二分图

    证明图 G=(V,E) 中所有环都为偶环是图 G=(V,E) 是二分图的充要条件:

    1.必要性(如果G=(V,E)是二分图,则图G内所有环全为偶环)

      设二分图G为G=(X,Y,E),其中X,Y为图中顶点拆分为的两个集合,设环C为 $left{v_0, v_1, v_2, ..., v_k ight}$,则环的长度为 $k$,根据二分图性质可以得 $v_i$ 相间的出现与 X 和 Y 中,假如设 $left{v_0, v_2, v_4, ...., v_k ight}subseteq X, left{v_1, v_3, v_5, ..., v_{k-1} ight}subseteq Y$,则 $k$ 为偶数,所以环C是偶环。

    2.充分性(如果图G=(V,E)内的环全为偶环,则G为二分图)

      在图G中任取一个点 $v$ ,将图中所有点划分为两个集合,集合 $X= left{v_i|v_i 与 v 的距离为偶数 ight}$,$Y=E-X$,假设存在一条边 $(v_i, v_j)$ ,且 $v_i, v_j$ 都是 $X$ 集合内的点,那么会形成 $v$ 到 $v_i$ 到 $v_j$ 到 $v$ 的一个环,很明显环的长度为奇数不满足集合 $X$ 的条件,则说明边 $(v_i, v_j)$ 不存在,同理对于集合 $Y$ 也可以证明集合内任意两个点之间没有边,说明图G是一个二分图(分为X集合和Y集合)


    图的匹配与二分图的匹配


      图的匹配到底指什么了?假设对于一个图G=(V,E),刚开始图中的顶点都是未匹配的顶点,如果我们任取一个未匹配的顶点,假设存在另一个未匹配的顶点,使得两个顶点之间有一条边连接,于是这两个未匹配的顶点可以通过这条边匹配起来,这就叫构成一个图的匹配

      很明显,二分图的顶点可以分成两个集合 $X$ 与 $Y$,对于集合 $X$ 内的任意一个点,如果要形成一个匹配,那么只能在集合 $Y$ 中寻找一个未匹配的点来尝试构成匹配。这点是二分图匹配较一般图匹配来说较未容易的地方。

      每个点都有可能找到与他匹配的一个点,但是我们可能尽可能的在一个二分图中找到尽量多的点对匹配。当找到了最多的点对匹配,我们就称这个是此二分图的最大匹配。如下图所示

      当然,一个二分图的最大匹配不是惟一


    二分图最大匹配与匈牙利算法


      很多时候,我们只需要算二分图的最大匹配数,这个时候就需要用到匈牙利算法。

      首先要引入几个概念,设 $G=(V,E)$ 是一个无向图。如顶点集 $V$ 可分割为两个互不相交的子集 $V_1, V_2$,选择这样的子集中边数最大的子集为图的最大匹配问题。

      如果一个匹配中,$|V_1|le |V_2|$ 且匹配数 $|M|=|V_1|$,则称此匹配为完全匹配,也称完备匹配。特别的当 $|V_1|=|V_2|$ 称为完美匹配

      交错路:如果图上的一条路径中的边交替表现为一条匹配边和非匹配边,则称这是一条交错路。更准确的来说,我们设 $M$ 表示图的匹配边集合(也称 $M$ 为图的一个匹配),$p$ 是 $G$ 上的一条通路,如果 $p$ 中的边为属于 $M$ 的边与不属于 $M$ 的边交替出现,则称 $p$ 为一条交错路。

      再来说一说匈牙利算法的思想,主要想法是给集合 $V_1$ 中的点尽可能多的在 $V_2$ 中寻找点进行配对

      下面是对匈牙利算法步骤一个模拟,更好的感觉算法过程:

      1.先给 $V_1$ 的1号节点找匹配边,从小到大遍历与1号点连接的 $V_2$ 内的点,发现 $V_2$ 内1号点可以与它配对,于是

      2.然后给 $V_1$ 的2号点找匹配,发现 $V_2$ 内2号点能够与之匹配,于是

      3.然后给 $V_1$ 的3号点找匹配,发现与它唯一相连的 $V_2$ 内的1号点已经被匹配了。可以观察到与 $V_2$ 的1号点匹配的是 $V_1$ 的1号点,于是我们可以尝试给 $V_1$ 的1号点换一个匹配点,于是第二个与 $V_1$ 1号点相连的是 $V_2$ 的2号点。但是 $V_2$ 的2号点也已经被匹配了(被 $V_1$ 的2号点匹配),所以我们继续又给 $V_1$ 的2号点找匹配,很明显可以将 $V_2$ 的4号点与之匹配,那么最后的结果是 $V_1$ 的2号点与 $V_2$ 的4号点匹配,$V_1$ 的1号点与 $V_2$ 的2号点匹配,最后 $V_1$ 的3号点与 $V_2$ 1号点匹配。

    上图总结来说,目的是给 $V_1$ 的3号点找匹配,找匹配的过程中需要给 $V_1$ 的1号点找新的匹配,给 $V_1$ 的1号点找新匹配的过程中,又需要给 $V_1$ 的2号点找新匹配,这是一个递归的过程,当新匹配找完之后又回溯到给 $V_1$ 的3号点找匹配。

    时间戳优化

      在给 $V_1$ 的1号点找新匹配时,很明显需要跳过旧匹配的连接点,但是给 $V_1$ 的1号点遍历链接的点时,是按照 $V_2$ 内的点从大到小或者从小到大的顺序遍历的,在遍历到新匹配点($V_2$ 的2号点)之前,我们已经遍历到了 $V_2$ 的1号点,而 $V_2$ 的1号点是一个旧匹配点,需要跳过

      这时就需要引入一个叫时间戳的概念,算法一开始时先遍历 $V_1$,每次遍历一个点时间戳就加一。我们一开始给 $V_1$ 1号点找匹配点,这个时候时间戳是1,给 $V_1$ 2号点找匹配点时时间戳为2,依次类推。

      每次给 $V_1$ 内的点找匹配点时,需要遍历 $V_2$ 内的点,我们创建一个 $vis$ 的数组,$vis[i]$ 表示最后一次访问 $V_2$ 内的 $i$ 号点是哪一个时间戳(初始化全为0)。 

      现在回过头来看为啥在给 $V_1$ 的3号点找匹配时(此时时间戳为3),需要给 $V_1$ 的1号点找新匹配,是因为遍历 $V_1$ 的3号点的连接点时,第一个找到了 $V_2$ 的1号点,于是我们将 $vis[i] = 3$,表示$V_2$ 的1号点最后一次访问时时间戳为3,而此点已经和 $V_1$ 的1号点匹配了,所以要找新匹配(注意不一定能找得到)。在给 $V_1$ 的1号点找新匹配时,我们又再次访问到了 $V_2$ 的1号点,但是此时 $vis[1] = 3$,和此时的时间戳的值相同,于是就跳过此点,重新找新的点,这样就完美跳过了旧匹配的点。

    for(int i = 1; i <= n; i++){
        tim++;    //tim表示此时时间戳
        if(dfs(i))    ans++;
    }
    int dfs(int u){
        for(int i = head[u]; ~i; i = edge[i].next){
            int v = edge[i].v;
            if(vis[v] == tim)    continue;    //时间戳和此时相同,跳过
            vis[v] = tim;    //更新时间戳
            if(pre[v] == 0 || dfs(pre[v])){    //dfs(i)表示给 v2 的 i 号点找匹配
                pre[v] = u;
                return 1;
            }
        }
        return 0;
    }

      4.最后 $V_1$ 的4号点和 $V_2$ 的3号点匹配,达成了最大匹配,结束匈牙利算法。

      模板代码:

    #include <iostream>
    #include <cstring>
    using namespace std;
    const int maxe = 5e4+5;    
    const int maxn = 1005;
    struct Edge{    //链式前向星所需的结构体
        int v;
        int next;
        Edge(int _v = 0, int _next = 0){
            v = _v;
            next = _next;
        }
    }; 
    Edge edge[maxe << 1];    //边数组
    int head[maxn], cnt;    //链式前向星所需变量
    int n, m, e, ans;    //n表示 v1 内点的数量,m表示 v2 内点的数量,e表示边的数量,ans表示最大匹配数
    int vis[maxn];    //vis[i] 表示 v2 的 i 号点最后一次访问的时间戳
    int pre[maxn];    //pre[i] 表示 v2 的 i 号点和 v1 内的哪个点匹配,默认为0表示无匹配
    int tim;    //此时时间戳
    void addedge(int u, int v){    //链式前向星加边
        edge[++cnt] = Edge(v, head[u]);
        head[u] = cnt;
        return;
    }
    int dfs(int u){    //dfs(i) 表示给 v1 的 i 号点找匹配点
        for(int i = head[u]; ~i; i = edge[i].next){    //链式前向星遍历连接边
            int v = edge[i].v;    //v表示遍历边时找到的 $v2$ 内的点
            if(vis[v] == tim)    continue;    //如果最后一次访问的时间戳和此时相同,就跳过
            vis[v] = tim;    //修改点v最后一次访问的时间戳
            if(pre[v] == 0 || dfs(pre[v])){    //如果点v无匹配点,或者点v的匹配点(v1内的点)找到了新的匹配点
                pre[v] = u;    //点v和点u匹配
                return 1;    //为点u找到了匹配点,返回1
            }
        }
        return 0;    //点u没有找到匹配点,返回0
    }
    int main(){
        memset(head, -1, sizeof head);
        cin >> n >> m >> e;
        for(int i = 1; i <= e; i++){
            int u, v;
            cin >> u >> v;
            addedge(u, v);
        }
        for(int i = 1; i <= n; i++){    //遍历v1内的点
                    tim++;    //时间戳加一
            if(dfs(i))    ans++;    //给 v1 的 i号点找匹配点
        }
        cout << ans;
        return 0;
    }    

  • 相关阅读:
    201871010107公海瑜《面向对象程序设计(java)》第十三周学习总结 公海瑜
    201871010107公海瑜《面向对象程序设计(java)》第67周学习总结 公海瑜
    《2019面向对象程序设计(Java)课程学习进度条》 公海瑜
    201871010107公海瑜《面向对象程序设计(java)》第二周学习总结 公海瑜
    201871010107公海瑜《面向对象程序设计(java)》第十二周学习总结 公海瑜
    201871010107公海瑜《面向对象程序设计(Java)》第四周学习总结 公海瑜
    201871010107公海瑜《面向对象程序设计(java)》第十一周学习总结 公海瑜
    201871010107公海瑜《面向对象程序设计(java)》第十周学习总结 公海瑜
    警告:[SetPropertiesRule]{Server/Service/Engine/Host/Context} Setting property 'source' to 'org.eclipse.jst.j2ee.server:strut2demo' did not find a matching property.
    IOException while loading persisted sessions: java.io.EOFException
  • 原文地址:https://www.cnblogs.com/qiyueliu/p/13308437.html
Copyright © 2020-2023  润新知