二分图匹配

本文介绍了二分图匹配的概念,包括最小点覆盖数、最大独立集、最小路径覆盖数及其相互关系的定理,并通过具体题目阐述了这些理论在实际问题中的应用。还提到了匈牙利算法和KM算法在解决二分图匹配问题中的作用,以及相关POJ和HDU的编程挑战题。

定义:

  • 二分图:一个图被分成了两部分,相同的部分没有边

  • 匹配:二分图G的子图M中,M的边集{E}中的任意两条边都不指向同一个顶点

  • 极大匹配:在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数

  • 最大匹配:是所有极大匹配当中边数最大的一个匹配

  • 完全匹配(完备匹配):一个匹配中,图中的每个顶点都和图中某条边相关联

有什么用:

  • 定理1:最小点覆盖数 = 最大匹配数

    点覆盖:点集合使得任意一条边至少有一个端点在集合中

  • 定理2:最大独立集 = 顶点数 - 最大匹配数

    独立集:点集合中任何两个顶点都不互相连接

  • 定理3:最小路径覆盖数 = 顶点数 – 最大匹配数

    路径覆盖:任何一个点都属于且仅属于一条路径

代码实现:(匈牙利算法)
匈牙利算法与交错路

#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
vector<int> g[1005];
int n,m,e;
int link[1005],cnt;
int vis[1005];
int find(int x){
    for(int i=0;i<g[x].size();i++){
        int u=g[x][i];
        if(!vis[u]){
            vis[u]=1;
            if(link[u]==0||find(link[u])){
                link[u]=x;
                return 1;
            }
        }
    }
    return 0;
}
int main(){
    cin>>n>>m>>e;
    int x,y;
    for(int i=1;i<=e;i++){
        cin>>x>>y;
        if (x>=1&&y>=1&&x<=n&&y<=m)
        	g[x].push_back(y);
    }
    for(int i=1;i<=n;i++){
        memset(vis,0,sizeof(vis));
        if(find(i)) cnt++;
    }
    cout<<cnt;
    return 0;
}

时间复杂度:

O(nm) 其中n是点数,m是边数

但事实上,对于绝大部分的二分图,匈牙利算法都跑不够上限

KM算法

题目:

最小点覆盖数:

定理证明:http://www.cnblogs.com/jasonlixuetao/p/4756026.html

POJ 3041

题意:有一个N*N的网格,该网格有K个障碍物.你有一把武器,每次你使用武器可以清楚该网格特定行或列的所有障碍.问你最少需要使用多少次武器能清除网格的所有障碍物?

/*把网格的行1到N看成左边点集的点,网格的列号看成右边点集的点
如果(i,j)格有障碍,那么就在左边i点到右边j点之间连接一条边
接下来求最小点覆盖数即可*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=500+10;
int n;
vector<int> g[maxn];
int vis[maxn];
int link[maxn];
int find(int u) {
	for(int i=0;i<g[u].size();i++){
		int v=g[u][i];
		if(!vis[v]) {
			vis[v]=1;
			if(!link[v]||find(link[v])){
				link[v]=u;
				return 1;
			}
		}
	}
	return 0;
}
int main(){
	int n,k;
	scanf("%d%d",&n,&k);
	while(k--) {
		int u,v;
		scanf("%d%d",&u,&v);
		g[u].push_back(v); 
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		memset(vis,0,sizeof(vis));
		if(find(i)) ans++;
	}
	printf("%d\n",ans);
	return 0;
}
最大独立集:

定理证明:最大独立集=顶点数-最小顶点覆盖数=顶点数-最大匹配数

HDU 3829

题意:动物园有若干只狗和猫,每个小朋友都要么喜欢一只狗不喜欢一只猫,要么反过来。现在要移除一些狗和猫,小朋友只有在不移除自己喜欢的动物,移除了自己不喜欢的动物才会开心。问最多能让几个小朋友开心。

/*把人当作节点,矛盾的人连边,做最大独立集*/
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=5e2+7;
struct Edge{
    int v,nxt;
    Edge(int v=0,int nxt=0):v(v),nxt(nxt){}
}e[N*N];
int n,tmp;
int head[N],cnt;
void add(int u,int v){
    e[++cnt]=Edge(v,head[u]);head[u]=cnt;
    e[++cnt]=Edge(u,head[v]);head[v]=cnt;
}
int vis[N],link[N];
bool find(int u){
    for(int i=head[u];~i;i=e[i].nxt){
        int v=e[i].v;
        if(vis[v]) continue;
        vis[v]=1;
        if(link[v]==-1||find(link[v])){
            link[v]=u;
            return true;
        }
    }
    return false;
}
struct Node{
    char x[5],y[5];
}a[N];
char s1[5],s2[5];
int main(){
    int n,m,k;
    while(scanf("%d%d%d",&n,&m,&k)!=EOF){
        memset(head,-1,sizeof(head));cnt=-1;
        memset(link,-1,sizeof(link));
        for(int i=1;i<=k;i++)
            scanf("%s%s",a[i].x,a[i].y);
        for(int i=1;i<=k;i++)
            for(int j=i+1;j<=k;j++)
                if(strcmp(a[j].x,a[i].y)==0||strcmp(a[j].y,a[i].x)==0)
                    add(i,j);
        int ans=0;
        for(int i=1;i<=k;i++){
            memset(vis,0,sizeof(vis));
            if(find(i)) ans++;
        }
        printf("%d\n",k-ans/2);
    }
}
拓展:
二分图的最大团

