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

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

概念

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

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

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

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

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

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

(一)匈牙利算法

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

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

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

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

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

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

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

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

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

补充定义和定理

  • 最大匹配数:最大匹配的匹配边的数目
  • 最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
  • 最大独立数:选取最多的点,使任意所选两点均不相连
  • 最小路径覆盖数:对于一个 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\n", 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.\n");
				return;
			}                
		}
    }
    printf("BICOLORABLE.\n");
    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 匹配成功。

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

  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]

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

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

#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\n", KM());
    }
    return 0;
}
上一篇:训练指南 UVA - 11383(KM算法的应用 lx+ly >=w(x,y))


下一篇:训练指南 UVALive - 4043(二分图匹配 + KM算法)