二分图
百科名片
在图论中,二分图是一类特殊的图,又称为双分图、二部图、偶图。二分图的顶点可以分成两个互斥的独立集 U 和 V 的图,使得所有边都是连结一个U 中的点和一个V 中的点。顶点集U、V 被称为是图的两个部分。等价的,二分图可以被定义成图中所有的环都有偶数个顶点。
设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
举例
众所周知,二分图最典型的例子就是一个班级里的男女生谈恋爱。
左边代表男生,右边代表女生,连线代表有好感,匹配就是把他们互相有好感的撮合起来。当然,是一夫一妻制度,不能一个人脚踏很多船。
所谓最大匹配,就是撮合最多对。
完美匹配就是所有的人都不落单。
定义
给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
选择这样的边数最大的子集称为图的最大匹配问题(Maximal Matching Problem)
如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配。
最大匹配在实际中有广泛的用处,求最大匹配的一种显而易见的算法是:先找出全部匹配,然后保留匹配数最多的。但是这个算法的复杂度为边数的指数级函数。因此,需要寻求一种更加高效的算法。
二分图的一个性质:最大匹配数+最大独立集=X+Y(结点总数)。
如果边上带权的话,找出权和最大的匹配叫做求最佳匹配。
二分图的判定——黑白染色
我们只要对一个图进行黑白染色,即一条边的两个点一定是黑色和白色,如果没有矛盾,就是二分图,否则就不是二分图,方法类似灌水。
例1 二分图判定
HihoCoder-1121
链接:https://hihocoder.com/problemset/problem/1121
https://vjudge.net/problem/HihoCoder-1121
思路
上面已讲解(灌水、黑白染色)
代码
用链式前向星
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const M=80005;
int const N=10005;
struct Edge {
int to,nt;
} e[M];
int n,m,cnt,check;
int h[N],vis[N];
inline void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
void dfs(int k,int c) {
vis[k]=c;
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(vis[v]==c) check=0;
else if(vis[v]==-1) dfs(v,1^c);
}
}
int main() {
int cas;
scanf("%d",&cas);
while(cas--) {
cnt=0;
memset(h,-1,sizeof(h));
scanf("%d%d",&n,&m);
for(int i=0; i<m; i++) {
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
memset(vis,-1,sizeof(vis));
check=1;
for(int i=1; i<=n; i++) if(vis[i]==-1) dfs(i,0);
puts(check ? "Correct" : "Wrong");
}
return 0;
}
用动态数组
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=10005;
vector<int> map[N];
int n,m,check;
int vis[N];
int dfs(int k,int c) {
if(vis[k]) return (vis[k]==c+1);
vis[k]=c+1;
for(int i=0; i<map[k].size(); i++)
if(!dfs(map[k][i],c^1)) return 0;
return 1;
}
int main() {
int cas;
scanf("%d",&cas);
while(cas--) {
scanf("%d%d",&n,&m);
check=1;
for(int i=1; i<=n; i++) map[i].clear();
memset(vis,0,sizeof(vis));
for(int i=1; i<=m; i++) {
int x,y;
scanf("%d%d",&x,&y);
map[x].push_back(y);
map[y].push_back(x);
}
for(int i=1; i<=n; i++) if((!vis[i]) && (!dfs(i,0))) {
check=0;
break;
}
puts(check ? "Correct" : "Wrong");
}
return 0;
}
二分图的最大匹配
使用匈牙利算法,即增广路算法。
下面给出简单的代码和注释
#include <iostream>
#include <cstring>
using namespace std;
#define M 100
#define N 100
int m,n;
//X类点集有n个结点,Y类点集有m个结点,map[i][j]=true表示结点i到结点j有一条边(结点计数由1开始)
bool map[N][M];
//表示结点i在dfs过程中已匹配
bool chk[M];
int match[M];
//match[m」存储了匹配的方案(matchLiJ表示与 i匹配的左边的点的编号),最初值为0,表示无匹配
//深度优先搜索,从元素p出发寻找增广路
int dfs(int p) {
int i;
for(i=1; i<=m; i++) {
//如果结点i还未匹配并且p到i是有边的,令p匹配i,然后递归继续搜索
if (map[p][i] && !chk[i]) {
chk[i] = 1;
if(!match[i] | | dfs (match[i])) {
match[i] = p;
return 1;
}
}
return 0;
}
int main() {
int i,res=0;
int x,y;
cin>>n>>m;
memset(match, 0,sizeof(match));
while(cin>>x>>y && x && y) map[x][y] = true;
//循环n次,每次确定一个元素被匹配
for(i = 1; i <= n; i++) {//此写法可以前一类点集顺序匹配的输出!
//初始化chk数组
memset(chk, false, sizeof(chk);
//深度优先搜索寻找增广路,若果找到,匹配数加1
if(dfs(i)) res++;
}
// res是最大匹配数.
printf("res = %d\n",res);|
return 0;
总结:匈牙利算法的递归写法是用交错轨每次寻找一个在Y类节点中没有匹配的点,类似灌水的方法,时间复杂度(VE)。
优点是简单易懂,容易实现。
例2 二分图最大匹配之匈牙利算法
HihoCoder-1122
链接:https://hihocoder.com/problemset/problem/1122
https://vjudge.net/problem/HihoCoder-1122
思路
使用匈牙利算法,上面已讲解
代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=10005;
int const M=50005;
struct Edge {
int to,nt;
} e[M<<1];
int cnt,n,m,t;
int h[N],vis[N],p[N];
inline void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
int find(int k) {
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(vis[v]) continue;
vis[v]=1;
if(!p[v] || find(p[v])) {
p[v]=k;
return 1;
}
}
return 0;
}
int main() {
scanf("%d%d",&n,&m);
ms(-1,h);
for(int i=0; i<m; i++) {
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
int ans=0;
for(int i=1; i<=n; i++) {
ms(0,vis);
if(find(i)) ans++;
}
printf("%d\n",ans>>1);
return 0;
}
例3 The Perfect Stall
POJ-1274
链接:http://poj.org/problem?id=1274
https://vjudge.net/problem/POJ-1274
题目大意
本题的英文题面应该可以看得懂,只要初中英语水平就可以看懂。这里不在给出。有疑问可以使用翻译工具和词典。
思路
这是一题较裸的题,不给思路。
提醒一下,本题有多组数据。
代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=205;
struct Edge {
int to,nt;
} e[N*N];
int cnt,n,m;
int h[N],vis[N],match[N];
inline void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
int dfs(int k) {
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(vis[v]) continue;
vis[v]=1;
if(!match[v] || dfs(match[v])) {
match[v]=k;
return 1;
}
}
return 0;
}
int main() {
while(scanf("%d%d",&n,&m)!=EOF) {
memset(h,-1,sizeof(h));
memset(match,0,sizeof(match));
cnt=0;
for(int i=1; i<=n; i++) {
int k;
scanf("%d",&k);
for(int j=1; j<=k; j++) {
int t;
scanf("%d",&t);
add(i,t);
}
}
int ans=0;
for(int i=1; i<=n; i++) {
memset(vis,0,sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n",ans);
}
return 0;
}
二分图的最大独立集
独立集和最大团是两个相同的概念,之前我们就讲过求解一般图的最大团的方法,是一个优化了的搜索+dp的算法。但是对于二分图这个特殊的图而言,其最大独立集=总的点数-最大匹配数,为我们提供了一个很不错的特殊算法。
例4 Girls and Boys
HDU-1068
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1068
https://vjudge.net/problem/HDU-1068
代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=1005;
int d[N*N],n[N*N],p[N],u[N];
int find(int pos) {
int now=pos;
while(n[pos]) {
pos=n[pos];
int data=d[pos];
if(!u[data]) {
u[data]=1;
if(p[data]==-1 || find(p[data])) {
p[data]=now;
p[now]=data;
return 1;
}
}
}
return 0;
}
int main() {
int v,pos;
while(scanf("%d",&v)!=EOF) {
int all,num;
pos=v;
for(int i=0; i<v; i++) {
scanf("%d: (%d)",&num,&all);
int now=i;
while(all--) {
scanf("%d",&d[pos]);
n[now]=pos;
now=pos++;
}
n[now]=0;
p[i]=-1;
}
int sum=0;
for(int i=0; i<v; i++) {
if(p[i]==-1) {
for(int t=0; t<v; t++) u[t]=0;
sum+=find(i);
}
}
printf("%d\n",v-sum>sum ? v-sum:sum);
}
return 0;
}
例5 Knights in Chessboard
LightOJ-1010
链接:http://lightoj.com/volume_showproblem.php?problem=1010
https://vjudge.net/problem/LightOJ-1010
思路
这题的正解是找规律,但用二分图也可以做。
二分图的最小点覆盖
即用最少的点去关联所有的边。
二分图的最小点覆盖=最大匹配数。
例6 Muddy Fields
POJ-2226
链接:http://poj.org/problem?id=2226
https://vjudge.net/problem/POJ-2226
代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=55;
int const M=1005;
int n,m,sx,sy,cnt;
int match[M],vis[M],h[M];
int bx[N][N],by[N][N];
char mat[N][N];
struct Edge {
int to,nt;
} e[M<<1];
void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
int dfs(int k) {
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(!match[v] || dfs(match[v])) {
match[v]=k;
return 1;
}
}
}
return 0;
}
int main() {
scanf("%d%d",&n,&m);
for(int i=0; i<n; i++) scanf("%s",mat[i]);
for(int i=0; i<n; i++) {
int last=-1;
for(int j=0; j<m; j++) {
if(mat[i][j]=='.') {
if(last==j-1) {
last=j;
continue;
}
sx++;
for(int k=last+1; k<j; k++) bx[i][k]=sx;
last=j;
}
}
if(mat[i][m-1]=='*') {
sx++;
for(int k=last+1; k<m; k++) bx[i][k]=sx;
}
}
for(int j=0; j<m; j++) {
int last=-1;
for(int i=0; i<n; i++) {
if(mat[i][j]=='.') {
if(last==i-1) {
last=i;
continue;
}
sy++;
for(int k=last+1; k<i; k++) by[k][j]=sy;
last=i;
}
}
if(mat[n-1][j]=='*') {
sy++;
for(int k=last+1; k<n; k++) by[k][j]=sy;
}
}
ms(-1,h);
for(int i=0; i<n; i++) for(int j=0; j<m; j++)
if(mat[i][j]=='*') add(bx[i][j],by[i][j]);
int ans=0;
for(int i=1; i<=sx; i++) {
ms(0,vis);
if(dfs(i)) ans++;
}
printf("%d\n",ans);
return 0;
}
例7 Machine Schedule
HDU-1150
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1150
https://vjudge.net/problem/HDU-1150
代码
#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=105;
int const M=2005;
int n,m,t,cnt;
int vis[N],h[N],match[N];
struct Edge {
int to,nt;
} e[M];
void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
int dfs(int k) {
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(match[v]==-1 || dfs(match[v])) {
match[v]=k;
return 1;
}
}
}
return 0;
}
int main() {
while(scanf("%d",&n) && n) {
scanf("%d%d",&m,&t);
ms(-1,h);
cnt=0;
ms(-1,match);
for(int i=0; i<t; i++) {
int a,b,c;
scanf("%d%d%d",&c,&a,&b);
if(a && b) add(a,b);
}
int ans=0;
for(int i=0; i<n; i++) {
ms(0,vis);
if(dfs(i)) ans++;
}
printf("%d\n",ans);
}
return 0;
}
二分图的最小边覆盖
即用最少数量的边覆盖所有点
最少边覆盖=总的点数-最大匹配数
例8 Antenna Placement
POJ-3020
链接:http://poj.org/problem?id=3020
https://vjudge.net/problem/POJ-3020
题目大意
一个矩形中,有N个城市’*’,现在这n个城市都要覆盖无线,若放置一个基站,那么它至多可以覆盖相邻的两个城市。
问至少放置多少个基站才能使得所有的城市都覆盖无线?
代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <queue>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=405;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
struct Edge {
int to,nt;
} e[N*10];
int n,m,cnt;
int match[N],h[N],vis[N];
char mat[45][45];
inline void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
int dfs(int k) {
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(match[v]==-1 || dfs(match[v])) {
match[v]=k;
return 1;
}
}
}
return 0;
}
int main() {
int cas;
scanf("%d",&cas);
while(cas--) {
scanf("%d%d",&n,&m);
for(int i=0; i<n; i++) scanf("%s",mat[i]);
cnt=0;
memset(h,-1,sizeof(h));
memset(match,-1,sizeof(match));
for(int i=0; i<n; i++) for(int j=0; j<=m; j++)
if(mat[i][j]=='*') for(int k=0; k<4; k++) {
int tx=i+dx[k];
int ty=j+dy[k];
if(tx<0 || ty<0 || tx>=n || ty>=m) continue;
if(mat[tx][ty]=='o') continue;
add(i*m+j,tx*m+ty);
}
int ans=0;
for(int i=0; i<n*m; i++) {
memset(vis,0,sizeof(vis));
if(dfs(i)) ans++;
}
int sum=0;
for(int i=0; i<n; i++) for(int j=0; j<m; j++)
if(mat[i][j]=='*') sum++;
printf("%d\n",sum-(ans>>1));
}
return 0;
}
二分图的最小路径覆盖
在一个DAG(有向无环图)中,路径覆盖就是在图中找一些路径,使之覆盖了图中的所有顶点,且任何一个顶点有且只有一条路径与之关联;(如果把这些路径中的每条路径从它的起始点走到它的终点,那么恰好可以经过图中的每个顶点一次且仅一次);如果不考虑图中存在回路,那么每条路径就是一个弱连通子集.
最小路径覆盖数=点数-最大匹配数
例9 Taxi Cab Scheme
POJ-2060 LA=3126
链接:
POJ: http://poj.org/problem?id=2060
LA: https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=1127
vjudge: https://vjudge.net/problem/POJ-2060
题目大意
有 n 个乘客需要乘出租车出行,给出他们出发时间,出发地点,目的地(input 保证数据按照出发时间升序给出),问最少需要安排多少辆车才能满足他们的需求。
代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#define R register int
#define re(i,a,b) for(R i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=505;
int const M=300005;
int n,m,t,cnt;
int vis[N],h[N],ho[N],x[N<<1],y[N<<1],match[N],mt[N];
struct Edge {
int to,nt;
} e[M];
void add(int a,int b) {
e[cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt++;
}
inline int calc(int x,int y) {
return ho[x]*60+mt[x]-(ho[y]*60+mt[y]);
}
inline int dist(int a,int b) {
return abs(x[a]-x[b])+abs(y[a]-y[b]);
}
int dfs(int k) {
for(int i=h[k]; i!=-1; i=e[i].nt) {
int v=e[i].to;
if(vis[v]) continue;
vis[v]=1;
if(!match[v] || dfs(match[v])) {
match[v]=k;
return 1;
}
}
return 0;
}
int main() {
int cas;
scanf("%d",&cas);
while(cas--) {
scanf("%d",&n);
cnt=0;
ms(-1,h);
ms(0,match);
for(int i=1; i<=n; i++) scanf("%d:%d %d %d %d %d",&ho[i],&mt[i],&x[2*i-1],&y[2*i-1],&x[2*i],&y[2*i]);
for(int i=1; i<n; i++) for(int j=i+1; j<=n; j++) {
int delta=dist(2*i-1,2*i)+dist(2*i,2*j-1);
int need=calc(j,i);
if(delta<need) add(i,j);
}
int ans=0;
for(int i=1; i<=n; i++) {
ms(0,vis);
if(dfs(i)) ans++;
}
printf("%d\n",n-ans);
}
return 0;
}
其他练习
-
POJ-3041 最小点覆盖
链接:http://poj.org/problem?id=3041
https://vjudge.net/problem/POJ-3041 -
POJ-1422 最小路径覆盖
链接:http://poj.org/problem?id=1422
https://vjudge.net/problem/POJ-1422 -
POJ-2771 最大独立集
链接:http://poj.org/problem?id=2771
https://vjudge.net/problem/POJ-2771 -
POJ- 2112 二分枚举+最大匹配
链接:http://poj.org/problem?id=2112
https://vjudge.net/problem/POJ-2112 -
HDU-3036 二分枚举+最大匹配+bfs
链接:http://acm.hdu.edu.cn/showproblem.php?pid=3036
https://vjudge.net/problem/HDU-3036 -
HDU-3118
链接:http://acm.hdu.edu.cn/showproblem.php?pid=3118
https://vjudge.net/problem/HDU-3118