定义:对于一般图来说,团是一个顶点集合,这些顶点两两之间都有边。最大团就是使得选出的这个顶点集合最大

补图:一个图G的补图是一个图有着跟G相同的点,而且这些点之间有边相连当且仅当在G里面他们没有边相连。

方法:二分图的最大团=补图的最大独立集
最小路径覆盖数:

DAG 的最小不相交路径覆盖

定理证明:

首先,若最大匹配数为0,则二分图中无边,也就是说有向图G中不存在边,那么

显然:最小路径覆盖=|G|-最大匹配数=|G|-0=|G|。

若此时增加一条匹配边x1--y2,则在有向图|G|中,x、y在同一条路径上,最小路径覆盖数减少一个。

继续增加匹配边,每增加一条,最小路径覆盖数减少一个,则公式:最小路径覆盖=|G|-最大匹配数得证。

最小不相交路径覆盖数:

POJ 1422

题意:T组数据,有 N 个村庄 M 条有向路,构成的图不会出现环,现在计划空降多组士兵,每组士兵一个人,士兵可以不回头的走完全程,别的士兵走过的地方,不能再走了。问最少将降落几组士兵可以遍历全部的节点。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int N=200+10;
vector<int> g[N];
int t,n,m;
int link[N];
int vis[N];
int find(int u){
    for(int i=0; i<g[u].size(); ++i){
        int v=g[u][i];
        if(vis[v]) continue;
        vis[v]=1;
        if(!link[v]||find(link[v])){
            link[v]=u;
            return 1;
        }
    }
    return 0;
}
int main(){
    int u,v;
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&n,&m);
        memset(link,0,sizeof(link));
        for(int i=1;i<=n;++i)
            g[i].clear();
        for(int i=1;i<=m;++i){
            scanf("%d%d",&u,&v);
            g[u].push_back(v);
        }
        int ans=0;
        for(int i=1;i<=n;++i){
        	memset(vis,0,sizeof(vis));
        	ans+=find(i);
    	}
        printf("%d\n",n-ans);
    }
    return 0;
}

最小相交路径覆盖数:

POJ 2594

题意:在一张有向无环图中,放置若干个机器人,每个机器人都有一个属于自己的路径。图中每一个节点都可以属于多个路径。问最少需要投放几个机器人才能遍历完图中每个节点。

/*如果两个点a和b是连通的,只不过中间需要经过其它的点,那么可以在这两个点之间加边
先跑一遍floyd确定哪些点是连通的,然后就转化成了最小不相交路径数问题*/
#include<iostream>
#include<cstring> 
#include<cstdio>
using namespace std;
typedef long long ll;
const int N=5e2+7;
int mp[N][N];
int vis[N],link[N];
int n,m,u,v;
bool find(int u){
    for(int v=1;v<=n;v++){
        if(mp[u][v]&&!vis[v]){
            vis[v]=1;
            if(link[v]==-1||find(link[v])){
                link[v]=u;
                return true;
            }
        }
    }
    return false;
}
int main(){
    while(scanf("%d%d",&n,&m)!=EOF){
        if(n==0&&m==0) break;
        memset(link,-1,sizeof(link));
        memset(mp,0,sizeof(mp));
        for(int i=1;i<=m;i++){
            scanf("%d%d",&u,&v);
            mp[u][v]=1;
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                if(!mp[i][j]) continue;
                for(int k=1;k<=n;k++){
                    if(!mp[j][k]) continue;
                    mp[i][k]=1; 
                }
            }
        }
        int ans=0;
        for(int i=1;i<=n;i++){
            memset(vis,0,sizeof(vis));
            if(find(i)) ans++;
        }
        printf("%d\n",n-ans);
    }
}
杂题:

