鲜花:其实一直想改一下笔记的形式,以一个算法专题作为一篇博文的内容。这个系列到 100 期就完结吧。
二分图最大独立集
选择最多的点,使得这个点集中的点互相没有连边。
答案显然为 \(n-最小点覆盖=n-最大匹配\)(\(n\) 为总点数)。但是好像最小点覆盖那一期忘记写了,所以解释一下为什么最小点覆盖和最大匹配相等。
首先最小点覆盖就是求一个最小点集能够覆盖所有的边。感性理解一下即可发现,对于无环二分图,我们可以不停地选择一个最大匹配的端点去覆盖从它的端点出发的非最大匹配边,当然最后一条最大匹配边可能需要单独处理;对于偶环,同上,只是无需单独处理。而二分图是没有奇环的,因此我们选择的点即为最大匹配的个数,容易证明这也是最小的(没有浪费)。
P3355
「互不攻击」是一个非常明显的二分图标志。
容易发现骑士总是从一个颜色的格子跳到另一个颜色的格子。考虑将黄色作为左部点,红色作为右部点(反之亦可)。需要注意建图时应排除障碍。
将格子看作点、攻击关系看作边,此题要求放最多的骑士且互不攻击,相当于选最多的点且点之间没有连边,于是求最大独立集即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=2e2+5;
const int dx[]={2,1,2,-1,1,-2,-1,-2};
const int dy[]={1,2,-1,2,-2,1,-2,-1};
int n,m,tot,tot2;
int idx,ans,match[N*N];
int vis[N*N],p[N][N];
bool yes[N][N];
vector<int> G[N*N];
bool hungary(int cur){
if(vis[cur]==idx){
return 0;
}
vis[cur]=idx;
for(int i:G[cur]){
if(!match[i]||hungary(match[i])){
match[i]=cur;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1,x,y;i<=m;i++)
cin>>x>>y,yes[x][y]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
//if(!yes[i][j]){
if((i+j)%2==1)
p[i][j]=++tot;
else
p[i][j]=++tot2;
//}
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if((i+j)%2==0&&!yes[i][j])
for(int k=0;k<8;k++)
if(i+dx[k]>=1&&i+dx[k]<=n&&j+dy[k]>=1&&j+dy[k]<=n&&!yes[i+dx[k]][j+dy[k]])
G[p[i][j]].push_back(p[i+dx[k]][j+dy[k]]);
for(int i=1;i<=tot2;i++){
idx++;
if(hungary(i))
ans++;
}
cout<<n*n-m-ans;
return 0;
}
P5030
观察可得,长脖子鹿总是奇偶交替地跳(指行),于是将偶数行作为左部点,奇数行作为右部点即可。
然后与上题一模一样了。
这两个题注意方向数组的顺序,应当为右下、左下、右上、左上,调换顺序不会影响正确性,但是先访问没有去过的地方效率会高很多(上面的点被之前匹配的可能性更高)。
P6268
首先肯定男当左部点、女当右部点。
但这题并没有直接告诉你每个学生的性别,于是我们根据题目所给关系染色并重新建图即可。
实际上只需要给出关系的人参与建图即可,其他人直接无脑计入。
code
#include<bits/stdc++.h>
using namespace std;
const int N=3e3+5;
int n,m,tot,tot2;
int idx,ans,match[N];
int vis[N],color[N];
bool yes[N];
vector<int> G[N],T[N];
pair<int,int> e[N];
void fill_it(int cur,int c){
color[cur]=c;
for(int i:G[cur])
if(!color[i])
fill_it(i,3-c);
}
bool hungary(int cur){
if(vis[cur]==idx){
return 0;
}
vis[cur]=idx;
for(int i:T[cur]){
if(!match[i]||hungary(match[i])){
match[i]=cur;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
u++,v++;
G[u].push_back(v);
G[v].push_back(u);
yes[u]=yes[v]=1;
e[i]={u,v};
}
for(int i=1;i<=n;i++)
if(yes[i]&&!color[i])
fill_it(i,1);
for(int i=1;i<=n;i++){
if(yes[i]){
for(int j:G[i]){
if(yes[j]){
if(color[i]==1)
T[i].push_back(j);
else
T[j].push_back(i);
}
}
}
}
for(int i=1;i<=n;i++){
if(yes[i]&&color[i]==1){
idx++;
if(hungary(i))
ans++;
}
}
cout<<n-ans;
return 0;
}
UVA12083
和上题一样的套路题,不讲。
code
#include<bits/stdc++.h>
using namespace std;
const int N=5e2+5;
int t,n,idx,ans;
int vis[N],match[N],color[N];
struct STU{
int h;
char se;
string m,sp;
}stu[N];
vector<int> G[N],T[N];
void fillit(int cur,int c){
color[cur]=c;
for(int i:G[cur])
if(!color[i])
fillit(i,3-c);
}
bool hungary(int cur){
if(vis[cur]==idx){
return 0;
}
vis[cur]=idx;
for(int i:T[cur]){
if(!match[i]||hungary(match[i])){
match[i]=cur;
return 1;
}
}
return 0;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++){
G[i].clear(),T[i].clear();
cin>>stu[i].h>>stu[i].se>>stu[i].m>>stu[i].sp;
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(abs(stu[i].h-stu[j].h)<=40&&stu[i].se!=stu[j].se&&stu[i].m==stu[j].m&&stu[i].sp!=stu[j].sp){
G[i].push_back(j);
G[j].push_back(i);
}
}
}
memset(color,0,sizeof color);
for(int i=1;i<=n;i++)
if(!color[i])
fillit(i,1);
for(int i=1;i<=n;i++){
for(int j:G[i]){
if(color[i]==1)
T[i].push_back(j);
else
T[j].push_back(i);
}
}
ans=idx=0;
memset(vis,0,sizeof vis);
memset(match,0,sizeof match);
for(int i=1;i<=n;i++){
if(color[i]==1){
idx++;
if(hungary(i))
ans++;
}
}
cout<<n-ans<<'\n';
}
return 0;
}
总结:
-
如何识别二分图模型?最大匹配:最多、不共点;最小点覆盖:至少二选一;最大独立集:最多、不连边。
-
做二分图的题一般步骤:观察题目标志、确定左 / 右部点、确定边、识别模型、开做。
-
左 / 右部点无法直接确定时,考虑染色。
-
仔细观察图或者自己画图,发现性质。