浅谈二分图
本文是wiki与这篇题解的整合
定义
二分图,又称二部图,英文名叫 Bipartite graph。
二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,将节点划分成满足以上性质的两个集合。
性质
- 如果两个集合中的点分别染成黑色和白色,可以发现二分图中的每一条边都一定是连接一个黑色点和一个白色点。
- 二分图不存在长度为奇数的环,因为每条边的 \(u\) 在一个集合, \(v\) 在另一个集合,所以想要从一个点出发,再回到这个点只可能经过偶数条边。
应用
二分图最大匹配
我会匈牙利!!!
假设有 \(n\) 个顶点,\(m\) 条边。
因为增广路长度为奇数,路径起始点非左即右,所以我们先考虑从左边的未匹配点找增广路。 注意到因为交错路的关系,增广路上的第奇数条边都是非匹配边,第偶数条边都是匹配边,于是左到右都是非匹配边,右到左都是匹配边。 于是我们给二分图 定向,问题转换成,有向图中从给定起点找一条简单路径走到某个未匹配点,此问题等价给定起始点 \(s\) 能否走到终点 $t $ 。 那么只要从起始点开始 DFS 遍历直到找到某个未匹配点,\(O(m)\)。未找到增广路时,我们拓展的路也称为 交错树。
代码
#include <bits/stdc++.h>
using namespace std;
int n,m,t,book[505],match[505];
vector <int> e[505];
int dfs(int x,int tag)
{
if(book[x]==tag)return 0;
book[x]=tag;
for(auto v : e[x])
{
if((match[v]==0)||dfs(match[v],tag))//这个人还没有选或者让与它连边的点换一个点连边
{
match[v]=x;
return 1;//匹配成功
}
}
return 0;
}
int main()
{
scanf("%d%d%d",&n,&m,&t);
for(int i=1;i<=t;i++)
{
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
}
int sum=0;
for(int i=1;i<=n;i++)
{
if(dfs(i,i))
{
sum++;
}
}
cout<<sum;
return 0;
}
我会网络流!!!
将源点向左边所有点连边,再将右边的点连向汇点,最大流就是二分图最大匹配。
跑网络流用的是dinic的话,时间复杂度是:\(O(\sqrt{n}m)\) 的。
二分图最小点覆盖(König 定理)
最小点覆盖:选最少的点,满足每条边至少有一个端点被选。
二分图中,最小点覆盖 \(=\) 最大匹配。
二分图最大独立集
最大独立集:选最多的点,满足两两之间没有边相连。
因为在最小点覆盖中,任意一条边都被至少选了一个顶点,所以对于其点集的补集,任意一条边都被至多选了一个顶点,所以不存在边连接两个点集中的点,且该点集最大。因此二分图中,最大独立集 \(=\) \(n-\)最小点覆盖。
二分图最大权完美匹配
二分图最大权完美匹配:还是二分图匹配,只不过加上了个边权。。。
做法
考虑仍然用二分图最大匹配的思路解决这个问题。。。
比如我们以一个员工和工作的匹配。。。
-
首先,将左边的顶点赋值为最大权重,右边赋值为 \(0\)
-
进行匹配
原则:如果能找到 左边顶点+右边顶点=边权,直接匹配;若找不到匹配,对路径中所有做顶点减一,右顶点加一。
-
代码(P1559)
#include <bits/stdc++.h> using namespace std; #define int long long const int N=22; int n; struct edge { int v,nxt; }e[N*N]; int head[N],et=0; inline void add(int u,int v) { et++; e[et].v=v,e[et].nxt=head[u]; head[u]=et; } int vl[N],vr[N],in[N];//vl:标记经过的左边点,vr:标记经过的右边点,in:匹配数组 int l[N],r[N];//l:左边每个数的值,r:右边每个数的值 int c[N][N],en[N][N]; int minn; inline int dfs(int u) { if(vl[u])return 0; vl[u]=1; for(int i=head[u];i;i=e[i].nxt) { int v=e[i].v; if(vr[v])continue; if(l[u]+r[v]==c[u][v])//如果左边+右边=边权,成功!!! { vr[v]=1; if(!in[v]||dfs(in[v])) { in[v]=u; return 1; } } else { minn=min(minn,l[u]+r[v]-c[u][v]);//给匹配失败作准备,l[u]+r[v]-c[u][v] 这个可以优化,每次加一或减一,想一想就会了。。。qwq } } return 0; } inline void KM() { for(int i=1;i<=n;i++) { r[i]=0,l[i]=-1e18; for(int j=1;j<=n;j++) { l[i]=max(l[i],c[i][j]); } } //-----------------^ 这是第一步 for(int i=1;i<=n;i++) { while(1) { memset(vl,0,sizeof vl); memset(vr,0,sizeof vr); minn=1e18;//要记得清空 if(dfs(i))break//匹配成功,直接跳出 for(int j=1;j<=n;j++) { if(vl[j])l[j]-=minn; if(vr[j])r[j]+=minn; }//修改 } } //------------^ 这是第二步 } signed main() { scanf("%d",&n); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { cin>>c[i][j]; } } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { cin>>en[i][j]; } } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { c[i][j]=c[i][j]*en[j][i];//算边权--->题目要求 add(i,j);//建边 } } KM(); int ans=0; for(int i=1;i<=n;i++) { ans+=c[in[i]][i];//直接加上边权 } cout<<ans; return 0; }