二分图最小点覆盖
定义:一个最小的点集,使得图中的每一条边都至少有一个端点在点集中。
Konig定理:二分图的最小点覆盖的点数等于二分图的最大匹配的边数。
证明过于复杂,参考蓝书。
类比二分图最大匹配问题,问题转化为二分图最小点覆盖问题除了要满足0要素与1要素,还要满足2要素,即:
每一条关系边的两个端点中至少要有一个被选中。
例题1:AcWing 376.机器任务
此题显然,拿来练手
例题2:AcWing 377.泥泞的区域
这题关键在于构造二分图。根据贪心思想,一个覆盖的最佳方案,若修改成直接用长木板铺完一整行或一整列的泥泞区域,不会使得该方案变差。也就是说我们考虑一个点是否被覆盖,要么它就被所在行的木板覆盖,或者被所在列的木板覆盖。若一行或一列中的泥泞连通块用断开的木板遮挡起来,一定不会更优(因为可以重叠)。所以我们不妨抽象出“行泥泞块”、“列泥泞块”,这便是本题的2要素,对于任意一个泥泞点来说,只要任选一个泥泞块被覆盖即可。代码如下:
#include<iostream>
#include<cstring>
using namespace std;
const int N=55,M=2505,K=6e6+5;
char a[N][N];
struct Node{
int row,col;
}g[N][N];
bool st[M];
int head[M],to[K],ne[K],idx,match[M];
void add(int x,int y){
ne[++idx]=head[x];
to[idx]=y;
head[x]=idx;
}
bool find(int u){
for(int i=head[u];i;i=ne[i]){
int j=to[i];
if(st[j]) continue;
st[j]=true;
if(!match[j] || find(match[j])){
match[j]=u;
return true;
}
}
return false;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i]+1;
int cnt=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i][j]=='*'){
if(a[i][j-1]!='*') cnt++;
g[i][j].row=cnt;
}
}
}
int v=cnt;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(a[j][i]=='*'){
if(a[j-1][i]!='*') cnt++;
g[j][i].col=cnt;
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i][j]=='*'){
int x=g[i][j].row,y=g[i][j].col;
add(x,y);
add(y,x);
}
}
}
int ans=0;
for(int i=1;i<=v;i++){
memset(st,false,sizeof st);
if(find(i)) ans++;
}
cout<<ans<<endl;
return 0;
}
二分图最大独立集
独立集的概念:每个点两两都没有边相连的点集。
团的概念:每个点两两之间都有一条边相连的点集。
显然一个无向图的最大独立集等于其补图(补图可以理解为一个无向图的所有点加上其边的补集)的最大团。
二分图最大独立集的求法:点数-最大匹配数
由于要使得独立集最大,那么就要考虑删掉的点数最少。那么由于每一条边都只能连接不多于一个点,那么就需要删去最小覆盖。而最小覆盖数等于最大匹配数。
例题1:AcWing 378.骑士放置
这题是个裸题,直接考虑类似于骨牌放置问题的方法,将每个点染成黑白相间的,那么将一个点与它可以走到的所有点之间连1条边,显然它们颜色不同。那么我们只要求该图的最大独立集就可以了。
代码如下:
#include<iostream>
#include<cstring>
using namespace std;
const int N=105;
int n,m,T;
bool st[N][N],ban[N][N];
struct Node{
int x,y;
}match[N][N];
int dx[8]={-2,-1,1,2,2,1,-1,-2};
int dy[8]={1,2,2,1,-1,-2,-2,-1};
bool find(int x,int y){
for(int i=0;i<8;i++){
int kx=x+dx[i],ky=y+dy[i];
if(kx<1 || kx>n || ky<1 || ky>m || ban[kx][ky] || st[kx][ky]) continue;
st[kx][ky]=true;
Node t=match[kx][ky];
if(!t.x || find(t.x,t.y)){
match[kx][ky]={x,y};
return true;
}
}
return false;
}
int main(){
cin>>n>>m>>T;
for(int i=1;i<=T;i++){
int x,y;
cin>>x>>y;
ban[x][y]=true;
}
int ans=n*m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if((i+j)%2 && !ban[i][j]){
memset(st,false,sizeof st);
if(find(i,j)) ans--;
}
}
}
cout<<ans-T<<endl;//注意实际上只有n*m-T个点
return 0;
}
二分图的扩展问题
1.有向无环图(DAG)的不重复最小路径覆盖
有向无环图转化为二分图:对于一个n个节点的DAG中的任意一条有向边(x,y),将其转化为二分图上的边(x,y+n),其中y+n是右部节点。那么不重的最小路径覆盖就是n-最大匹配数。
简要证明:因为图是DAG,所以必然存在出度为0的点,即路径的终点。那么显然这样的点是不与右部任何一个节点相连的,也就是说该点是非匹配点。所以我们只要求出非匹配点的个数就相当于知道了路径的条数了。
2.DAG的可重复最小路径覆盖
这里考虑将可间接到达的节点直接相连,可避免处理交叉的问题,这样可以转化为上面的处理方法。具体操作如下:
a.对图求一个传递闭包,预处理出各个点之间的联通关系
b.将所有间接联通的点之间直接连上一条有向边
c.对得到的新图(易证仍为DAG)求其不可重复的最小路径覆盖
例题1:AcWing 379.捉迷藏
这题实际上是个裸题,考虑到在一条路径上的点可以相互看见,所以一条完整的路径上至多选择1个点,那么这个问题就变成了DAG的可重复最小路径覆盖问题。
代码如下:
#include<iostream>
#include<cstring>
using namespace std;
const int N=205,M=3e4+5;
bool d[N][N];
int match[N];
bool st[N];
int n,m;
bool find(int u){
for(int j=1;j<=n;j++){
if(!d[u][j] || st[j]) continue;
st[j]=true;
if(!match[j] || find(match[j])){
match[j]=u;
return true;
}
}
return false;
}
int main(){
cin>>n>>m;
while(m--){
int a,b;
cin>>a>>b;
d[a][b]=true;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
d[i][j]|=d[i][k]&d[k][j];
}
}
}
int ans=0;
for(int i=1;i<=n;i++){
memset(st,false,sizeof st);
if(find(i)) ans++;
}
cout<<n-ans<<endl;
return 0;
}