学习NOTE 8——二分图
好久没写图论的学习笔记了,来写一发最近做题学的二分图。
是什么
二分图就是类似于把一个图分成两半,分为左部点和右部点。听上去是“二分”,其实和二分算法没什么关系,就是一个分成两半的图。
举个例子:

就是像上图一样把一个图分成左部点和右部点两个部分。
那这是用来干什么的?
一般是在一个图上求出最大匹配数量/判断能否把点分成两个集合,且所有边都在集合之间。上面两种主要的问题一般用匈牙利算法和染色法解决。
怎么实现
上述两种算法我们分开讲。
1.匈牙利算法
要看最大匹配数量,那我们可以从一方入手,因为通常题目中都会有明确的“主动方”与“被动方”(?)。
这里我们用左边去比配右边,先从图入手:

1先和5匹配。现在还剩2,3,4和6,7,8。

2要和8匹配,但是3又不乐意了,就去和6匹配了。

结果4就没得匹配了,但是2还可以换啊。

2无奈,转头去找7。这时6,8都空出来了,4选6、3选8,完美!

代码实现:
首先建图,这个时候就要建单向边了 (双向奔赴可不好整啊)。
接下来就用dfs跑一遍就行了,但是为什么要用dfs?(因为我是大啥子) 啊不,是因为用DFS递归模拟"占位者层层让位"的过程,直到找到一个空闲的右点或所有路径都试完,实现增广路的查找。而且,用dfs比用bfs要好写的多。
(注:增广路:一条从"未匹配左点"出发,交替经过"未匹配边→匹配边→未匹配边→...",最终到达"未匹配右点"的路径。)
伪代码如下:
dfs(u, sfn): // 尝试为左点u找匹配,sfn标记本轮轮次
if dfn[u] == sfn: return false // 本轮已试过u,防死循环
dfn[u] = sfn // 标记u本轮已访问
for v in g[u]:// 遍历u能匹配的所有右点
if !rec[v] or dfs(rec[v], sfn):// v空闲 或 让v的匹配者另找
rec[v] = u// v匹配给u
return 1
return 0 // 都试过了,找不到
------------------------------------------------
main():
读入 n, m, e
建图 g[u].push_back(v)
ans = 0
for i = 1 to n:// 每个左点尝试一次
if dfs(i, i)://i作为轮次编号
ans++
输出 ans
这样,代码就很好写了!
放一下题:P3386
进行dfs,对每一个左点尝试匹配所有可连的右点,如果右点空闲则直接匹配,如果右点已被其他左点匹配,则递归尝试让那个左点另找右点,直到所有左点都尝试完毕,统计成功匹配的数量。
$\color{Cyan}Code$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10;
int n,m,e,ans,dfn[N],rec[N];//dfn:dfs序,rec:匹配的东西
vector<int> g[N];
bool dfs(int u,int sfn){
if(dfn[u]==sfn) return 0;
dfn[u]=sfn;
for(auto v:g[u])
if(!rec[v]||dfs(rec[v],sfn)) return (rec[v]=u,1);
return 0;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>e;
for(int i=1,u,v;i<=e;i++){
cin>>u>>v;
g[u].push_back(v);
}
for(int i=1;i<=n;i++) if(dfs(i,i)) ans++;
cout<<ans;
return 0;
}
2.染色
还是这个图:

只是换一下:

改成一条链的形式(刚刚的太乱了)。
现在我们把每个点染色,分成两个阵营。但是二分图染色有个要求,就是相邻两个点颜色不能一样。
就成了下面这样:

欸,怎么刚好1,2,3,4一个阵营,5,6,7,8一个阵营,你就说是不是分成两半了吧。
代码实现
这个算法的代码就更简单了!
但是上面的图不够好,画一个更好的:

染色:

1,3,4一个阵营,2单独一个阵营。
ok,上题:P10378
考虑将两个学校两个阵营,然后染色,每次找最大、小值并求和就行了。
伪代码:
dfs(u, c):// u:当前点, c:颜色(1或0)
flg[u] = 1// 标记已访问
if(c) s1++// 颜色1计数
else s2++// 颜色0计数
for v in g[u]:// 遍历所有邻居
if (!flg[v])// 未访问
dfs(v, !c)// 染相反颜色
main():
读入n, m
建图: g[u].push_back(v), g[v].push_back(u) // 双向边
for i = 1 to n:
if (!flg[i]): // 新连通块
s1 = s2 = 0
dfs(i, 1)// 从颜色1开始染
mn += min(s1, s2// B最少: 选人数少的
mx += max(s1, s2)// B最多: 选人数多的
输出 mn mx
$\color{Orange}Code$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,m,flg[N],mn,mx,s1,s2;
vector<int> g[N];
void dfs(int u,bool c){
flg[u]=1;
if(c) s1++;
else s2++;
for(auto v:g[u])
if(!flg[v]) dfs(v,!c);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1;i<=n;i++)
if(!flg[i]){
s1=s2=0;
dfs(i,1);
mn+=min(s1,s2);
mx+=max(s1,s2);
}
cout<<mn<<" "<<mx;
return 0;
}
TIP
注意:一定要分清楚题目要的是哪种方法,一定要读题,别问我怎么知道的。

浙公网安备 33010602011771号