二分图笔记

二分图定义

二分图是一张无向图,可以分成 2 个集合 AB。在同一个集合中的点没有边相连。

二分图判定

当且仅当图中不存在奇数环时,该图为二分图。

证明:反证法,构造一个奇数环。容易发现无论如何都不可能使相邻 2 点分到 2 个集合。

那么很容易想到一个判定二分图的方法:dfs染色法。

可以用 2 种颜色给图上的点进行染色。有边相连的点应该染成相反的颜色。如果在染色过程中出现冲突,则此图不是二分图。

示例代码:

bool dfs(int x,int c){
clr[x]=c;
for(int i=0;i<to[x].size();i++){
int v=to[x][i];
if(!vst[v]&&!dfs(v,3-c)) return 0;
else if(clr[v]==clr[x]) return 0;
}
return 1;
}

易证该算法的时间复杂度是 Θ(n)

例题1 P1525 关押罪犯。对于本题,我们希望分配到同一个监狱里的罪犯的最大仇恨值(即边权值)最小化,容易想到二分答案。

判定:存在一种方案,使得冲突影响力不超过mid。

该判定方法显然符合单调性(mid较小的方案,对于更大的mid一定可行)。

因此,如果我们忽略所有边权值小于mid的边后形成的图进行判定二分图成立,那么方案可行,继续二分mid,直到找到答案。

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,maxn,ans;
vector<int> t[20009],a[20009];
int vst[20009],Big=1;
void dfs(int u,int color,int num){
if(!Big) return ;
vst[u]=color;
for(int i=0;i<t[u].size();i++){
if(a[u][i]>num){
if(!vst[t[u][i]]) dfs(t[u][i],3-color,num);
else if(vst[t[u][i]]==color) Big=0;
}
}
return ;
}
bool ok(int num){
Big=1;
for(int i=1;i<=n;i++) vst[i]=0;
for(int i=1;i<=n&&Big;i++){
if(!vst[i]) dfs(i,1,num);
}
if(Big) return 1;
return 0;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int pa,pb,c; cin>>pa>>pb>>c;
t[pa].push_back(pb); a[pa].push_back(c);
t[pb].push_back(pa); a[pb].push_back(c);
maxn=max(c,maxn);
}
int l=0,r=maxn,mid;
ans=r;
while(l<=r){
mid=(l+r)/2;
if(ok(mid)){ans=mid;r=mid-1;}
else left=mid+1;
}
cout<<ans;
return 0;
}

二分图匹配

匹配是一个边的集合 E ,满足任意 2 条边之间没有公共端点。而最大匹配即为二分图中,包含边数最多的一组匹配(max|E|)。

首先了解以下几个概念:

匹配边:(x,y)E,反之称为非匹配边。

匹配点:如果 (x,y)E,则 xy 为匹配点。反之 xy 为非匹配点。

交错路:一条非匹配边和匹配边交替经过的路径。

增广路:一边的非匹配点到另一边的非匹配点的交错路。

寻找二分图最大匹配的算法为匈牙利算法,算法步骤:

1.设 S 为空集,所有边都是非匹配边

2.寻找增广路path,如果找到,把所有路径上的边的匹配状态取反,得到一个更大的匹配。

3.重复第 2 步,直到图中不存在增广路。

寻找增广路:递归地从每个点 x 出发递归 y 寻找增广路。如果找到,一边回溯一边修改匹配状态。match[x]=y

要么 y 没有匹配点,要么 y 有匹配点,但是从 y 的匹配点出发递归去找增广路,可以找到。

dfs的时间复杂度为 Θ(n),匈牙利算法的时间复杂度为 Θ(n2)

示例代码:

#include<bits/stdc++.h>
using namespace std;
const int N=100009;
int n,m,ans,match[N];
vector<int> to[N];
bool vst[N];
bool dfs(int x){
for(int i=0;i<to[x].size();i++){
int y=to[x][i];
if(vst[y]) continue;
if(!match[y]||dfs(match[y])){
match[y]=x;
return 1;
}
}
return 0;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y; cin>>x>>y;
to[x].push_back(y);
to[y].push_back(x);
}
for(int i=1;i<=n;i++){
memset(vst,0,sizeof(vst));
if(dfs(i)) ans++;
}
cout<<ans;
return 0;
}

例题1 T270574 棋盘覆盖。本题主要考察的是二分图建模,建模时要满足以下 2 个要求。

  1. 节点可以分成 2 个集合,集合内部的点没有边相连。
  2. 每个节点只能有 1 条边(匹配边)。

