二分图
- 点能分成两个独立的点集
- 匹配点:匹配边上的两点
- 最大匹配:选出最大的边数,使得这些边的顶点不重复
- 完美匹配:所有顶点都是匹配点。完美匹配一定时最大匹配,最大匹配不一定是完美匹配
- 最小覆盖:分为最小顶点覆盖与最小路径覆盖
- 最小顶点覆盖=最大匹配。选出最少的点集,覆盖所有的边
- 最小路径覆盖=n-最大匹配。(n为所有顶点)用最少的不相交路径覆盖所有点
- 最大独立集:n-最大匹配。(n为所有顶点)找出一个点集,使得点集中任意两点在图中无边。最大独立集与最小点覆盖互补。
- 最大团=补图的最大独立集。选出一些顶点,使得这些点两两有边
匈牙利算法
匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名。匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法。
时间复杂度邻接矩阵:最坏为O(n^3)邻接表:O(mn)
空间复杂度 邻接矩阵:O(n^2) 邻接表:O(m+n)
例子:
一: 先试着给1号男生找妹子,发现第一个和他相连的1号女生还名花无主,got it,连上一条蓝线
二:接着给2号男生找妹子,发现第一个和他相连的2号女生名花无主,got it
三:接下来是3号男生,很遗憾1号女生已经有主了,怎么办呢?
我们试着给之前1号女生匹配的男生(也就是1号男生)另外分配一个妹子。
(黄色表示这条边被临时拆掉)
重新找个妹子(注意这个步骤和上面是一样的,这是一个递归的过程)
此时发现2号男生还能找到3号女生,那么之前的问题迎刃而解了,回溯回去。
2号男生可以找3号妹子~~~ 1号男生可以找2号妹子了~~~ 3号男生可以找1号妹子
所以第三步最后的结果就是:
四: 接下来是4号男生,很遗憾,按照第三步的节奏我们没法给4号男生腾出来一个妹子,我们实在是无能为力了……香吉士同学走好。
这就是匈牙利算法的流程,其中找妹子是个递归的过程,最最关键的字就是“腾”字===============================================================================
邻接表:o(mn)
羊吃草
theme:n只羊在一条长度为400的线段上,每只羊只会在它喜欢的区间[a,b]吃草,只会位于整数点处,且每时刻每个点只会有一只羊,先q次询问,每次询问区间[l,r]中最多有多少只羊吃草。1<=n,q<=400
solution:二分匹配。将羊与它喜欢的区间相连,则进行q次匹配,在匈牙利算法dfs为给定节点u配对是判断一下v是否位于[l,r]范围即可。
//匹配指定右区间
#include<bits/stdc++.h>
using namespace std;
#define re(i,n) for(int i=0;i<n;i++)
const int N = 445;
const int M = 445;
int a[N],b[N];
int linker[M];//记录右边节点v所选的左边节点的编号u
int used[M];//记录右边节点有没有被用
vector<int>G[N];
int n,m,q;
int l,r;
void createG()
{
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
for(int j=a[i];j<=b[i];++j)
G[i].push_back(j);
}
}
bool dfs(int u)
{
for (int i=0;i<G[u].size();++i){ //扫描每个妹子
int v=G[u][i];
if (v>=l&&v<=r && used[v]==false)
{
used[v]=1;
if (linker[v]==0 || dfs(linker[v])) {名花无主或者能腾出个位置来,这里使用递归
linker[v]=u;
return true;
}
}
}
return false;
}
//匈牙利算法求二分图最大匹配
int hungary()
{
int ans=0;
fill(linker,linker+m+5,0);
for(int u=1; u<=n; ++u)//给每个左节点找右节点
{
fill(used,used+m+5,0);
if(dfs(u))//如果能成功给节点u找到一个匹配的右节点,且不影响之前选过的节点,则结果+1
ans++;
}
return ans;
}
int main()
{
cin>>n>>q;
m=401;
createG();
for(int i=1;i<=q;i++)
{
scanf("%d%d",&l,&r);
int ans=hungary();
printf("%d\n",ans);
}
}
邻接矩阵:o(n^3)
poj1274:The Perfect Stall
theme:n只牛m个坑,每只牛一个坑,每个坑只能占一只牛,给出每只牛能取哪几个坑,问最多能安排好多少只牛占一个坑。0 <= N,M<= 200
solution:裸的二分图匹配
//theme:n只牛m个坑,每只牛一个坑,每个坑只能占一只牛,给出每只牛能取哪几个坑,问最多能安排好多少只牛占一个坑。0 <= N,M<= 200
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define re(i,n) for(int i=0;i<n;i++)
const int N = 305;
const int M = 305;
int linker[M];//记录右边节点v所选的左边节点的编号u
int used[M];//记录右边节点有没有被用
int G[N][M];//图
int n,m;
void createG()
{
for(int u=1; u<=n; ++u)
{
int cnt;
scanf("%d",&cnt);
while(cnt--)
{
int v;
scanf("%d",&v);
G[u][v]=1;
}
}
}
bool dfs(int u)
{
for (int v=1;v<=m;v++){ //扫描每个妹子
if (G[u][v]==true && used[v]==false)
{
used[v]=1;
if (linker[v]==0 || dfs(linker[v])) {名花无主或者能腾出个位置来,这里使用递归
linker[v]=u;
return true;
}
}
}
return false;
}
//匈牙利算法求二分图最大匹配
int hungary()
{
int ans=0;
fill(linker,linker+m+5,0);
for(int u=1; u<=n; ++u)//给每个左节点找右节点
{
fill(used,used+m+5,0);
if(dfs(u))//如果能成功给节点u找到一个匹配的右节点,且不影响之前选过的节点,则结果+1
ans++;
}
return ans;
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
memset(G,0,sizeof(G));
createG();
int ans=hungary();
printf("%d\n",ans);
}
}
最大团
poj3692:Kindergarten
theme:n个男生,m个女生,其中女生之间、男生之间相互认识,有些女生与有些男生相互认识,问最能能从中选出多少人来是的任意两人相互认识?1 ≤ G, B ≤ 200
solution:由题意可知是求最大团,最大团为选最大的点集使得任意两点之间都有边,而最大团=补图的最大独立集,最大独立集=总的点数-最大匹配
//theme:n个男生,m个女生,其中女生之间、男生之间相互认识,有些女生与有些男生相互认识,问最能能从中选出多少人来是的任意两人相互认识?1 ≤ G, B ≤ 200
//最大团
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define re(i,n) for(int i=0;i<n;i++)
const int N = 305;
const int M = 305;
int linker[M];//记录右边节点v所选的左边节点的编号u
int used[M];//记录右边节点有没有被用
int G[N][M];//图
int n,m,cnt;
//建补图
void createG()
{
for(int i=1;i<=cnt;++i)
{
int u,v;
scanf("%d%d",&u,&v);
G[u][v]=0;
}
}
bool dfs(int u)
{
for (int v=1;v<=m;v++){ //扫描每个妹子
if (G[u][v]==true && used[v]==false)
{
used[v]=1;
if (linker[v]==0 || dfs(linker[v])) {名花无主或者能腾出个位置来,这里使用递归
linker[v]=u;
return true;
}
}
}
return false;
}
//匈牙利算法求二分图最大匹配
int hungary()
{
int ans=0;
fill(linker,linker+m+5,0);
for(int u=1; u<=n; ++u)//给每个左节点找右节点
{
fill(used,used+m+5,0);
if(dfs(u))//如果能成功给节点u找到一个匹配的右节点,且不影响之前选过的节点,则结果+1
ans++;
}
return ans;
}
int main()
{
int now=0;
while(~scanf("%d%d%d",&n,&m,&cnt)&&(n||m||cnt))
{
++now;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
G[i][j]=1;
createG();
int x=hungary();
int ans=n+m-x;
printf("Case %d: %d\n",now,ans);
}
}
二分多重匹配
hdu3605:Escape
theme:n个人,m间房,给出每个人能去哪几间房和房间最大容量,问这n个人能否都入住?1 <= n <= 100000, 1 <= m <= 10.
solution:二分多重匹配.
可以用网络流,加上一个源点与汇点。但节点数太大,可以考虑状态压缩,把n个人按能去的房间数组合,最多有2^10种
//theme:n个人,m间房,给出每个人能去哪几间房和房间最大容量,问这n个人能否都入住?1 <= n <= 100000, 1 <= m <= 10.
//邻接矩阵的匈牙利多重匹配(右节点有容量)
#include<iostream>
#include<cstdio>
using namespace std;
#define re(i,n) for(int i=0;i<n;i++)
const int N = 100005;
const int M = 11;
int linker[M][N];
int tem[M],num[M],used[M];//tmp表示当前右部顶点已匹配数目,num记录右边点最多能连几条边
int G[N][M];//图的邻接矩阵
int n,m;
bool dfs(int u)
{
for(int v=0; v<m; v++)
{
if(G[u][v] && !used[v])
{
used[v] =1;
if(tem[v] < num[v])
{
linker[v][tem[v]++] = u;
return 1;
}
for(int i=0; i<tem[v]; ++i)
if(dfs(linker[v][i]))
{
linker[v][i] = u;
return 1;
}
}
}
return 0;
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
for(int i=0; i<n; ++i)
for(int j=0; j<m; ++j)
scanf("%d",&G[i][j]);
for(int i=0; i<m; ++i)
scanf("%d",&num[i]);
for(int i=0; i<m; ++i)
tem[i] = 0;
int flag = 1;
for(int i=0; i<n; ++i)
{
for(int j=0; j<m; ++j)
used[j] = 0;
if(!dfs(i))
{
flag = 0;
break;
}
}
puts(flag?"YES":"NO");
}
}
匈牙利算法参考:https://blog.csdn.net/dark_scope/article/details/8880547