最近花了大概十天内时间去学习了一下二分图匹配,不学不知道,一学才发现自己有很多知识漏洞,不过幸亏 在赛前补了一下,这个总结有习题集和一些重要(难!)的二分图算法以及学习资源,记录下来供以后自己回顾用,也给大家提供一些好的资料。
首先就是最简单的二分图最大匹配,这个算法有两种,一种是匈牙利算法(O(V*E)),另一种是HK算法(O(sqrt(V)*E)),其实它们的本质区别在于匈牙利算法是直接从空匹配开始dfs,找增广路,而HK算法时预先找到一些不相交的极大最短增广路集,然后再进增广,其实就是先BFS一下,再去dfs。关于这两个个算法的详细解释,可以借鉴一下这个资料:匈牙利算法和HK算法。
匈牙利算法模板:
//hdu2063——模板题
#include <iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=505;
int n,m,k;
int line[maxn][maxn];
int used[maxn],girl[maxn];
bool found(int x){
for(int i = 1;i<=n;++i){
if(line[x][i]&&!used[i]){
used[i]=1;
if(girl[i]==0||found(girl[i])){
girl[i]=x;
return 1;
}
}
}
return 0;
}
int main(int argc, char** argv) {
int x,y;
while(scanf("%d",&k)&&k){
scanf("%d %d",&m,&n);
memset(line,0,sizeof(line));
memset(girl,0,sizeof(girl));
for(int i = 0;i<k;i++){
scanf("%d %d",&x,&y);
line[x][y]=1;
}
int sum=0;
for(int i = 1;i<=m;++i){
memset(used,0,sizeof(used));
if(found(i)) sum++;
}
printf("%d\n",sum);
}
return 0;
}
HK算法模板:
//hdu2389——HK算法模板
#include <iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
const int maxn=3e3+7;
const int INF =0x3f3f3f3f;
int G[maxn][maxn],visited[maxn];
int cx[maxn],cy[maxn];//标记分别与x,y匹配的点。
int dx[maxn],dy[maxn];//存储BFS时x,y的层数
int n,m,dis;
struct people{
int x,y;
int v;
}p[maxn];
struct umbrella{
int x,y;
}u[maxn];
int bfs(){
queue<int> q;
dis=INF;
memset(dx,-1,sizeof(dx));
memset(dy,-1,sizeof(dy));// dx、dy用来标记当前点是否在一个交替路中
for(int i=1;i<=m;i++){
if(cx[i]==-1){//初始化x点集中没有匹配的点入队
q.push(i);
dx[i]=0;
}
}
while(!q.empty()){//BFS搜出目前最短的增广路集合
int u=q.front();
q.pop();
if(dx[u]>dis) break;//已经找到最短增广路,跳出
for(int v=1;v<=n;v++){
if(G[u][v]&&dy[v]==-1){//如果u与v有边而且v没有被搜到
dy[v]=dx[u]+1;//v在一条交替路上
if(cy[v]==-1) dis=dy[v];//如果当前点是未匹配点,那么该交替路为增广路
else{//否则把当前节点入队,层数加一
dx[cy[v]]=dy[v]+1;
q.push(cy[v]);
}
}
}
}
if(dis==INF) return 0;//如果不存在增广路,那么满足最大匹配。
else return 1;
}
int find(int x)//类似经典的最大匹配写法
{
for(int i = 1;i<=n;++i){
if(!visited[i]&&G[x][i]&&dy[i]==dx[x]+1){//i在一条交替路上
visited[i]=1;
if(cy[i]!=-1&&dy[i]==dis) continue;//如果该点已经匹配,而且不满足最短增广路,就跳出
if(cy[i]==-1||find(cy[i])){
cy[i]=x;
cx[x]=i;
return 1;
}
}
}
return 0;
}
int match()
{
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
int ans=0;
while(bfs()){//每次bfs求出一些不相交的最短增广路
memset(visited,0,sizeof(visited));
for(int i = 1;i<=m;++i)
if(cx[i]==-1&&find(i))//如果i点还没有匹配,进行匹配
ans++;
}
return ans;
}
double dist(int i,int j){
return sqrt((p[i].x-u[j].x)*(p[i].x-u[j].x)+(p[i].y-u[j].y)*(p[i].y-u[j].y));
}
int main(int argc, char** argv) {
int ncase;
scanf("%d",&ncase);
for(int t = 1;t<=ncase;++t){
memset(G,0,sizeof(G));
int time;
scanf("%d %d",&time,&m);
for(int i = 1;i<=m;++i)
scanf("%d %d %d",&p[i].x,&p[i].y,&p[i].v);
scanf("%d",&n);
for(int i = 1;i<=n;++i)
scanf("%d%d",&u[i].x,&u[i].y);
for(int i = 1;i<=m;++i){
int r=p[i].v*time;
for(int j = 1;j<=n;j++)
if(dist(i,j)<=r)//建二分图
G[i][j]=1;
}
printf("Scenario #%d:\n",t);
printf("%d\n\n",match());
}
return 0;
}
其实关于二分图的难点不在于求最大匹配,其关键还是在于怎样去建图,关于怎样建图,可以参考这篇博客:二分图的建图
以及这篇论文:二分图常用建图技巧(强推!!!!!)
关于二分图最大匹配还有一些定理需要知道,
定理一:最小顶点覆盖=最大匹配
定理二:最小边覆盖(原图是二分图)=最小路径覆盖(原图是DAG图)=结点总数N-最大匹配
定理三:最大独立集=结点总数-最大匹配。
关于这三个定理的解释以及定义可以参考这篇文章:关于二分图的一些定理。(强推!!!)
注意关于最小路径覆盖有一个小trick,当有向图的边有相交时,我们不能直接去求最小路径覆盖,而需要先求图的传递闭包,再求出最小路径覆盖,后面kuangbin带你飞专题有这个坑点,具体可以参考这篇文章:关于最小路径覆盖的一些技巧。
讲完最大匹配,该讲最大权匹配了,最大权匹配就是边上面有权值(其实最大匹配也有权值,只不过为1!!),带上权值我们怎么做呢?这里有一个KM算法,专门就是为此服务的,其算法思想的核心在于给X点集和Y点集的每个点赋一个期望数组,同时开一个slack数组来记录每个点通过减少最少的期望能加一条边。
具体算法的过程,可以参考这篇博文:KM算法详解
模板如下:
//hdu2255——模板题
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn=307;
int n;
int ex_girl[maxn],ex_boy[maxn],girl_visited[maxn],boy_visited[maxn];
int slack[maxn],love[maxn][maxn];
int match[maxn];
bool find(int girl){
girl_visited[girl]=1;
for(int boy=0;boy<n;++boy){
if(boy_visited[boy]) continue;
int d=ex_girl[girl]+ex_boy[boy]-love[girl][boy];
if(!d){
boy_visited[boy]=1;
if(match[boy]==-1||find(match[boy])){
match[boy]=girl;
return true;
}
}
else slack[boy]=min(slack[boy],d);
}
return false;
}
int KM(){
memset(match,-1,sizeof(match));
memset(ex_boy,0,sizeof(ex_boy));
for(int i = 0;i<n;++i){
// ex_girl[i]=*max_element(love[i],love[i]+n);
ex_girl[i]=love[i][0];
for(int j = 1;j<n;++j)
ex_girl[i]=max(ex_girl[i],love[i][j]);
}
for(int i = 0;i<n;++i){
fill(slack,slack+n,INF);
while(1){
memset(girl_visited,0,sizeof(girl_visited));
memset(boy_visited,0,sizeof(boy_visited));
if(find(i)) break;
int d=INF;
for(int i = 0;i<n;++i){
if(!boy_visited[i])
d=min(d,slack[i]);
}
for(int i = 0;i<n;++i){
if(girl_visited[i]) ex_girl[i]-=d;
if(boy_visited[i]) ex_boy[i]+=d;
else slack[i]-=d;
}
}
}
int ans=0;
for(int i = 0;i<n;++i)
ans+=love[match[i]][i];
return ans;
}
int main(int argc, char** argv) {
while(~scanf("%d",&n)){
for(int i = 0;i<n;++i)
for(int j = 0;j<n;++j)
scanf("%d",&love[i][j]);
printf("%d\n",KM());
}
return 0;
}
下面讲一下二分图的多重匹配,多重匹配分为两种,一种是多重最大匹配,比较好写,另一种是多重最优匹配,比较难理解一些,其实二分图多重匹配就是在添加一个超级汇点和源点,然后把以前的X点集和Y点集的维度增加了一维,多了一个限制,其实就是以前的二分图是一对一,现在变成了一对多(一个点跟多个匹配边相连),多对多。
具体的解释可以参考这篇文章:二分图多重匹配
话不多说上代码!
二分图多重最大匹配
//hdu3605——二分图多重最大匹配板子题
#include <iostream>
#include<cstring>
using namespace std;
const int maxn=1e5+7;
int n,m,visited[15],G[maxn][15];
int match[15][maxn];
// match[i][j]=k 表示第 i 个星球上住的第 j 个人是 k
int cnt[15],cap[15];//cap为 y节点的容量,cnt为当前 y节点使用的容量
int dfs(int x){
for(int i = 1;i<=m;++i){
if(G[x][i]&&!visited[i]){
visited[i]=1;
if(cnt[i]<cap[i]){//如果 y 节点还有容量可以匹配
cnt[i]++;
match[i][cnt[i]]=x;
return 1;
}
for(int j = 1;j<=m;++j){//如果 y 节点容量已经满了,试着为 y 节点的某个对象换对象
if(dfs(match[i][j])){
match[i][j]=x;//y 节点的 第 i 个对象让给 x
return 1;
}
}
}
}
return 0;
}
int judge(){
memset(cnt,0,sizeof(cnt));
for(int i = 1;i<=n;++i){//为 x 节点匹配对象
memset(visited,0,sizeof(visited));
if(!dfs(i)) return 0;
}
return 1;
}
int main(int argc, char** argv) {
while(~scanf("%d%d",&n,&m)){
for(int i = 1;i<=n;++i)
for(int j = 1;j<=m;++j)
scanf("%d",&G[i][j]);
for(int i = 1;i<=m;++i) scanf("%d",&cap[i]);
if(judge()) printf("YES\n");
else printf("NO\n");
}
return 0;
}
关于这个算法的解析可以参考这篇文章:网络流三·二分图多重匹配
二分图多重最优匹配:
//hihocoder 1393——二分图最优匹配
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN=250;
const int MAXE=20010;
const int INF=0x3f3f3f3f;
struct Node
{
int to,next,val;
Node(int to,int next,int val):to(to),next(next),val(val){};
Node(){}
};
Node edge[MAXE];
int head[MAXN],cnt;
void addEdge(int u,int v,int val)
{
edge[cnt]=Node(v,head[u],val);head[u]=cnt++;
edge[cnt]=Node(u,head[v],0);head[v]=cnt++;
}
int step[MAXN];
bool BFS(int st,int ed)
{
memset(step,-1,sizeof(step));
step[st]=0;
queue<int> que;
que.push(st);
while(!que.empty())
{
int u=que.front();que.pop();
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(edge[i].val>0&&step[v]==-1)
{
step[v]=step[u]+1;
que.push(v);
if(v==ed) return true;
}
}
}
return false;
}
int DFS(int st,int ed,int flow)
{
if(st==ed||flow==0) return flow;
int curr=0;
for(int i=head[st];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(step[v]==step[st]+1&&edge[i].val>0)
{
int d=DFS(v,ed,min(flow,edge[i].val));
if(d>0)
{
edge[i].val-=d;
edge[i^1].val+=d;
flow-=d;
curr+=d;
if(flow==0) break;
}
}
}
if(curr==0) step[st]=INF;
return curr;
}
int Dinic(int st,int ed)
{
int flow=0;
while(BFS(st,ed))
{
flow+=DFS(st,ed,INF);
}
return flow;
}
int main()
{
int t,n,m,st,ed,need,num,good,item,sum;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
sum=cnt=0;st=n+m+1;ed=st+1;
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)
{
scanf("%d",&need);
sum+=need;
addEdge(n+i,ed,need);
}
for(int i=1;i<=n;i++)
{
scanf("%d%d",&num,&good);
addEdge(st,i,num);
for(int j=1;j<=good;j++)
{
scanf("%d",&item);
addEdge(i,n+item,1);
}
}
int res=Dinic(st,ed);
if(res!=sum) printf("No\n");
else printf("Yes\n");
}
}
这里借鉴了hihocoder里的习题来解释。。。。
如果x部节点可以匹配多个y部节点,y部节点可以同时匹配多个x部节点,那么应该用网络流来解决。(因为匈牙利算法无法应对两边都可以选多个这种情况)
怎么建图呢?
很简单,假设x部节点的容量为capx[ i ],y部节点的容量为capy[ i ],同时给出x部节点可以与y部节点相连的的边,那么对于每个x部节点,超级源点都与x部节点连边,边权为capx[i];对于每个y部节点,都与超级汇点连接边,边权为capy[i]。然后连接每个x与y直接相连的边,边权为1。
这样一来,求出最大流就是最大匹配方案了:流量通道上的边的剩余流量代表匹配结果
下面介绍一个算法;婚姻匹配算法:(GS算法)
这个算法的实际应用很多,也挺有意思的,我在这也就不对算法进行解释了,关于具体思路可以参考这篇文章
Stable Matching Problem稳定匹配问题-----稳定婚姻算法
话不多说,上板子和题。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=200+5;
int B_g[N][N],G_b_score[N][N];
int bg[N],gb[N];
bool mark[N][N];
int n;
struct node
{
int num,v;
double x,y,z;
}node1[N],node2[N];
struct nodee
{
double dis;
int id,v;
}fz[N];
bool cmp(nodee a,nodee b)
{
if(a.dis==b.dis) return a.v>b.v;
return a.dis<b.dis;
}
double Dis(node a,node b)
{
double x=a.x-b.x;
double y=a.y-b.y;
double z=a.z-b.z;
return sqrt(x*x+y*y+z*z);
}
void stable_marry()
{
memset(mark,false,sizeof(mark));
memset(bg,-1,sizeof(bg));
memset(gb,-1,sizeof(gb));
queue<int>q;
while(!q.empty()) q.pop();
for(int i=1;i<=n;i++) q.push(i);
int head,nxt;
while(!q.empty())
{
head=q.front();
q.pop();
for(int i=1;i<=n;i++)
{
int nxt=B_g[head][i];
if(mark[head][nxt]) continue;
mark[head][nxt]=1;
if(gb[nxt]==-1)
{
gb[nxt]=head;
bg[head]=nxt;
break;
}
else if(G_b_score[nxt][gb[nxt]]<G_b_score[nxt][head])
{
q.push(gb[nxt]);
gb[nxt]=head;
bg[head]=nxt;
break;
}
}
}
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d%d%lf%lf%lf",&node1[i].num,&node1[i].v,&node1[i].x,&node1[i].y,&node1[i].z);
for(int i=1;i<=n;i++)
scanf("%d%d%lf%lf%lf",&node2[i].num,&node2[i].v,&node2[i].x,&node2[i].y,&node2[i].z);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
fz[j].dis=Dis(node1[i],node2[j]);
fz[j].id=j;
fz[j].v=node2[j].v;
}
sort(fz+1,fz+n+1,cmp);
for(int j=1;j<=n;j++)
B_g[i][j]=fz[j].id;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
fz[j].dis=Dis(node2[i],node1[j]);
fz[j].id=j;
fz[j].v=node1[j].v;
}
sort(fz+1,fz+n+1,cmp);
for(int j=1;j<=n;j++)
G_b_score[i][fz[j].id]=n-j+1;
}
stable_marry();
for(int i=1;i<=n;i++)
printf("%d %d\n",node1[i].num,node2[bg[i]].num);
printf("\n");
}
return 0;
}
接下来讲一个很牛逼的算法,它可以解决一般图和二分图的最大和最大权匹配,对,你没听错,就是可以解决你学的大部分问题。当然这么牛逼的算法,肯定不会很好理解,我也是花了好长时间才弄懂,但是自己还是不会敲,只会用板子(QAQ)。
下面推荐几个讲的不错的博客,当然我感觉讲的好,还是不好理解!!!
附上我看的网上的板子。
//URAL1099——模板题
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<vector>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<algorithm>
using namespace std;
const int maxn=300;
int N;
bool G[maxn][maxn];
int match[maxn];
bool InQueue[maxn],InPath[maxn],InBlossom[maxn];
int head,tail;
int Queue[maxn];
int start,finish;
int NewBase;
int father[maxn],Base[maxn];
int Count;
void CreateGraph(){
int u,v;
memset(G,0,sizeof(G));
scanf("%d",&N);
while(scanf("%d%d",&u,&v)!=EOF){
G[u][v]=G[v][u]=1;
}
}
void Push(int u){
Queue[tail++]=u;
InQueue[u]=1;
}
int Pop(){
int res=Queue[head++];
return res;
}
int FindCommonAncestor(int u,int v){
memset(InPath,0,sizeof(InPath));
while(true){
u=Base[u];
InPath[u]=1;
if(u==start)break;
u=father[match[u]];
}
while(true){
v=Base[v];
if(InPath[v])break;
v=father[match[v]];
}
return v;
}
void ResetTrace(int u){
int v;
while(Base[u]!=NewBase){
v=match[u];
InBlossom[Base[u]]=InBlossom[Base[v]]=1;
u=father[v];
if(Base[u]!=NewBase)father[u]=v;
}
}
void BlossomContract(int u,int v){
NewBase=FindCommonAncestor(u,v);
memset(InBlossom,0,sizeof(InBlossom));
ResetTrace(u);
ResetTrace(v);
if(Base[u]!=NewBase)father[u]=v;
if(Base[v]!=NewBase)father[v]=u;
for(int tu=1;tu<=N;tu++){
if(InBlossom[Base[tu]]){
Base[tu]=NewBase;
if(!InQueue[tu])Push(tu);
}
}
}
void FindAugmentingPath(){
memset(InQueue,0,sizeof(InQueue));
memset(father,0,sizeof(father));
for(int i=1;i<=N;i++){
Base[i]=i;
}
head=tail=1;
Push(start);
finish=0;
while(head<tail){
int u=Pop();
for(int v=1;v<=N;v++){
if(G[u][v]&&(Base[u]!=Base[v])&&match[u]!=v){
if((v==start)||(match[v]>0)&&father[match[v]]>0){
BlossomContract(u,v);
} else if(father[v]==0){
father[v]=u;
if(match[v]>0){
Push(match[v]);
} else {
finish=v;
return;
}
}
}
}
}
}
void AugmentPath(){
int u,v,w;
u=finish;
while(u>0){
v=father[u];
w=match[v];
match[v]=u;
match[u]=v;
u=w;
}
}
void Edmonds(){
memset(match,0,sizeof(match));
for(int u=1;u<=N;u++){
if(match[u]==0){
start=u;
FindAugmentingPath();
if(finish>0)AugmentPath();
}
}
}
void PrintMatch(){
Count=0;
for(int u=1;u<=N;u++){
if(match[u]>0)Count++;
}
printf("%d\n",Count);
for(int u=1;u<=N;u++){
if(u<match[u]){
printf("%d %d\n",u,match[u]);
}
}
}
int main(){
CreateGraph();
Edmonds();//进行匹配
PrintMatch();//输出匹配
return 0;
}
!!!终于把二分图匹配的知识点讲完了QAQ,还真是多的烦,下面开始给出kuangbin带你飞——二分图匹配专题的题解!!
hdu2255——KM算法 | 题解 |
hdu3488——二分图最大权匹配(拆点)或者跑一个最小费用最大流 | 题解 |
POJ2289——(一对多)多重最大匹配+二分 | 题解 |
POJ2112——(一对多)多重最大匹配+二分 | 题解 |
POJ3189——多重最大匹配+二分 | 题解 |
URAL1099——一般图匹配+带花树算法 | 题解 |
hdu4687——一般图匹配+带花树算法 | 题解 |