该题要求任意 2 张骨牌都不会重叠,即每个格子只能被 1 张骨牌覆盖,而每张骨牌可以覆盖 2 个格子。把骨牌看作边,格子看作点。

考虑把每个没有被禁止的格子分为 2 个不同的集合。

将格子像国际象棋棋盘交叉黑白染色,可以划分为 2 个不重叠的集合。黑色格子的行号+列号为奇数,白色格子的行号+列号为偶数。容易发现每个骨牌必然覆盖一个黑色格子和一个白色格子。

要求所需的最大骨牌数量,即求二分图最大匹配,用匈牙利算法。

时间复杂度为 Θ(n3)

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,t,ans,match[10009],to[2009][2009];
bool vst[10009];
int tot,To[400009],head[400009],nxt[400009];
void add(int u,int v){
To[++tot]=v;
nxt[tot]=head[u];
head[u]=tot;
}
bool ok(int x){
for(int i=head[x];i;i=nxt[i]){
int y=To[i];
if(vst[y]) continue;
vst[y]=1;
if(!match[y]||ok(match[y])){
match[y]=x;
return 1;
}
}
return 0;
}
int main(){
cin>>n>>t;
for(int i=1;i<=t;i++){
int x,y;
cin>>x>>y;
to[x][y]=1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<n;j++){
if(to[i][j]||to[i][j+1]) continue;
int u=(i-1)*n+j,v=u+1;
add(u,v);
add(v,u);
}
}
for(int i=1;i<n;i++){
for(int j=1;j<=n;j++){
if(to[i][j]||to[i+1][j]) continue;
int u=(i-1)*n+j,v=i*n+j;
add(u,v);
add(v,u);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(to[i][j]||(i+j)%2) continue;
int u=(i-1)*n+j;
memset(vst,0,sizeof(vst));
ans+=ok(u);
}
}
cout<<ans;
return 0;
}

例题2 T270575 車的放置。首先显然每行每列只能有 1 个車。考虑构造行和列 2 个集合,車为连边,那么車的数量就是行和列的最大匹配边数。但要求有 T 个点不能放置,禁止点 (i,j) 即行 i 和列 j 不能有连边。

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,t,ans;
int match[209];
int to[209][209];
bool vst[209];
bool ok(int x){
for(int i=1;i<=m;i++){
if(!vst[i]&&!to[x][i]){
vst[i]=1;
if(!match[i]||ok(match[i])){
match[i]=x;
return 1;
}
}
}
return 0;
}
int main(){
cin>>n>>m>>t;
for(int i=1;i<=t;i++){
int x,y;
cin>>x>>y;
to[x][y]=1;
}
for(int i=1;i<=n;i++){
memset(vst,0,sizeof(vst));
ans+=ok(i);
}
cout<<ans<<endl;
return 0;
}

例题3 T311481 导弹防御塔。题目要求“最大时间最小化”,因此显然符合二分答案的单调性,考虑二分答案来解决此问题。

二分图的多重匹配,即给出一个包含 n 个左部节点和 m 个右部节点的二分图,从中选出尽量多的边,使第 i 个左部节点,最多和 x 条选出的边相连,使第 j 个右部节点最多 y 条选出的边相连。

x=y=1 时,就是二分图的最大匹配。

解决多重匹配有以下几种方案:

  1. 拆点:即把左部每个点拆成至多 x 个点,右部的点拆成至多 y 个点,套用最大匹配即可。
  2. 网络流,在此不多赘述。

对于本题,对于 m 个入侵者和 n 枚导弹 2 个集合,易证对于mid的时间,最多发射出 p=mid+T2T1+T2 枚导弹,即把 1 枚导弹拆成 p 枚导弹。但注意导弹在空中飞行也有时间,不一定在 mid 时间前发射的导弹就一定能在时间限制前摧毁目标。

