二分图
1. 二分图的定义
二分图,又称二部图,英文名叫 Bipartite graph。
二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,将节点划分成满足以上性质的两个集合。
如图
取自于oi-wiki
2. 二分图的判定
观察二分图的定义可以发现
- 一个图中每一条边都连接着不同颜色的两个点
- 一个图是二分图当且仅当图中不存在奇环
因为在一个环中只有偶数时绕一圈才会回到相同的颜色.
2.1 染色法
我们利用染色法进行二分图的判定
- 首先将所有节点初始化为未染色
- 从未染色点
开始染色并向结点 遍历(这里用 和 表示两个颜色) - 如果结点
未染色则重复操作2,否则判定 和 的颜色是否相同- 如果两结点颜色相同则改图不是二分图
时间复杂度
bool dfs(int x,int col){
c[x] = col;
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(!c[y] && !dfs(y,-col))return 0;
else if(c[y] == col)return 0;
}
return 1;
}
2.2 例题
P1525 [NOIP2010 提高组] 关押罪犯
题意:给定一个图,求把这个图分为两个点集,找出每个点集内最大边权最小的分法,求最小的最大边权
首先可以想到求 '最小的最大' 可以二分;
转化为是否存在一个分法,使得两个点集内最大边权是否小于等于
我们可以把大于
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4+10,M = 2e5+10;
struct made{
int ver,nx;
}e[M];
int hd[N],tot,c[N];
void add(int x,int y){
tot++;
e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
void first(){
tot = 0;
memset(hd,0,sizeof(hd));
memset(e,0,sizeof(e));
memset(c,0,sizeof(c));
}
int n,m;
bool f;
struct node{
int x,y,z;
}a[M];
bool dfs(int x,int col){
c[x] = col;
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(!c[y] && !dfs(y,-col))return 0;
else if(c[y] == col)return 0;
}
return 1;
}
bool check(int mid){
first();
for(int i = 1;i <= m;i++)
if(a[i].z > mid){
add(a[i].x,a[i].y);
add(a[i].y,a[i].x);
}
f = 1;
for(int i = 1;i <= n;i++)
if(!c[i])f &= dfs(i,1);//该图不一定是连通图
return f;
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
int l = 0,r = 1e9;
while(l < r){
int mid = l + r >> 1;
if(check(mid))r = mid;
else l = mid + 1;
}
printf("%d\n",l);
return 0;
}
3. 二分图的匹配
3.1 二分图最大匹配
- 图的匹配: 无向图中一个边集中,任意两条边都没有公共端点,则称这组边的集合为该图的一组匹配,对于一组匹配
。- 匹配变/非匹配边:属于
的边被称为匹配边,不属于 的边被称为非匹配边 - 匹配点/非匹配点:匹配边的端点为匹配点,其他节点为非匹配点
- 增广路:如果在二分图中存在一条连接两个非匹配点的路径
,使得非匹配边与匹配边在 交替出现,则 是匹配 的增广路,也称交错路
- 匹配变/非匹配边:属于
- 二分图的最大匹配:包含边数最多的一组匹配被称为二分图的最大匹配
性质:二分图的一组匹配 是最大匹配,当且仅当图中不存在 的增广路
因为如果存在增广路,那么我们把增广路上的所有边的状态全部取反,那么得到的新的边集
仍然是一组匹配,且边数增加了1
取自于《算法竞赛进阶指南》
匈牙利算法(增广路算法)
又称增广路算法,用于计算二分图的最大匹配,过程为
- 首先将所有边定为非匹配边
- 寻找增广路
,把路径上所有边状态取反,得到一个更大的 - 重复第2步,直至图中不存在增广路
匈牙利算法依次尝试每一个左部结点
- y本身就是非匹配边,无向边
自己构成一条长度为 的增广路 - y已经与左部点
匹配,但从 出发可以找到另一个右部点 与之匹配。此时 为一条增广路
每一个左部结点最多遍历二分图一次,时间复杂度为
bool dfs(int x){
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(v[y])continue;
v[y] = 1;
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
return 0;
}
//main函数
for(int i = 1;i <= n;i++){
memset(v,0,sizeof(v));
if(dfs(i))ans++;
}
3.2 二分图的多重匹配
给定一个包含
多重匹配一般有这三种解决方案:
- 拆点。把第
个左部结点拆为 个不同的左部结点,右部结点同理,然后跑二分图的最大匹配即可。 - 当右部点
均成立,则我们可以每个左部节点跑 次dfs即可。 - 网络流。
3.3 二分图带权匹配
完备匹配:一个二分图,其左右部点数相同均为
二分图带权匹配:给定一个二分图,二分图中的每条边都有一个权值,求出该二分匹配的一组最大匹配,使得匹配边的权值之和最大(前提是先保证匹配数最大,其次保证边权和最大)
3.3.1 KM算法
一些定义:
- 交错树:在匈牙利算法中如果某个左部结点匹配失败,则称该节点
经过的路径构成一颗树,因为该树中非匹配边与匹配边交错更替,所以称作交错树。 - 顶标:在二分图中给左部节点和右部节点都给出一个整数值,满足任意一组边
, 都成立, 和 称为节点的顶标。 - 相等子图:二分图中所有节点和满足
的边构成的子图,称为二分图的相等子图。
定理:若在相等子图中存在完备匹配,则这个完备匹配就是二分图的带权最大匹配。
3.4 例题
I 372. 棋盘覆盖
题意:在一个
二分图匹配模型:
- 结点可以分为独立的两个集合,每个集合内部有
条边 - 每个结点只能与一条匹配边相连
在本题中,任意两块骨牌都不重叠,即每个结点最多只被一个骨牌覆盖,而骨牌大小为
为使骨牌不重叠的情况下尽量多放,即求该二分图的最大匹配。
时间复杂度 。
小优化:我们可以发现从右部点到左部点的连边是无用的,所以只需建左部点向右部点的边就可以了(减少内存)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+10,M = 2e5+10;
//构造二分图匹配
int n,m,E;
struct made{
int ver,nx;
}e[M<<1];
int hd[N],tot,cnt,ans;
int match[N];
bool v[N],f[N];
int id(int x,int y){
return (x-1)*n+y;
}
void add(int x,int y){
tot++;
e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
bool dfs(int x){
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(v[y])continue;
v[y] = 1;
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
return 0;
}//
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int x,y;
scanf("%d%d",&x,&y);
f[id(x,y)] = 1;
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
if(f[id(i,j)])continue;//被禁不能连边
if(i > 1 && !f[id(i-1,j)])add(id(i,j),id(i-1,j));
if(i < n && !f[id(i+1,j)])add(id(i,j),id(i+1,j));
if(j > 1 && !f[id(i,j-1)])add(id(i,j),id(i,j-1));
if(j < n && !f[id(i,j+1)])add(id(i,j),id(i,j+1));
}
}
for(int i = 1;i <= n;i++)
for(int j = (i&1)?1:2;j <= n;j += 2){
memset(v,0,sizeof(v));
if(dfs(id(i,j)))ans++;
}//找左部结点即白色结点
printf("%d\n",ans);
return 0;
}
II 373. 車的放置
题意:在一个
懂了上一题这题直接秒
同上一题,我们可以发现在任意一行或一列中只能存在一个棋子,所以我们可以把行数定义为二分图中的左部点,把列数定义为右部点,且满足左部点与右部点内部不存在边(不可能有一个点同时在第
时间复杂度 。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 210,M = 5e4+10;
//二分图匹配模型
int n,m,q;
struct made{
int ver,nx;
}e[M<<1];
int hd[N],tot,ans;
int match[N];
bool v[N],f[N][N];
void add(int x,int y){
tot++;
e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
bool dfs(int x){
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(v[y])continue;
v[y] = 1;
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
return 0;
}//
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i = 1;i <= q;i++){
int x,y;
scanf("%d%d",&x,&y);
f[x][y] = 1;
}
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
if(!f[i][j])add(i,j);//
for(int i = 1;i <= n;i++){
memset(v,0,sizeof(v));
if(dfs(i))ans++;
}
printf("%d\n",ans);
return 0;
}
III P1402 酒店之王
题意:给定一个三分图(雾,一个人有喜欢与不喜欢的菜品,以及喜欢与不喜欢的房间,只有两个条件都满足该客人才会留下,求最大客人数。
我们可以从客人向菜品与房间分别跑一次最大匹配,只有都匹配到了则
点击查看代码
#include <bits/stdc++.h>
using namespace std;
//三分图最大匹配(雾
const int N = 210,M = 5e4+10;
int n,m,k,ans;
struct made{
int ver,nx;
}e1[M],e2[M];
int hd1[N],hd2[N],tot1,tot2;
int m1[N],m2[N],l1[N],l2[N];
bool v1[N],v2[N];
void add1(int x,int y){
tot1++;
e1[tot1].nx = hd1[x],e1[tot1].ver = y,hd1[x] = tot1;
}
void add2(int x,int y){
tot2++;
e2[tot2].nx = hd2[x],e2[tot2].ver = y,hd2[x] = tot2;
}
bool dfs1(int x){
for(int i = hd1[x];i;i = e1[i].nx){
int y = e1[i].ver;
if(v1[y])continue;
v1[y] = 1;
if(!m1[y] || dfs1(m1[y])){
m1[y] = x;
return 1;
}
}
return 0;
}
bool dfs2(int x){
for(int i = hd2[x];i;i = e2[i].nx){
int y = e2[i].ver;
if(v2[y])continue;
v2[y] = 1;
if(!m2[y] || dfs2(m2[y])){
m2[y] = x;
return 1;
}
}
return 0;
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++){
int x;scanf("%d",&x);
if(x)add1(i,j);
}
for(int i = 1;i <= n;i++)
for(int j = 1;j <= k;j++){
int x;scanf("%d",&x);
if(x)add2(i,j);
}
for(int i = 1;i <= n;i++){
memset(v1,0,sizeof(v1));
memset(v2,0,sizeof(v2));
memcpy(l1,m1,sizeof(m1));
memcpy(l2,m2,sizeof(m2));
if(dfs1(i) && dfs2(i))ans++;
else{//不符
memcpy(m1,l1,sizeof(l1));
memcpy(m2,l2,sizeof(l2));//还原
}
}
printf("%d\n",ans);
return 0;
}
IV P2055 [ZJOI2009] 假期的宿舍
本题非常好,建议先自己做。
题意:有一些人需要找一些座位,一些人原来就有座位,一些人是新来的,也有一些人(有座位的)要离开,这些人中有一些关系,每个人只能坐在和自己有直接关系(包括自己)的座位上,判断留下来的人是否都能找到座位。
首先我们找出座位,以及有座位但是要离开的人
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110,M = 2e4+10;
int t,n,now,ans;
struct made{
int ver,nx;
}e[M];
int hd[N],tot;
int match[N],a[N],b[N];
bool v[N];
void add(int x,int y){
tot++;
e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
bool dfs(int x){
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(v[y])continue;
v[y] = 1;
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
return 0;
}
int main(){
scanf("%d",&t);
while(t--){
tot = ans = now = 0;
memset(match,0,sizeof(match));
memset(hd,0,sizeof(hd));
scanf("%d",&n);
for(int i = 1;i <= n;i++)scanf("%d",&a[i]);
for(int i = 1;i <= n;i++){
scanf("%d",&b[i]);
if(a[i] && b[i])now++;
}
for(int i = 1;i <= n;i++){
if(a[i] && !b[i])add(i,i);//自己有座位
for(int j = 1;j <= n;j++){
int x;scanf("%d",&x);
if(a[i] && b[i])continue;//i离开了
if(x && a[j])add(i,j);
}
}
for(int i = 1;i <= n;i++){
memset(v,0,sizeof(v));
if(dfs(i))ans++;
}
if(ans == n - now)printf("^_^\n");
else printf("T_T\n");
}
return 0;
}
V 374. 导弹防御塔
题意:有一些防御塔可以发出导弹攻击目标,防御塔需要
首先我们可以发现如果时间较短可以完全摧毁,则时间更长一定可以,显然答案具有单调性,可以二分,问题转化为是否能在
我们可以先算出
时间复杂度 。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 60,M = 3e5+10;
const double eps = 1e-8;
//二分图多重匹配
int n,m;
double t1,t2,V;
struct made{
int ver,nx;
}e[M];
int hd[N],tot;
int match[M];
bool v[M];
struct node{
int x,y;
}a[N],b[N];
void add(int x,int y){
tot++;
e[tot].ver = y,e[tot].nx = hd[x],hd[x] = tot;
}
double distan(int x,int y){
return (double)sqrt((a[x].x-b[y].x)*(a[x].x-b[y].x)+(a[x].y-b[y].y)*(a[x].y-b[y].y));
}
bool dfs(int x){
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(v[y])continue;
v[y] = 1;
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
return 0;
}//模板
void build(double mid){
int cnt = 0;
for(int i = 1;i <= n;i++){
double t = t1;
for(int k = 1;k <= m;k++){
if(t > mid)break;
cnt++;
for(int j = 1;j <= m;j++){
double dis = distan(i,j);
if(dis / V + t <= mid)add(j,cnt);
}
t += t1 + t2;
}
}//建二分图
}
bool check(double mid){
tot = 0;
memset(hd,0,sizeof(hd));
memset(match,0,sizeof(match));
build(mid);
for(int i = 1;i <= m;i++){
memset(v,0,sizeof(v));
if(!dfs(i))return 0;//
}
return 1;
}
int main(){
scanf("%d%d%lf%lf%lf",&n,&m,&t1,&t2,&V);
t1 /= 60;
for(int i = 1;i <= m;i++)scanf("%d%d",&b[i].x,&b[i].y);
for(int i = 1;i <= n;i++)scanf("%d%d",&a[i].x,&a[i].y);
double l = t1,r = 1e6;
while(l + eps < r){
double mid = (l + r) / 2;
if(check(mid))r = mid;
else l = mid;
}
printf("%.6lf\n",l);
return 0;
}
VI 375. 蚂蚁
题意:在直角坐标系中有
首先我们观察两个点相交与不相交时的性质:
根据三角形两边之和一定大于第三边,我们可以发现把相交的线段变为不相交的线段使得线段总和变短了,这相当于求二分图的最小匹配,又因为该图是一个稠密图,且存在完备匹配,所以用KM算法更好,我们把边权取反,求最大匹配就行了。
时间复杂度 。
注:注意一下浮点数。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110,M = 2e4+10;
const double eps = 1e-8;
//二分图带权最大匹配
int n;
struct node{
int x,y;
}a[N],b[N];
int hd[N],tot;
int match[N];
double la[N],lb[N],w[N][N],upd[N],d;
bool va[N],vb[N];
double distan(int x,int y){
return sqrt((double)(a[x].x-b[y].x)*(a[x].x-b[y].x)+(a[x].y-b[y].y)*(a[x].y-b[y].y));
}
bool dfs(int x){
va[x] = 1;//
for(int y = 1;y <= n;y++){
if(!vb[y]){
if(fabs(la[x] + lb[y] - w[x][y]) < eps){//相等子图
vb[y] = 1;//
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
else upd[y] = min(upd[y],la[x] + lb[y] - w[x][y]);
}
}
return 0;
}//匈牙利算法
int main(){
scanf("%d",&n);
for(int i = 1;i <= n;i++)scanf("%d%d",&b[i].x,&b[i].y);
for(int i = 1;i <= n;i++)scanf("%d%d",&a[i].x,&a[i].y);
//KM
for(int i = 1;i <= n;i++){
la[i] = INT_MIN;
lb[i] = 0;
for(int j = 1;j <= n;j++){
w[i][j] = -distan(i,j);//将最小匹配改为最大匹配
la[i] = max(la[i],w[i][j]);
}
}
for(int i = 1;i <= n;i++){
while(1){
memset(va,0,sizeof(va));
memset(vb,0,sizeof(vb));
d = INT_MAX;
for(int j = 1;j <= n;j++)upd[j] = INT_MAX;
if(dfs(i))break;
for(int j = 1;j <= n;j++)
if(!vb[j])d = min(d,upd[j]);
for(int j = 1;j <= n;j++){
if(va[j])la[j] -= d;
if(vb[j])lb[j] += d;//
}
}
}
//
for(int i = 1;i <= n;i++)printf("%d\n",match[i]);
return 0;
}
VII P6577 【模板】二分图最大权完美匹配
4. 二分图的覆盖
4.1 二分图最小点覆盖
- 给定一个二分图,求出一个最小的点集
,使得图中任意一条边都有至少一个端点属于 ,称为二分图的最小点覆盖。
König 定理:二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数。
证明
我们可以构造一组点覆盖,使其包含的点数等于最大匹配包含的边数。 构造如下:- 先求出最大匹配。
- 从每个左部非匹配点出发,再
一遍并记录所访问过的点。 - 取左部未被标记的点,右部被标记的点,就可以得到二分图最小点覆盖。
我们发现:
- 左部非匹配节点一定被标记————因为是出发点。
- 右部非匹配节点一定未被标记————不然有增广路。
- 一对匹配点一定同时被标记或未被标记————因为只要到右部匹配点一定连到左部匹配点。
所以我们取了左部未被标记的节点,右部被标记的节点,可以发现选出的点数一定与匹配边一一对应。
再来看是否符合所有边都被覆盖:
- 匹配边一定被覆盖。
- 非匹配边中两端如果左端为非匹配点,那么右部节点一定被标记,会被覆盖。
- 非匹配边中两端如果右端为非匹配点,那么左部结点一定未被标记,不然就有增广路,也会被覆盖。
得证。
4.2 例题
I 376. 机器任务
题意:有两个机器
二分图最小覆盖模型: 每条边有
本题中每个任务至少选
时间复杂度 。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
//二分图最小点覆盖
const int N = 110,M = 2e4+10;
int n,m,k;
struct made{
int ver,nx;
}e[M];
int hd[N],tot,ans;
int match[N];
bool v[N];
void add(int x,int y){
tot++;
e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
bool dfs(int x){
for(int i = hd[x];i;i = e[i].nx){
int y = e[i].ver;
if(v[y])continue;
v[y] = 1;
if(!match[y] || dfs(match[y])){
match[y] = x;
return 1;
}
}
return 0;
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i = 1;i <= k;i++){
int op,x,y;
scanf("%d%d%d",&op,&x,&y);
if(!x || !y)continue;//最开始为模式0,关于0的模式都不需要改变
x++,y++;
add(x,y);
}
for(int i = 1;i <= n;i++){
memset(v,0,sizeof(v));
if(dfs(i))ans++;
}
printf("%d\n",ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!