二分图的特点
什么题用二分图??
当题目中的元素可以分为"0/1" 两个部分时,可以尝试用二分图匹配解决问题。
二分图公式
- 最小路径覆盖= n - 最大匹配
- 最大独立集 = n - 最大匹配
嗯就是这样。
模板:二分图最大匹配
匈牙利算法:
核心思想:反复横跳,找增广路(可以借鉴网络流)
using namespace std;
const int maxn=1005,maxm=5e4 + 5;
struct edge{
int to,nxt;
}e[maxm];
int head[maxn],cnt=0,ans=0;
void link(int u,int v){
e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;
}
int n,m,E,match[maxn],vis[maxn];
bool dfs(int u){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!vis[v]){
vis[v]=true;
if(match[v]==0||dfs(match[v])){
match[v]=u;
return true;
}
}
}
return false;
}
int main(){
scanf("%d%d%d",&n,&m,&E);
for(int i=1;i<=E;i++){
int u,v;
scanf("%d%d",&u,&v);
link(u,v);
}
for(int i=1;i<=n;i++){
memset(vis,0,sizeof vis);
if(dfs(i))ans++;
}
printf("%d",ans);
return 0;
}
做题时的几个细节 (update on 2.15th)
- 在不确定或不方便区分左侧点右侧点时,为了方便起见,可以连无向边最后最大匹配除以二就是原答案。 (详见骑士共存问题)
- 确定区分左右侧点时,那么肯定是有向边,跑最大匹配时多从右侧点出发也没有影响,适用于不清楚左侧点坐标的时候,任意图跑最大匹配都可以从两边都出发,对结果没有影响。 详见:
P7338 『MdOI R4』Color - 有时不必每次memset,可以给 vis 数组打上时间戳以免被卡常。
例题 1. P3355 骑士共存问题
传送门
题目大意:
给你一个带有障碍物的棋盘,问最多可以放多少马导致他们不会相互攻击。
分析:
马是走日的,如果你国际象棋打得多的话你会发现红色棋盘上的马只能吃走到红色棋盘上,黄色同上。
因此“0/1” 元素找到了,把红色放左,黄色点放右,最后减去障碍物的个数就可。
然而我这个为了省事,找到一个点就连起双向边,最后把最大匹配除以二就可。
using namespace std;
inline int read()
{
char ch=getchar();
int s=0,w=1;
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=s*10+ch-'0';ch=getchar();}
return s*w;
}
const int maxn=505;
int num[maxn][maxn],head[maxn*maxn];
struct edge{
int to,nxt;
}e[maxn*maxn*4];
int cnt=1,Cnt=0;
void link(int u,int v){
e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;
}
int dx[9]={1,1,2,2,-1,-1,-2,-2};
int dy[9]={2,-2,1,-1,2,-2,1,-1};
int n,m,ans;
bool vis[maxn*maxn],ban[maxn][maxn];
int match[maxn*maxn];
bool dfs(int x){
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(!vis[v]){
vis[v]=true;
if(!match[v]||dfs(match[v])){
match[v]=x;
return true;
}
}
}
return false;
}
void Link(int x,int y){
for(int i=0;i<8;i++){
int xx=x+dx[i],yy=y+dy[i];
if(xx>0&&xx<=n&&yy>0&&yy<=n&&!ban[xx][yy]){
link(num[x][y],num[xx][yy]);
link(num[xx][yy],num[x][y]);
}
}
}
int main(){
//freopen("1.in","r",stdin);
n=read();m=read();
for(int i=1;i<=m;i++){
int x=read(),y=read();
ban[x][y]=true;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)num[i][j]=++Cnt;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(!ban[i][j])Link(i,j);
}
}
for(int i=1;i<=Cnt;i++){
memset(vis,0,sizeof vis);
if(dfs(i))ans++;
}
printf("%lld",(long long)(n*n)-m-(ans/2));
}
例题 2. P2763 试题库问题
直接说解法:
把要用的题型和数量拆开放在左边,把已有的放在右边,跑最大匹配即可。
注意拆点时的细节问题。
//Shiyan Wang P2763
using namespace std;
const int maxn=1005,maxk=25;
struct edge{
int to,nxt;
}e[maxn*maxn];
int head[maxn],cnt = 0;
void link(int u,int v){
e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;
}
int n,k,p,Cnt1,Cnt2,ai;
int need[maxk],match[maxn],tag[maxn*maxn];//tag用于拆点,每个左侧点是由第几个题目类型拆出来的
bool vis[maxn*maxn];
vector<int> ans[maxk];
vector<int> v[maxk];
bool dfs(int x){
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
if(!vis[y]){
vis[y]=1;
if(match[y]<0||dfs(match[y])){
match[y]=x;
return true;
}
}
}
return false;
}
int main(){
//左半边点
scanf("%d%d",&k,&n);
for(int i=1;i<=k;i++){
scanf("%d",need+i);
Cnt1+=need[i];//左侧点的个数 ->总共需要多少道题
}//统计右侧点
for(int i=1;i<=n;i++){
scanf("%d",&p);
for(int j=1;j<=p;j++){
scanf("%d",&ai);
v[ai].push_back(i);//类型->所给题号
}
}
//拆左侧点并进行连接
for(int i=1;i<=k;i++)
for(int j=1;j<=need[i];j++){
tag[++Cnt2]=i;//编号->类型
for(int K=0;K<v[i].size();K++){
link(Cnt2,v[i][K]);
}
}
//匹配
memset(match,-1,sizeof match);
int Ans = 0;
for(int i=1;i<=Cnt1;i++){
memset(vis,0,sizeof vis);
if(dfs(i))Ans++;
}
if(Ans<Cnt1){puts("No Solution!");return 0;}
for(int i=1;i<=n;i++){
ans[tag[match[i]]].push_back(i);
}
for(int i=1;i<=k;i++){
printf("%d:",i);
for(int j=0;j<ans[i].size();j++)printf(" %d",ans[i][j]);
puts("");
}
return 0;
}