因此本题思路为 二分+拆点建图+二分图最大匹配。时间复杂度为 Θ(mnp+n2)

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const double eps=1e-9;
int n,m,match[4009];
double t1,t2,v,dist[59][59];
PII a[59],b[59];
bool vst[4009];
vector<int> to[59];
double get_dist(int i,int j){
double x=a[i].first-b[j].first;
double y=a[i].second-b[j].second;
return sqrt(x*x+y*y);
}
bool find(int x){
for(int i=0;i<to[x].size();i++){
int y=to[x][i];
if(vst[y]) continue;
vst[y]=1;
if(!match[y]||find(match[y])){
match[y]=x;
return 1;
}
}
return 0;
}
bool check(double mid){
memset(match,0,sizeof(match));
for(int i=1;i<=m;i++){
memset(vst,0,sizeof(vst));
if(!find(i)) return 0;
}
return 1;
}
int main(){
cin>>n>>m>>t1>>t2>>v;
t1/=60;
for(int i=1;i<=m;i++){
cin>>a[i].first>>a[i].second;
}
for(int i=1;i<=n;i++){
cin>>b[i].first>>b[i].second;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
dist[i][j]=get_dist(i,j);
}
}
double l=0,r=1e9;
while(r-l>eps){
double mid=(l+r)/2;
int p=(mid+t2)/(t1+t2);
p=min(p,m);
for(int i=1;i<=m;i++){
to[i].clear();
for(int j=1;j<=n;j++){
for(int k=1;k<=p;k++){
if(k*t1+(k-1)*t2+dist[i][j]/v<mid-eps){
to[i].push_back((j-1)*p+k);
}
}
}
}
if(check(mid)) r=mid;
else l=mid;
}
cout<<fixed<<setprecision(6)<<r;
return 0;
}

二分图的最小点覆盖

对于二分图,最小点覆盖的点数等价于二分图的最大匹配包含的边数,即为König定理。

证明:

设二分图最小点覆盖的点数为 n ,最大匹配书边数为 m,证明 n=m

1.证明 nm

因为所有的 m 条匹配边之间没有公共点。而最小点覆盖想要覆盖这些匹配边,至少也得要 m 个点才能完全覆盖。

所以 nm 显然成立。

2.证明 n=m 可以取到

构造方案:恰好取了 m 个点,且这 m 个点能够将所有的边覆盖掉。

构造方式:

  1. 求出最大匹配,有 m 条匹配边。

  2. 从左部每个非匹配点出发,跑一遍增广路径,将路径上的所有点标记(这里增广路径一定不会成功,因为成功的话就不是最大匹配了,最大匹配后没有增广路径)。

  3. 选出左边所有未被标记的点和右边所有被标记的点。

那么如何证明这种构造方式得到的点数是 m:

(1) 明确有以下三个性质:

1.左边所有的非匹配点一定都被标记(因为每次构造是从左边非匹配点出发的,是起点)

2.右边所有的非匹配点一定没有被标记(因为右边非匹配点被标记的话,就会形成增广路径)

3.对于每个匹配边,左右两点要么同时被标记,要么同时不被标记(因为左边的匹配点一定是从右边某个匹配点过来的)。

(2) 我们选择的是左边所有未被标记的点,则由性质 1 可知这些点一定是匹配点。

我们选择的是右边所有被标记的点,则由性质 2 可知这些点一定是匹配点。

所以我们选择的所有点一定是匹配边上的点。

(3) 而对于每个匹配边,左右要么同时被标记,要么同时不被标记。

同时被标记的匹配边,由于我们选择了右边所有被标记的点,所以这些匹配边我们全选了。

同时不被标记的匹配边,由于我们选择了左边所有不被标记的点,所以这些匹配边我们全选了。

(2),(3) 可知,我们的选择是所有的 m 条匹配边,且每个匹配边我们只会选择左,或者右边一个点。一共 m 个点。

所以这种构造方式得到的点数是 m

接着证明这种构造方式覆盖了所有的边。

首先匹配边我们已知全部覆盖,因为由上面的证明我们可以知道 m 条边匹配边都被选了。

剩下的非匹配边有两种情况:

(1) 左边的非匹配点连接右边的匹配点:

因为左边非匹配点我们都标记了,从这个非匹配点出发走增广路径,所以这样的边,它右边的匹配点也一定被标记,而右边被标记的点会被我们选择,所以我们选择的点会覆盖这样的边。

(2) 左边匹配点连接右边非匹配点:

如果左边匹配点被标记,且有这样的一条边,那么走下去,右边的这个非匹配点也会被标记。与性质 2 右边所有非匹配点都不会被标记矛盾(存在增广路径)。

所以这样的边的左边匹配点一定不会被标记,那么它会被选中,所以这样的边也会被覆盖。

(1),(2) 可知这种构造方式覆盖了所有的边。

综上,这种构造方式可以构造出用 m 个点覆盖所有的边的情况=最大匹配数,证明了等号可以取得。

所以 n=m,即最小点覆盖数=最大匹配数。

二分图最小点覆盖建模前提:每条边 2 个端点,并且每条边至少选择 1 个端点。

