二分图知识点杂记
这篇文章记录了我最近学到的二分图知识点,主要是帮助我自己复习。大家也可以参考一下。
二分图简介
不要紧,看不懂标准的数学语言,可以概括一下。
其实,二分图就是一张图。如果我们把它的顶点分为两类,那么满足同类顶点之间没有边相连。
二分图有很多作用,这里刚学也体现不出来,后面我会补充的!
二分图的判定
判断一个图是否为二分图通常使用染色法。
染色法的原理是:如果一张图中存在奇环,那么此图一定不是二分图。因为只要出现奇环,就会发现有一条边连接了同一个集合中的两个顶点,是不符合二分图的定义的。因此,我们可以借这个原理,来判断一张图是否为二分图。
我这里来介绍一下它的算法流程:
首先,准备一个颜色数组记录每个顶点的颜色,初始化颜色为 \(0\)。
- 找到一个没有被染色的顶点,尝试将其染色。
- 不断找与它有边相连的顶点,若未染色,就将其染为与当前顶点相反的颜色。
- 如果相邻顶点已经染色,那么判断:若相邻顶点与当前顶点的颜色相同,则此图不是二分图,退出循环。
- 否则不断重复以上操作,直到每个顶点都被染色。
这就是二分图的判定方法,通常用 DFS 或 BFS 算法实现。这里,我给出了 DFS 算法的程序:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int color[1010];
vector<int> g[1010];
bool dfs(int u, int c)//u:当前顶点;c:当前颜色
{
color[u] = c;//标记颜色
for(int i = 0;i < g[u].size();i ++)
{
int v = g[u][i];//目标顶点
if(!color[v])//没染过色
if(dfs(v, 3 - c))//如果出现冲突
return 1;
if(color[v] == c)//出现冲突
return 1;
}
return 0;
}
int main()
{
ios::sync_with_stdio(false);
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);
}
bool fg = 0;
for(int i = 1;i <= n;i ++)
{
if(!color[i])//没染过色
if(dfs(i, 1))//如果出现了冲突
{
fg = 1;
break;
}
}
if(fg)
cout << "No" << endl;
else
cout << "Yes" << endl;
return 0;
}
【模版】二分图最大匹配
简化题意:设一个图是二分图。若在它的一个子图中,任意两条边都没有公共顶点,那么称此子图是二分图的一组匹配。在二分图中,包含边数最多的一组匹配称为二分图的最大匹配。
1、匈牙利算法
首先,我们设已经匹配的边叫做匹配边,没有匹配的边叫做非匹配边;已经和另一个节点匹配过的节点叫做匹配点;没有匹配过的节点叫做未匹配点。
然后,我们引入两个概念:
- 交替路:从一个未匹配点出发,依次走过非匹配边、匹配边、非匹配边……形成的路径,叫做交替路。
- 增广路:从一个未匹配点出发,走交替路。若能到达另外一个未匹配点,那么这条交替路称为增广路。
观察增广路,我们会发现:对于一条增广路,非匹配边一定比匹配边多一条。
而如果我们把非匹配边与匹配边交换,匹配边也会比原来多一条。那么,增广路就可以增加一组匹配。
匈牙利算法的核心思想就是不断的找图中的增广路,直到找不到增广路时,此时就能达到最大匹配。
这里,我给出了 DFS 的参考代码:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, e, ans = 0;
vector<int> g[510];
int match[510];
bool vis[510];
bool dfs(int u)
{
for(int i = 0;i < g[u].size();i ++)
{
int v = g[u][i];
if(vis[v])// 已经到过了
continue;
vis[v] = 1;
if(!match[v] || dfs(match[v]))// 如果节点 v 还未匹配,或者可以把这个匹配通过调整让给节点 u
{
match[v] = u;// 标注 v 节点与 u 节点匹配
return true;// 返回:可以增加一组匹配
}
}
return false;// 不可以再增加匹配
}
int main()
{
scanf("%d %d %d", &n, &m, &e);
for(int i = 0, u, v;i < e;i ++)
{
scanf("%d %d", &u, &v);
g[u].push_back(v);
}
for(int i = 1;i <= n;i ++)
{
memset(vis, 0, sizeof(vis));
if(dfs(i))// 如果可以匹配完成,就增加答案
ans ++;
}
printf("%d\n", ans);
return 0;
}
上面的代码的主要思想就是:
首先,\(\text{dfs}(u)\)(\(1 \leq u \leq n\))代表为左部节点 \(u\) 匹配节点的过程。
我们枚举节点 \(u\) 所有的可匹配对象,设第 \(i\) 个可匹配对象为 \(v_i\)。
那么有这些情况:
- \(vis_i = 1\),说明已经尝试过匹配节点 \(v_i\) 了,继续尝试下一个可匹配节点。
- \(vis_i = 0\),尝试匹配节点 \(v_i\)。
- \(match_{v_i} = 0\),说明还没有别的节点与 \(v_i\) 尝试匹配过。那么使 \(match_{v_i} = u\),返回找到匹配方式。
- \(match_{v_i} \neq 0\),说明已经有别的节点与 \(v_i\) 尝试匹配过。
- \(dfs(match_{v_i}) = 1\),说明可以调整匹配方案将节点 \(v_i\) 空出来给节点 \(u\) 匹配。那么使 \(match_{v_i} = u\),返回找到匹配方式。
- \(dfs(match_{v_i}) = 0\),说明不可以调整匹配方案将节点 \(v_i\) 空出来给节点 \(u\) 匹配。那么尝试下一个可匹配节点。
- 如果经过上面的循环没有任何匹配方式,返回没有匹配方式。
最后,如果 \(\text{dfs}(u) = 1\)(\(1 \leq u \leq n\)),说明可以再增加一组匹配,则答案数量增加。
最后,输出答案数量,即为二分图的最大匹配数量。
二分图实际运用例题
在二分图的实际运用中,发现二分图算法与构图(目前对于我来说)是比较重要与困难的步骤。因此,希望通过记录的方式来巩固。
P1894 [USACO4.2] 完美的牛栏The Perfect Stall
点击查看思路提示
这题构图很简单,主要就是二分图最大匹配的模版套用,适合入门。
题解:点这里
P1640 [SCOI2010] 连续攻击游戏
点击查看思路提示
这题主要考查如何构图,与一点优化。作为入门题还是比较适合的。
题解:点这里
浙公网安备 33010602011771号