学习笔记——二分图及建图技巧
简介
二分图是一种特殊的图。其定义为:
节点由两个集合组成,且两个集合内部没有边的图。
那啥时候可以用二分图呢?当且仅当图可以进行合法的黑白染色的时候,可以考虑二分图。
二分图匹配
匈牙利算法
最基础的问题就是二分图的最大匹配。所谓最大匹配,就是对于左右两个集合内的点,每个点都只能选一次,并且左右匹配的点之间有连边,求最多能左右匹配的点对数。举个最简单的例子,就是有 \(n\) 个男生和 \(m\) 个女生,之间有相互喜欢的关系,求最终最多能牵几根红线。
由于这个问题比较基础,所以具体可以看网上其它博客,这里仅作简述。
我们常用的方法是匈牙利算法,这个算法,你可以感性地将之理解为一个有礼貌地戴绿帽的过程。
算法流程
- 从 \(1\sim n\) 便利每个男生。
- 对于每个男生,先随便找一个两情相悦的女生。然后分两种情况:
\(1\)、 这个女生没有匹配,那么就匹配上。
\(2\)、 这个女生已经匹配了,那么就礼貌地问问那个匹配她的男生:“你好,我可以把你戴绿帽吗?”然后问题转化为那个可悲的被戴绿帽的男生去匹配其他女生的问题,可以递归解决。 - 然后如果可以通过一连串的戴绿帽,就可以使答案加一。
这就是匈牙利的全过程,更具体的推荐扶咕咕的题解。
Code
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
using namespace std;
const int MAXN=510;
int n,m,e,mtc[MAXN];
bool used[MAXN],edge[MAXN][MAXN];
bool find(int x){
for(int i=1;i<=m;i++){
if(used[i]||!edge[x][i]) continue;
used[i]=1;//标记在当前处理时,该女生是否访问。
if(mtc[i]==0||find(mtc[i])){
mtc[i]=x;
return 1;
}
}return 0;
}
int main()
{
scanf("%d%d%d",&n,&m,&e);
for(int i=1,u,v;i<=e;i++){
scanf("%d%d",&u,&v);
edge[u][v]=1;
}int ans=0;
memset(mtc,0,sizeof(mtc));
for(int i=1;i<=n;i++){
memset(used,0,sizeof(used));
if(find(i)) ans++;
}printf("%d\n",ans);
}
Ps:一个优化
显然,匈牙利的复杂度是 \(O(n^2)\) 的,所以有人看到百万级别的题就不会往二分图匹配上去想了。这显然不对,因为有的题目每个点连出去的边只有几条,但是点却是上万的,此时,如果在遍历的时候用 \(vector\) 或者前向星存边,那么就可以只跑必定有边的匹配,可以大大降低复杂度。例如这道题,就可以这么做:
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=1e6+10;
vector<int> e[MAXN];
int vis[MAXN],mtc[MAXN];
bool find(int x,int t){
for(int i=0;i<e[x].size();i++){
int s=e[x][i];
if(vis[s]==t) continue;
vis[s]=t;//记录时间戳
if(mtc[s]==-1||find(mtc[s],t))
{mtc[s]=x;return 1;}
}return 0;
}
int main()
{
int n,a,b,mx=0;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&a,&b);
e[a].push_back(i);
e[b].push_back(i);
mx=max(mx,max(a,b));
}
memset(mtc,-1,sizeof(mtc));
for(int i=1;i<=mx;i++){//如果 memset 就太慢了。
if(!find(i,i)){//注意这里把 vis 改成 int 类型的,这样就不用 memset 而可以用记录时间戳取代之。
printf("%d\n",i-1);
return 0;
}
}printf("%d\n",mx);
}
建图技巧
trick1:二分图矩阵模型
题目一般是给出一个矩阵,然后给出行列的限制,求最大能放几个合法的点。先看例题吧。
ZJOI矩阵游戏
题目中可以推断出一个简单的结论:每行每列必须至少有一个黑色点。
那么我们可以把行列拆开,作为左右部图,然后如果 \(i\) 行 \(j\) 列是 \(1\),那么说明第 \(i\) 行 \(j\) 列已经有一个黑色的点,那么就把 \(i\) 行向 \(j\) 列建边。最后跑一次匈牙利,就可以了。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=210;
bool used[MAXN],mp[MAXN][MAXN];
int n,mtc[MAXN];
bool find(int x){
for(int i=1;i<=n;i++){
if(mp[x][i]&&!used[i]){
used[i]=1;
if(mtc[i]==0||find(mtc[i]))
{mtc[i]=x;return 1;}
}
}return 0;
}
int main()
{
int T;
for(scanf("%d",&T);T--;){
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&mp[i][j]);
int ans=0;
memset(mtc,0,sizeof(mtc));
for(int i=1;i<=n;i++){
memset(used,0,sizeof(used));
ans+=find(i);
}
puts(ans==n?"Yes":"No");
}
}
trick2:匹配时记录路径
有的聚聚可能说了,我二分图直接用网络流跑不就好了,又快又直接。当然可以,不过如果要记录匹配路径的话,就会麻烦了,因为网络流的常用算法 \(dinic\) 是利用反向边回流实现的。
上例题
这题很显然是要输出路径的,只要在跑匈牙利的时候,成功匹配就记录一下就可以了。这题的建图应该是裸的吧?
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=1e4+10;
vector<int> e[MAXN];
int mtc[MAXN],vis[MAXN],pp[MAXN];
bool find(int x){
for(int i=0;i<e[x].size();i++){
int s=e[x][i];
if(vis[s]) continue;
vis[s]=1;
if(mtc[s]==-1||find(mtc[s]))
{mtc[s]=x;pp[x]=s;return 1;}
}return 0;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=0,d;i<n;i++){
scanf("%d",&d);
int t1=(i+d+n)%n,t2=(i-d+n)%n;
if(t1>t2) swap(t1,t2);
e[i].push_back(t1);
e[i].push_back(t2);
}
memset(mtc,-1,sizeof(mtc));
int ans=0;
for(int i=n-1;i>=0;i--){//具体题目要求,该题需要最小的字典序
memset(vis,0,sizeof(vis));
ans+=find(i);
}
if(ans<n){
puts("No Answer");
return 0;
}
printf("%d",pp[0]);
for(int i=1;i<n;i++)
printf(" %d",pp[i]);
}
最大独立集
二分图最大独立集,就是在整个二分图中选出最多的点,使得选出的点中没有连边。
解决方式
二分图最大独立集 = 二分图总点数 - 二分图最大匹配数
感性理解就是,对于每个匹配,两边只能选一个点,因为已经是最大匹配,所以我对于有匹配的只选一边,这样可以保证其他点是独立的,否则与最大匹配相违背。
建图技巧
trick1:黑白染色建图
比如这题,显然不是 2-sat 问题,所以考虑最大独立集。
但是题目并没有说明先输入的是男生还是女生,所以要先建图,然后跑大法师先染色。然后再跑最大独立集。
但是如果是矩阵中非行列限制,而是一些奇怪的走位,比如这题,同样我们也要用最大独立集来解决,此时就不用跑染色,直接复制一份,并在最后求答案时,把匹配数除以二就可以了。
下面贴上面两题的代码:
//P6268
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=1010;
int mtc[MAXN];//1 boy|2 girl
bool mp[MAXN][MAXN],vis[MAXN];
vector<int> boy,girl,e[MAXN];
bool find(int x){
for(int i=0;i<girl.size();i++){
int s=girl[i];
if(mp[x][s]&&!vis[s]){
vis[s]=1;
if(mtc[s]==0||find(mtc[s])){
mtc[s]=x;return 1;
}
}
}return 0;
}
void dfs(int x,bool fl){
vis[x]=1;
if(fl) girl.push_back(x);
else boy.push_back(x);
for(int i=0;i<e[x].size();i++){
int s=e[x][i];
if(vis[s]) continue;
dfs(s,!fl);
}
}
int main()
{
int n,m,a,b;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
a++;b++;
e[a].push_back(b);
e[b].push_back(a);
mp[a][b]=mp[b][a]=1;
}
for(int i=1;i<=n;i++)
if(!vis[i]) dfs(i,0);
memset(mtc,0,sizeof(mtc));
int ans=0;
for(int i=0;i<boy.size();i++){
memset(vis,0,sizeof(vis));
if(find(boy[i])){
ans++;
}
}
printf("%d\n",n-ans);
}
//P4304
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define inf 1<<30
#define INF 1ll<<60
using namespace std;
const int MAXN=4e4+10;
vector<int> e[MAXN];
int vis[MAXN],mtc[MAXN];
bool find(int x,int t){
for(int i=0;i<e[x].size();i++){
int s=e[x][i];
if(vis[s]==t) continue;
vis[s]=t;
if(mtc[s]==-1||find(mtc[s],t))
{mtc[s]=x;return 1;}
}return 0;
}
int dx[]={-1,-2,1,2,-1,-2,1,2},
dy[]={-2,-1,-2,-1,2,1,2,1};
int mp[210][210],n;
int X(int x,int y){return (x-1)*n+y;}
int main()
{
char ch;
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
scanf(" %c",&ch);
mp[i][j]=ch-'0';
}
}
int sum=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(!mp[i][j]){
sum++;
for(int k=0;k<8;k++){
int ii=i+dx[k],jj=j+dy[k];
if(ii>=1&&ii<=n&&jj>=1&&jj<=n&&!mp[ii][jj])
// cerr<<X(i,j)<<' '<<X(ii,jj)<<'\n',
e[X(i,j)].push_back(X(ii,jj));
}
}
int ans=0;
memset(mtc,-1,sizeof(mtc));
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(!mp[i][j])
ans+=find(X(i,j),X(i,j));
printf("%d\n",sum-ans/2);
}