例题1 T311483 机器任务。把两台机器 A,B 看作 2 个集合,考虑二分图最小点覆盖的特点:每一条边的 2 个结点至少要选择 1 个,代入此题就是每个任务必须在 2 台机器中选择一台,要么用 ai 模式,要么用 bi 模式。因此本题答案显然为二分图最小点覆盖。

注意因为机器初始时为 0 ,因此要把端点有 0 的点排除在外(不影响重启次数)。

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,k;
vector<int> to[100009];
int match[109];
bool vst[109];
bool hungary(int x){
for(int i=0;i<to[x].size();i++){
int y=to[x][i];
if(vst[y]) continue;
vst[y]=1;
if(!match[y]||hungary(match[y])){
match[y]=x;
return 1;
}
}
return 0;
}
int main(){
while(cin>>n){
if(n==0) break;
cin>>m>>k;
for(int i=0;i<=n;i++) to[i].clear();
for(int i=1;i<=k;i++){
int j,a,b;
cin>>j>>a>>b;
if(a==0||b==0) continue;
to[a].push_back(b);
}
int ans=0;
for(int i=0;i<n;i++){
memset(vst,0,sizeof(vst));
if(hungary(i)) ans++;
}
cout<<ans<<endl;
}
return 0;
}

例题2 T311743 泥泞的区域。对于任意一个泥泞的格子 (i,j),要么是被横着盖,要么是被竖着盖,满足二分图最小点覆盖的建模前提。接下来将每一个泥泞的格子编横块号和列块号,将两个编号连起来,运行二分图最小点覆盖即可。

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x3f3f3f3f;
int n,m;
char a[59][59];
int match[2509];
int b[59][59],c[59][59];
bool vst[2509];
vector<int> to[2509];
bool hungary(int x){
for(int i=0;i<to[x].size();i++){
int y=to[x][i];
if(vst[y]) continue;
vst[y]=1;
if(!match[y]||hungary(match[y])){
match[y]=x;
return 1;
}
}
return 0;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++) cin>>a[i][j];
}
int cnt1=1,cnt2=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i][j]=='*') b[i][j]=cnt1;
else cnt1++;
}
cnt1++;
}
for(int j=1;j<=m;j++){
for(int i=1;i<=n;i++){
if(a[i][j]=='*') c[i][j]=cnt2;
else cnt2++;
}
cnt2++;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i][j]=='*'){
to[b[i][j]].push_back(c[i][j]);
}
}
}
int ans=0;
for(int i=1;i<=cnt1;i++){
memset(vst,0,sizeof(vst));
if(hungary(i)) ans++;
}
cout<<ans;
return 0;
}

二分图最大独立集

独立集:一个点集,其中任意 2 点之间都没有边相连。

最大独立集:点数最多的独立集。

二分图最大独立集 = n 二分图最小点覆盖(最大匹配)

证明:求最大独立集的过程可以看作是删去数量最少的点和它们的连边,使得所有边都被删除,剩下的点就是最大独立集。而删去的点集即为最小点覆盖,因此二分图最大独立集和二分图最小点覆盖互为补集。

例题1 T311485 骑士放置。将棋盘交叉黑白染色,观察发现,当骑士在黑色格子内时,能攻击的点必定是白色格子,反之必定是黑色格子。满足二分图“同一个集合内的点必定没有连边”。连边意味着能攻击到,而本题要求的是最多放置多少个不能攻击的骑士,因此选出来的点不能有连边,即求二分图最大独立集。

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x3f3f3f3f;
typedef pair<int,int> PII;
int cx[8]={2,2,-1,-1,1,1,-2,-2};
int cy[8]={-1,1,-2,2,-2,2,-1,1};
int n,m,t;
bool vst[109][109];
PII match[109][109];
int unable[109][109];
bool hungary(int x,int y){
for(int i=0;i<8;i++){
int nx=x+cx[i],ny=y+cy[i];
if(nx<1||nx>n||ny<1||ny>m||unable[nx][ny]||vst[nx][ny]) continue;
vst[nx][ny]=1;
if((match[nx][ny].first==0&&match[nx][ny].second==0)||hungary(match[nx][ny].first,match[nx][ny].second)){
match[nx][ny]=make_pair(x,y);
return 1;
}
}
return 0;
}
int main(){
cin>>n>>m>>t;
for(int i=1;i<=t;i++){
int x,y;
cin>>x>>y;
unable[x][y]=1;
}
int ans=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
memset(vst,0,sizeof(vst));
if(unable[i][j]||((i+j)&1)) continue;
if(hungary(i,j)) ans++;
}
}
cout<<n*m-ans-t;
return 0;
}
posted @   11jiang08  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示