zoukankan      html  css  js  c++  java
  • 二分图[匈牙利算法 & KM算法]

    二分图[匈牙利算法 & KM算法]

    概念

    • 二分图:把一个图的顶点划分为两个不相交集 UV ,使得每一条边都分别连接UV中的顶点。如果存在这样的划分,则此图为一个二分图。

      img

    • 匹配:在图论中,一个「匹配」(matching)是一个集合,其中任意两条没有公共顶点。例如,图 3、图 4 中红色的边就是图 2的匹配。

    • 我们定义匹配点匹配边未匹配点非匹配边,例如图 31、4、5、7 为匹配点,其他顶点为未匹配点;(1,5)(4,7)为匹配边,其他边为非匹配边。

    • 最大匹配:一个图所有匹配中,所含匹配边数最多的匹配

    • 完美匹配:如果一个图的某个匹配中,所有顶点都是匹配点,那么它就是一个完美匹配。

    (一)匈牙利算法

    求解最大匹配问题的一个算法是匈牙利算法

    • 交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。如图5 中9→4→8→1→6→2 就是一条交替路。

    img

    • 增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。例如,图 5中的一条增广路如图 6 所示(图中的匹配点均用红色标出):

      img

      • 其实途径的那个未匹配点一定是终点

      • 增广路有一个重要特点:匹配边比匹配多一条

        我们把匹配边和非匹配边取反, 匹配边就多了一条

    我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)

    补充定义和定理

    • 最大匹配数:最大匹配的匹配边的数目
    • 最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
    • 最大独立数:选取最多的点,使任意所选两点均不相连
    • 最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 0(即单个点)。
    • 定理(不会证, 能用就行)
      • 定理一:最大匹配数 = 最小点覆盖数(这是 Konig 定理)
      • 定理2:最大匹配数 = 最大独立数
      • 定理3:最小路径覆盖数 = 顶点数 - 最大匹配数

    例题: Asteroids 穿越小行星群

    题目描述

    贝茜想驾驶她的飞船穿过危险的小行星群.小行星群是一个的网格(1≤N≤500),在网格内有K个小行星(1≤K≤10000)

    幸运地是贝茜有一个很强大的武器,一次可以消除所有在一行或一列中的小行星,这种武器很贵,所以她希望尽量地少用.给出所有的小行星的位置,算出贝茜最少需要多少次射击就能消除所有的小行星.

    输入格式

    1行:两个整数NK,用一个空格隔开.

    2行至K+1行:每一行有两个空格隔开的整数R, C(1≤R, C≤N),分别表示小行星所在的行和列.

    输出格式

    一个整数表示贝茜需要的最少射击次数,可以消除所有的小行星

    样例

    样例输入

    3 4
    1 1
    1 3
    2 2
    3 2
    

    样例输出

    2
    

    分析

    考虑把所有横坐标放在一个集合, 所有纵坐标放在另一个集合, 这两个集合构成二分图, 对应的坐标之间建立一条边

    我们现在要把所有横坐标, 所有纵坐标都覆盖(才能全部打下来), 也就是求 最小点覆盖数 , 根据我们上面的定理, 也就是求最大匹配数. 这里想明白了, 就是道模板题了

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    using namespace std;
    const int maxn = 10005;
    int n, k, head[maxn], tot, vis[maxn], group[maxn], ans;
    struct edge{
    	int to, nx;
    }e[maxn];
    void add(int x, int y){//建边不多说
    	e[++tot].to = y;
    	e[tot].nx = head[x];
    	head[x] = tot;
    }
    int xyl(int u){//匈牙利算法,重点哦
    	for(int i=head[u]; i; i=e[i].nx){//遍历一个横坐标x的邻接边
    		int v = e[i].to;//x对应的纵坐标y
    		if(vis[v] == 0){//未在增广路径中
    			vis[v] = 1;//进入增广路径
    			if(group[v]==0 || xyl(group[v])){//如果这个y没有匹配的x, 或他匹配的x能找到另一个未匹配的y
    				group[v] = u;//更改y匹配的x
    				return 1;//找到了增广路
    			}
    		}
    	}
    	return 0;//未找到增广路
    }
    int main(){
    	scanf("%d%d", &n, &k);
    	while(k--){
    		int r, c; scanf("%d%d", &r, &c);
    		add(r, c);
    	}
    	for(int i=1; i<=n; i++){
    		memset(vis, 0, sizeof(vis));
    		if(xyl(i)) ans ++;
    	}
    	printf("%d
    ", ans);
        return 0;
    }
    

    (二) 二分图判定

    如果一个图是连通的,可以用如下的染色法判定是否二分图:

    • 从某个未染色的结点u开始,做BFS或者DFS 。把u染为1,枚举u的儿子v。如果v未染色,就染为与u相反的颜色(-1),如果已染色,则判断uv的颜色是否相同,相同则不是二分图。
    • 如果一个图不连通,则在每个连通块中作判定。
    #include <bits/stdc++.h>
    const int maxn = 505;
    using namespace std
    vector<int> e[maxn];
    int m,n,color[maxn];
    bool flag;//全局,标记是否有环
    void dfs(int u){
        if(flag) return;//如果已经存在环就没必要接着递归了
        for(int i = 0; i < e[u].size(); i++){//遍历所有邻接点
        	int v = e[u][i];
            if(color[v]==0){//v还未访问,染色并递归
            	color[v] = -color[u];
            	dfs(v);
            }
            else if(color[v]==color[u]){
            	flag=1;//说明有环
            	return;
            }
        }
    } 
    int main(){
        memset(color, 0, sizeof(color));
        memset(e, 0, sizeof(e));
        for(int i = 0; i < m; i++){
            int u,v;scanf("%d%d",&u,&v);    
            e[u].push_back(v);e[v].push_back(u);
        }
        for(int i = 0; i < n; i++){
    		if(color[i] == 0){//可能图是不连通的
    			color[i] = 1;
    			dfs(i);
    			if(flag){
    				printf("NOT BICOLORABLE.
    ");
    				return;
    			}                
    		}
        }
        printf("BICOLORABLE.
    ");
        return 0;
    }
    

    (三)KM算法

    概念

    • 顶标:设顶点 Xi 的顶标为 A[i],顶点 Yj的顶标为 B[j] ,顶点 Xi与 Yj之间的边权为 w[i][j],初始化时,A[i] 的值为与该点关联的最大边权值,B[j] 的值为0
    • 相等子图:选择 A[i]+B[j]=w[i][j] 的边 <i,j>构成的子图,就是相等子图。
    • 算法执行过程中,对任一条边<i,j> ,A[i]+B[j]>=w[i][j] 恒成立。
    • slack数组存的数是Y部的点相等子图时,最小要增加的值

    算法图示

    1. 从X1X1 开始跑匈牙利,匹配的条件是:A[i]+B[j]=w[i][j],显然 X1 和 Y3 匹配成功。

      img

    2. 接着从 X2 开始,A[X2]+B[Y3]=w[X2][X3] 此时 Y3 已被 X1匹配,让 X1 换一个匹配对象,但在 X1 的邻接点没有满足:A[i]+B[j]=w[i][j]的点,这些相临边和顶标和的最小差值为:minz=1,把此时已标记的 X 部的顶标减去minz, Y 部的此时标记的顶标加上minz, 此时A[X1]+B[Y1]==w[X1][Y1]

      img

    3. 最后从X3 开始找增广路,X3 匹配 Y3 ,不满足,调整顶标,即A[3]=5−1=4,匹配Y3 成功,尝试 X2 寻找新的匹配,此时 Y1 满足匹配,尝试让 X1 寻找新的匹配,此时X1已找不到新的为匹配的点,匹配失败,回溯到 X2
      img

    #include <bits/stdc++.h>
    using namespace std;
    const int maxn = 300 + 10,maxe=1e4+5,Inf = 0x3f3f3f3f;
    struct Edee{int to,w,next;}e[maxe];
    int n,m,len,head[maxn],g[maxn][maxn];
    int wx[maxn], wy[maxn];//每个点的顶标值(需要根据二分图处理出来)
    int match[maxn];//每个Y部点所匹配的X部的点
    int visx[maxn], visy[maxn];//每个点是否加入增广路
    int slack[maxn];//边权和顶标最小的差值
    void Insert(int u,int v){ e[++len].to=v;e[len].next=head[u];head[u]=len; }
    bool dfs(int u){//进入DFS的都是X部的点,找到增广路返回1,否则返回0
        visx[u] = 1;//标记进入增广路    
        for(int i = head[u]; i ; i=e[i].next){
        	int v = e[i].to;
            if(visy[v] == 0){//如果Y部的点还没进入增广路      
                int t = wx[u] + wy[v] - g[u][v];//边权与顶标的差值
                if(t == 0){//t为0说明是相等子图            
                    visy[v] = 1;//加入增广路                
                    if(match[v] == -1 || dfs(match[v])){                    
                        match[v] = u;//进行匹配
                        return 1;
                    }
                }
                else if(t > 0) slack[v] = min(slack[v], t);//slack[v]存的是Y部的点需要变成相等子图顶标值最小增加多少
            }
        }
        return 0;
    }
    int KM(){
        memset(match, -1, sizeof(match));
        memset(wx, 0, sizeof(wx));//wx的顶标为该点连接的边的最大权值
        memset(wy, 0, sizeof(wy));//wy的顶标为0
        for(int u = 1; u <= n; u++) for(int i = head[u]; i ; i=e[i].next) wx[u] = max(wx[u], g[u][e[i].to]);//预处理出顶标值  
        for(int i = 1; i <= n; i++){//枚举X部的点    
            memset(slack, 0x3f, sizeof(slack));
            while(1){
                memset(visx, 0, sizeof(visx));
                memset(visy, 0, sizeof(visy));
                if(dfs(i)) break;//已经匹配正确
                int minz = Inf;
                for(int j = 1; j <= n; j++) if(!visy[j] && minz > slack[j]) minz = slack[j];//找出还没经过的点中,需要变成相等子图的最小额外增加的顶标值          
                for(int j = 1; j <= n; j++) if(visx[j]) wx[j] -= minz; //将X部已访问的顶标减去minz,
                for(int j = 1; j <= n; j++)
                    if(visy[j])wy[j] += minz;//Y部已访问的顶标加上minz
                    else slack[j] -= minz;//未在增广路,但相应的X部已访问的顶标减少了,其相邻的未访问的期望也减小
            }
        }
        int ans = 0;//二分图最优匹配权值
        for(int i = 1; i <= n; i++) if(match[i] != -1) ans += g[match[i]][i];
        return ans;
    }
    int main(){
        while(scanf("%d%d", &n,&m) != EOF){
            for(int i = 1; i <= m; i++){
                int u,v,w;scanf("%d%d%d", &u,&v,&w);
                g[u][v]=w;Insert(u,v);
            }
            printf("%d
    ", KM());
        }
        return 0;
    }
    
  • 相关阅读:
    Jenkins发布.Net Core项目到IIS
    2019 SDN上机第2次作业
    2019 SDN上机第1次作业
    第二次结对编程作业
    第3组 团队展示
    第一次结对编程作业
    第一次个人编程作业
    第一次博客作业
    android json解析及简单例子
    详述Google针对Android平板App发布的十大开发准则
  • 原文地址:https://www.cnblogs.com/hzoi-poozhai/p/12839168.html
Copyright © 2011-2022 走看看