【笔记】二分图
二分图
二分图:可以把所有点分为两个点集,相同点集里的点之间没有边的无向图。
当且仅当一个无向图中有奇环存在时,这个图不是二分图。
一、二分图判定 - 染色法
我们可以把图中的点染成两种颜色,白色是一个点集,黑色是另一个点集。
染色的规则:因为相同点集里的点之间没有边,所以每个节点的颜色必须与父节点不同。
实现方法:从任意一个节点开始遍历,模拟染色的过程。当遇到已经遍历过的结点时,如果这个结点又要被染成另一种颜色,那么这个图就不是二分图。使用 DFS 或 BFS 实现。
连通块:如果图不连通,那么当所有连通块都是二分图的时候,整个图也是一张二分图。因此在遍历的过程中要对所有连通块分别染色。
bool dfs(int u,int col){
vis[u]=1;
c[u]=col;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v]){
if(c[u]!=c[v]) continue;
else return 0;
}
if(!dfs(v,col^1)) return 0;
}
return 1;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
if(!dfs(i,0)){
cout<<"NO";
return 0;
}
}
}
cout<<"YES";
时间复杂度:\(O(n+m)\)。
另:并查集判断二分图
类似扩展域并查集,对于每个边 \((u,v)\),合并其敌人关系 \((u,v+n)\) 和 \((u+n,v)\)。
判断是否出现 \((u,u+n)\) 或 \((v,v+n)\) 即可。
bool check(){
for(int i=1;i<=m;i++){
Merge(e[i].u+n,e[i].v);
Merge(e[i].u,e[i].v+n);
if(Find(e[i].u)==Find(e[i].u+n)||Find(e[i].v)==Find(e[i].v+n)) return 0;
}
return 1;
}
二、二分图最大匹配 - 匈牙利算法
记二分图 \(G\langle V_1,V_2,E\rangle\)。\(V_1,V_2\) 为两个点集,\(E\) 为边集。
匹配:在二分图中选择若干条边组成一个边集合,使得边集中的边连接的点两两没有公共点。
最大匹配:边数量最多的匹配。
匈牙利算法:求二分图最大匹配的算法。
算法思路:考虑贪心求解。遍历集合 \(V_1\) 中的每个点 \(u\),寻找 \(u\) 的邻居结点(与 \(u\) 相连),如果 \(v\) 还没有被匹配,那么将 \(v\) 与 \(u\) 配对;如果 \(v\) 匹配过了,那么就去寻找与 \(v\) 配对的结点 \(k\) 的其他邻居结点,如果 \(k\) 能按照同样的方式找到新的匹配,那么就把 \(k\) 与新匹配配对,把 \(v\) 与 \(u\) 配对。如果 \(k\) 无法找到新匹配,那么 \(u\) 也就匹配失败。
增广路:寻找新匹配的路径叫做增广路。增广路由奇数条边组成,第奇数条边为非匹配边,第偶数条边为匹配边。因此匈牙利算法也可理解为寻找增广路的过程:每次找到增广路,因为增广路非匹配边比匹配边多 \(1\),所以把增广路取反,将原来的匹配边变为非匹配边,这样匹配数就多了 \(1\)。直到找不到增广路,就求得了最大匹配。
时间复杂度:由于寻找新配对结点最多遍历整张图,所以时间复杂度为 \(O(nm)\)。但实际情况往往跑不满,因为很少会让每个点都更改自己的匹配。
实现方式:维护一个数组 \(p\),记录 \(V_2\) 中的点与 \(V_1\) 中的点的配对情况,以及一个标记数组 vis 记录结点是否被遍历过。对每个结点都进行上述操作,每次清空 vis,即可求出最大匹配。
注意事项:因为匹配时永远是 \(V_1\) 找 \(V_2\),所以建图时建单双向边均可(但必须有 \(V_1\) 指向 \(V_2\) 的边)。同时两个点集的编号可以独立也可以共用(共用就必须建单向边了),因为只有 \(V_1\) 中的点会作为 \(u\) 进行搜索,其结点编号存储在 \(p\) 中,而且边表中我们也只存从 \(V_1\) 的点出发的边;而 \(V_2\) 中结点编号只作为 \(p\) 下标。
//p:匹配的节点。vis:是否遍历过。
bool dfs(int u){
if(vis[u]++) return 0;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!p[v]||dfs(p[v])){
p[v]=u;
return 1;
}
}
return 0;
}
for(int i=1;i<=n;i++){
if(dfs(i)) ans++;
for(int j=1;j<=n;j++) vis[j]=0;//每次清空vis,只用为左部点清空
}
时间复杂度 \(O(n(n+m))\)。
衍生问题:二分图最小点覆盖、二分图最大独立集、最小路径覆盖
最小点覆盖:在无向图中,选最少的点,使得每条边两端至少有一个被选中。二分图中,最小点覆盖=最大匹配(一般图中为 NP Hard 问题)。
最大独立集:在无向图中,选最多的点,使得每条边两端至多有一个被选中。也就是一个点集,点集中的各点没有关系。最大独立集和最小点覆盖互补,任何一个最小点覆盖,它的补集一定是最大独立集。二分图中,最大独立集=总点数-最大匹配(一般图中为 NP Hard 问题)。
最大独立集的构造:选择所有未匹配点,及每条匹配边中一个端点,此时选中点两两没有关系且点数最大。从每个左部未匹配点出发跑交替路,从左部到右部是未匹配边,从右部回来是匹配边。此时左部访问到的节点是所有左部未匹配点和匹配边的左端点,右部访问到的节点是匹配边的右端点。左部访问到的点和右部未访问到的点即是一个最大独立集。由于最大独立集合最小点覆盖互补,最小点覆盖即为左部未访问到的点和右部访问到的点。反证法易证。
最小路径覆盖:在有向图中,选择最少的简单路径,覆盖所有点集,且各路径点集之间不允许有交集,要求路径数最少。有向无环图可进行拆点(每个点 \(u\) 拆成两个点 \(ul\) 入点和 \(ur\) 出点,有向边 \((u,v)\) 变为无向边 \((ur,vl)\),因为原图是有向无环图,所以拆点后为二分图,出入点分别成为新图的左部和右部)。有向无环图的最小路径覆盖=原图结点数-拆点后二分图最大匹配数(一般有向图中为 NP Hard 问题)。
三、Hall 定理
二分图完美匹配:设有二分图 \(G\langle V_1,V_2,E\rangle\),边集 \(M\) 为其中的最大匹配,且 \(|M|=|V_1|\),则 \(M\) 是 \(V_1\) 到 \(V_2\) 的完美匹配。
设有二分图 \(G\langle V_1,V_2,E\rangle\),\(|V_1|\le|V_2|\),如果 \(G\) 中存在 \(V_1\) 到 \(V_2\) 的完美匹配,当且仅当对于任意的点集 \(S \subset V_1\),均有 \(|S| \le |N(S)|\),其中 \(N(S)\) 是 \(S\) 的邻居集合,即与 \(S\) 点集有边相连的所有点的集合(全部在 \(V_2\) 集合里)。
四、例题
1. Luogu P1525 [NOIP 2010 提高组] 关押罪犯
最大值最小,二分答案枚举最大值,并将边权 \(>mid\) 的边保留作为二分图中的边,若能染色成功,则合法。找到最小的合法 \(mid\)。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn=2e4+10;
const int maxm=1e5+10;
struct Node{
int to,nxt,w;
}e[2*maxm];
int n,m;
int d[maxm],c[maxn],mid;
int h[maxn],tot;
bool vis[maxn];
void Add(int u,int v,int w){
tot++;
e[tot].to=v;
e[tot].nxt=h[u];
e[tot].w=w;
h[u]=tot;
}
bool dfs(int u,int col){
vis[u]=1;
c[u]=col;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].w<=d[mid]) continue;
if(vis[v]){
if(c[u]!=c[v]) continue;
else return 0;
}
if(!dfs(v,col^1))return 0;
}
return 1;
}
bool check(){
memset(vis,0,sizeof(vis));
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++){
if(!vis[i]){
if(!dfs(i,0)) return 0;
}
}
return 1;
}
int bfind(){
int l=0,r=2*m;
while(l<r){
mid=(l+r)/2;
if(check()) r=mid;
else l=mid+1;
}
return l;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
d[i]=w;
Add(u,v,w);
Add(v,u,w);
}
if(m==1){
cout<<0;
return 0;
}
sort(d+1,d+1+m);
cout<<d[bfind()];
return 0;
}
2. Luogu P1640 [SCOI2010] 连续攻击游戏
以属性值为左部点,武器为右部点,建立二分图。选择武器属性值就是二分图匹配的过程。由于求 \(\operatorname{mex}-1\),将属性值从小到大依次匹配,输出最后一个匹配成功的属性值。
此题来不及每次清空 vis,于是我们每次遍历都给代表已访问的 vis 换一个数,这样相当于一开始全都没访问。
分析时间复杂度。正常匈牙利算法时间复杂度为 \(O(nm)\),但此题我们只对 \(m\)(属性值)进行搜索,单次搜索也最多跑 \(m\) 次,复杂度就是 \(O(m^2)\),足够通过。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int M=4e6+10;
int n,ans;
int h[N],tot;
int vis[N],p[N],now;
struct Node{
int to,nxt;
}e[M];
void Add(int u,int v){
tot++;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
bool dfs(int u){
if(vis[u]==now) return 0;
vis[u]=now;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!p[v]||dfs(p[v])){
p[v]=u;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
int x,y;
cin>>x>>y;
Add(x,i);
Add(y,i);
}
now=1;
for(int i=1;i<=10000;i++,now++){
if(dfs(i)) ans++;
else break;
}
cout<<ans;
return 0;
}
3. Luogu P5182 棋盘覆盖
- 每个格子上最多放一个骨牌
- 骨牌占用相邻两个格子
骨牌相当于相邻的两个格子连边。对上下左右相邻的格子建无向边。边只在相邻的点之间,此时如果我们对相邻格子染上不同的颜色(类似国际象棋棋盘),相同颜色的归为一个点集,可以发现是二分图。然后跑二分图最大匹配。时间复杂度 \(O(n^2m^2)\)。
需要注意,如果我们对所有点连无向边,且对所有点跑匹配,最终一对匹配的两个点都会算上,答案需要 \(\div 2\)。如果只对一种颜色的格子向另一种颜色连有向边,匹配时也只跑一种颜色的格子,就不用担心这个问题,时空复杂度也更优。判断颜色的方式是 \(i+j\) 的奇偶性。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,t,ans;
int h[N],tot;
int a[110][110];
int p[N];
bitset<N> vis;
struct Node{
int to,nxt;
}e[N];
void Add(int u,int v){
tot++;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
bool dfs(int u){
if(vis[u]) return 0;
vis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!p[v]||dfs(p[v])){
p[v]=u;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>t;
for(int i=1;i<=t;i++){
int x,y;
cin>>x>>y;
a[x][y]=1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(a[i][j]) continue;
if(i<n&&!a[i+1][j]) Add((i-1)*n+j,i*n+j),Add(i*n+j,(i-1)*n+j);
if(j<n&&!a[i][j+1]) Add((i-1)*n+j,(i-1)*n+j+1),Add((i-1)*n+j+1,(i-1)*n+j);
}
}
for(int i=1;i<=n*n;i++){
vis=0;
if(dfs(i)) ans++;
}
cout<<ans/2;
return 0;
}
4. Luogu P1129 [ZJOI2007] 矩阵游戏
根据交换操作:同一行的黑格,不论怎么交换,永远在同一行。同一列的同理。
最终目标:对角线为黑。即每一行、每一列都有一个黑格。即存在 \(n\) 个黑格,他们互不在一行、一列。
我们选中一个黑格,就不希望与他同行、同列的再被选。每一行、每一列只被一个黑格选中,类似二分图匹配。对于每个黑格,其横纵坐标分别作为二分图的左、右部点,黑格作边。若最后所有的横纵坐标均能匹配成功,则有解。否则无。
时间复杂度 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;
int T,n;
int g[210][210];
int p[210];
bitset<210> vis;
bool dfs(int u){
if(vis[u]) return 0;
vis[u]=1;
for(int i=1;i<=n;i++){
if(g[u][i]){
if(!p[i]||dfs(p[i])){
p[i]=u;
return 1;
}
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>T;
while(T--){
cin>>n;
for(int i=1;i<=n;i++){
p[i]=0;
for(int j=1;j<=n;j++){
cin>>g[i][j];
}
}
bool flag=1;
for(int i=1;i<=n;i++){
vis=0;
if(!dfs(i)){
flag=0;
break;
}
}
if(flag) cout<<"Yes\n";
else cout<<"No\n";
}
return 0;
}
5. UVA1194 Machine Schedule
每个任务有两种选择,两种选择至少选一个,求最少选几个。
因此按两台机器的模式为点,任务为边,建立二分图。问题转化为二分图的最小点覆盖,选最少的点覆盖所有边,可以归约为最大匹配问题。
#include<bits/stdc++.h>
using namespace std;
const int N=210;
const int M=1e3+10;
int n,m,k;
int h[N],tot;
int p[N],ans;
bitset<N> vis;
struct Node{
int to,nxt;
}e[M];
void Add(int u,int v){
tot++;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
bool dfs(int u){
if(vis[u]) return 0;
vis[u]=1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!p[v]||dfs(p[v])){
p[v]=u;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
while(cin>>n&&n){
cin>>m>>k;
ans=tot=0;
memset(h,0,sizeof(h));
memset(p,0,sizeof(p));
for(int i=1;i<=k;i++){
int x,u,v;
cin>>x>>u>>v;
Add(u,v+n);
}
for(int i=1;i<=n;i++){
vis=0;
if(dfs(i)) ans++;
}
cout<<ans<<'\n';
}
return 0;
}
6.Luogu P10939 骑士放置
一个格子若放了马,那其周围的八个日字上的格子都不能放。我们可以把这些格子之间连上边,此时变成一张二分图。要求最多能放多少个马,也就是最多多少个格子之间没有连边,也就是二分图最大独立集,用总点数(\(N\times M-T\))减去最大匹配。
不难发现,日字连边连接的两点,如果放在黑白相间的国际象棋棋盘上,两点的颜色一定不同。因此我们也可以只对一种颜色往另一种颜色连有向边,对一种颜色跑最大匹配。
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10;
const int M=2e5+10;
const int dx[9]={0,1,2,2,1,-1,-2,-2,-1};
const int dy[9]={0,2,1,-1,-2,-2,-1,1,2};
int n,m,t,ans;
int a[110][110];
int h[N],tot;
int vis[N],p[N],now;
struct Node{
int to,nxt;
}e[M];
void Add(int u,int v){
tot++;
e[tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
int calc(int x,int y){
return (x-1)*m+y;
}
bool dfs(int u){
if(vis[u]==now) return 0;
vis[u]=now;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if(!p[v]||dfs(p[v])){
p[v]=u;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m>>t;
for(int i=1;i<=t;i++){
int x,y;
cin>>x>>y;
a[x][y]=1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
int xx,yy;
if((i+j)%2||a[i][j]) continue;
for(int k=1;k<=8;k++){
xx=i+dx[k],yy=j+dy[k];
if(xx>=1&&xx<=n&&yy>=1&&yy<=m&&!a[xx][yy]) Add(calc(i,j),calc(xx,yy));
}
}
}
now=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if((i+j)%2||a[i][j]) continue;
now++;
if(dfs(calc(i,j))) ans++;
}
}
cout<<n*m-t-ans;
return 0;
}
7. Luogu P2764 最小路径覆盖问题
先考虑怎么用路径覆盖。一开始可以把每个点都看成一个路径,此时能覆盖全图。
然后,因为有向边的存在,路径之间可以拼接。但一个点可以合并两次,一次是与某一个出边连接的点所在的路径合并,一次是与某一个入边连接的点所在的路径合并。
这令我们非常不爽。因此直接把每个点 \(u\) 拆了,拆成一个入点 \(ul\) 和一个出点 \(ur\),专门连入边、出边。这样每个点只合并一次,就能实现路径的拼接。
于是路径就从这样:

变成了这样:

此时一个点就只与一个点匹配了,未匹配的点是路径的端点。
一开始,路径个数是 \(n\)。每拼接两个路径,路径个数就 \(-1\)。要求最小路径覆盖,自然需要合并次数尽可能多,也就是两个点匹配尽可能多。于是我们一开始对整张无环图拆点,问题就转化为了以左部点为出点、右部点为入点的二分图最大匹配。最后答案即为 \(n-\) 匹配数。
最后考虑怎么输出这些路径。观察上图,没有匹配的入点所对应的出点一定是路径的起点。根据这个找到起点,搜到终点依次输出即可。
#include<bits/stdc++.h>
using namespace std;
const int N=310;
const int M=2e4+10;
struct Node{
int to,nxt;
}e1[M],e2[M];
int n,m,ans;
int h1[N],h2[N],tot;
int p[N],ind[N];
bitset<M>vis;
void Add1(int u,int v){
tot++;
e1[tot].to=v;
e1[tot].nxt=h1[u];
h1[u]=tot;
}
void Add2(int u,int v){
tot++;
e2[tot].to=v;
e2[tot].nxt=h2[u];
h2[u]=tot;
}
bool dfs1(int u){
if(vis[u]) return 0;
vis[u]=1;
for(int i=h1[u];i;i=e1[i].nxt){
int v=e1[i].to;
if(!p[v]||dfs1(p[v])){
p[v]=u;
return 1;
}
}
return 0;
}
void dfs2(int u){
cout<<u<<' ';
for(int i=h2[u];i;i=e2[i].nxt){
int v=e2[i].to;
dfs2(v);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
Add1(u,v+n);//1~n 出点,n+1~2n 入点
}
for(int i=1;i<=n;i++){
if(dfs1(i)) ans++;
vis=0;
}
tot=0;
for(int i=n+1;i<=2*n;i++){
if(p[i]){
Add2(p[i],i-n);//将匹配的出入点用原结点重新建图
ind[i-n]++;//统计入度
}
}
for(int i=1;i<=n;i++){
if(!ind[i]){//找到起点,搜索
dfs2(i);
cout<<'\n';
}
}
cout<<n-ans;
return 0;
}

浙公网安备 33010602011771号