HDU 1045

题意:给出一张图,‘X’代表墙,‘.’ 代表空地。在空地上放一些炮塔,炮塔不能处在同一行同一列,除非被墙隔开。问最多能放多少个炮塔。

/*如果不存在墙,把每一列缩成一点,每一行缩成一点进行匹配即可
如果存在墙,墙的两侧其实就相当于是两个互不相关的点
首先按列遍历,对每一列的联通串缩点并染色
然后按行遍历,同样的对联通串缩点,并与所染色的对应列联通串连边*/ 
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e3+7;
struct Edge{
	int u,v,nxt;
}edge[N<<1];
int n,cnt,head[N],link[N],vis[N];
int col[N][N];
char mp[N][N];
void add(int u,int v){
	edge[++cnt].u=u;edge[cnt].v=v;edge[cnt].nxt=head[u];head[u]=cnt;
	edge[++cnt].u=v;edge[cnt].v=u;edge[cnt].nxt=head[v];head[v]=cnt;
}
int find(int u){
    for(int i=head[u];i;i=edge[i].nxt){
        int v=edge[i].v;
        if(!vis[v]){
            vis[v]=1;
            if(link[v]==0||find(link[v])){
                link[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int main(){
	while(scanf("%d",&n)!=EOF){
		if(n==0) break;
		memset(head,0,sizeof(head));
		memset(link,0,sizeof(link));
		cnt=0;
		for(int i=1;i<=n;i++)
			scanf("%s",mp[i]+1);
		int tot=1;
		for(int j=1;j<=n;j++){
			for(int i=1;i<=n;i++){
				if(mp[i][j]=='X') tot++;
				else col[i][j]=tot;
			}
			tot++;
		}
		tot=1;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(mp[i][j]=='X') tot++;
				else add(tot,col[i][j]+100);
			}
			tot++;
		}
		int ans=0;
        for(int i=1;i<=tot;i++){
            memset(vis,0,sizeof(vis));
            if(find(i)) ans++;
        }
        printf("%d\n",ans);
	}
	return 0;
} 

HDU 4185

题意:一张n乘n的二维图,由‘#’和‘.’构成。要求用尽可能多的1乘2的矩形去覆盖‘#’,矩形内不能含有‘.’,问最多能覆盖多少个矩形。

注意:这题的n没有题面说的那么大。

/*每一个 #就是一个点,相邻的 #互相连接,然后进行二分匹配即可*/
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e3+7;
const int go[4][2]={-1,0,1,0,0,1,0,-1};
struct Edge{
	int u,v,nxt;
}edge[N<<1];
int t,cs,n,cnt,head[N],link[N],vis[N];
int mp[N][N];
char ch[N][N];
void add(int u,int v){
	edge[++cnt].u=u;
	edge[cnt].v=v;
	edge[cnt].nxt=head[u];
	head[u]=cnt;
}
int find(int u){
    for(int i=head[u];i;i=edge[i].nxt){
        int v=edge[i].v;
        if(!vis[v]){
            vis[v]=1;
            if(link[v]==0||find(link[v])){
                link[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int main(){
	scanf("%d",&t);
	while(t--){
		cnt=0;
		memset(head,0,sizeof(head));
		memset(link,0,sizeof(link));
		memset(mp,0,sizeof(mp));
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
			scanf("%s",ch[i]+1);
		int tot=0;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(ch[i][j]=='#')
					mp[i][j]=++tot;
			}
		}
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(mp[i][j]){
					for(int k=0;k<4;k++){
						int nx=i+go[k][0];
						int ny=j+go[k][1];
						if(mp[nx][ny])
							add(mp[i][j],mp[nx][ny]);
					}
				}
			}
		}
		int ans=0;
        for(int i=1;i<=tot;i++){
            memset(vis,0,sizeof(vis));
            if(find(i)) ans++;
        }
        printf("Case %d: %d\n",++cs,ans/2);
	}
	return 0;
} 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值