算法 | 二分图
二分图又称作二部图,是图论中的一种特殊模型。 设 $G=(V,E)$ 是一个无向图,如果顶点 $V$ 可分割为两个互不相交的子集 $(A,B)$,并且图中的每条边 $(i,j)$ 所关联的两个顶点 $i$ 和 $j$ 分别属于这两个不同的顶点集 $(i\in A,j\in B)$,则称图 $G$ 为一个二分图。
染色法判别二分图
如下图,用红和绿(实际用 0 和 1 表示)对这个图染色,相邻的顶点不可染相同颜色,那么就能得到以下情况:
如果能将一个图无矛盾地染色,那么这就是一个二分图,将同色顶点放到一起,就将这个图一分为二,这便是染色法判别二分图的思想。
由此也能得到二分图的一个性质:不存在奇环。如果存在奇环,那么染色时一定会出现矛盾,不是二分图。(无环也是二分图)
复杂度
时间复杂度:$O(\left|V\right|+\left|E\right|)$
$\left|E\right|$ 为边数,$\left|V\right|$ 为顶点数。
代码实现
染色过程即对图进行了一次遍历,可用 DFS 实现,每层 DFS 染不同颜色即可。
连通图
const int SIZE = 1e5 + 10;
int V, E; // V为顶点,E为边
vector<int> edge[SIZE]; // vector邻接表,可改为数组邻接表效率更高
int color[SIZE]; // 储存颜色,用0和1表示两种颜色,-1表示还未染色。重要:使用前先memset为-1
/* vector<int> ans[2]; */ // 储存两颜色的顶点,某些题目需要输出
bool dfs(int n, int c)
{
color[n] = c;
/* ans[colr].push_back(node); */ // 储存两颜色的顶点,某些题目需要输出
for (auto nxt : edge[n])
{
if (color[n] == -1)
{
if (!dfs(nxt, !c))
return false;
}
else if (color[nxt] == c)
{
return false;
}
}
return true;
}
非连通图
若图并不联通,那么一次查找还不够。因为一个图如果是二分图,需要每个连通分支都是二分图,整个图才是二分图。
因此需要从点 $1$ 遍历到点 $V$,如果一个点还未染色,说明它没有联通,需要从这个点再染一边色。因此需要额外的以下代码在主函数内。
bool flag = true;
for (int i = 1; i <= V; i++)
{
if (color[i] == -1)
{
if (!dfs(i, 0))
{
flag = false;
break;
}
}
}
求二分图最大匹配
定义
二分图的匹配:给定一个二分图 $G$,在 $G$ 的一个子图 $M$ 中,$M$ 的边集 $\{E\}$ 中的任意两条边都不依附于同一个顶点,则称 $M$ 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
解释这个最大匹配,经常用男女配对来打比方(笑
如下图,易得这是一个二分图。可以把左边一列看为男生,右边一列看为女生,我们要求能够匹配上的男生女生的最大数目。当然,“海王”行为是不可取的,一个男生只能和一个女生匹配,反正同理。
匈牙利算法 (Hungarian Algorithm)
复杂度
时间复杂度:$O(\left|V\right|\cdot \left|E\right|)$
$\left|E\right|$ 为边数,$\left|V\right|$ 为顶点数。一般情况的时间复杂度会远小于该理论值。
算法过程
就以上图为例,编一个号方便讲解。左侧的子图四个顶点记为 $B_1,B_2,B_3,B_4$,右侧的子图四个顶点记为 $G_1,G_2,G_3,G_4$。
尝试给 $B_1$ 匹配女生:
$B_1$ 对 $G_1,G_3$ 有好感。
尝试与 $G_1$ 匹配,此时 $G_1$ 还单身,因此顺利匹配成功。
尝试给 $B_2$ 匹配女生:
$B_2$ 对 $G_1$ 有好感。
尝试与 $G_1$ 匹配,此时 $G_1$ 已经匹配了 $B_1$,尝试让 $B_1$ 更换匹配的女生。
$B_1$ 可更换为与 $G_3$ 匹配,然后 $B_2$ 就能成功与 $G_1$ 匹配。
尝试给 $B_3$ 匹配女生:
$B_3$ 对 $G_1,G_2$ 有好感。
尝试与 $G_1$ 匹配,此时 $G_1$ 已经匹配了 $B_2$,尝试让 $B_2$ 更换匹配的女生。
$B_2$ 没有可更换匹配的女生,因此 $B_2$ 无法与 $G_1$ 匹配。
尝试与 $G_2$ 匹配,此时 $G_2$ 还单身,因此顺利匹配成功。
尝试给 $B_4$ 匹配女生:
$B_4$ 对 $G_3,G_4$ 有好感。
尝试与 $G_3$ 匹配,此时 $G_3$ 已经匹配了 $B_1$,尝试让 $B_1$ 更换匹配的女生。
$B_1$ 没有可更换匹配的女生,因此 $B_4$ 无法与 $G_3$ 匹配。
尝试与 $G_4$ 匹配,此时 $G_4$ 还单身,因此顺利匹配成功。
匹配完成,最大匹配数:4
代码实现
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 1010;
int n1, n2, m; // n1为二分图的一个子图的顶点数,n2为另一个子图的顶点数,m为边数。
vector<int> edge[SIZE]; // 这里使用vector存图
int match[SIZE]; // 储存n2中的某个顶点匹配的n1中的顶点
bool vis[SIZE]; // 标记n2中的某个顶点是否已经匹配过
bool find(int x)
{
// 遍历所有与x连接的n2内的顶点i
for (auto i : edge[x])
{
// 如果顶点i本轮还未匹配过
if (!vis[i])
{
// 将其标记
vis[i] = true;
// 若顶点i还没有匹配到任何n1中顶点,则直接把i与x匹配
// 如果i已经匹配上,则查询与i匹配的n1中的元素能否换一个匹配,若可以,则将i与x匹配
if (match[i] == 0 || find(match[i]))
{
match[i] = x;
return true;
}
}
}
// 如果没法匹配上,返回false
return false;
}
int main(void)
{
cin >> n1 >> n2 >> m;
for (int i = 0; i < m; i++)
{
int u, v;
cin >> u >> v;
// 只用得到从n1到n2的边,因此只存了单向边
edge[u].push_back(v);
}
int cnt = 0; // 匹配的数量
// 从n1第一个元素开始,尝试匹配到最后一个元素
for (int i = 1; i <= n1; i++)
{
// 先清空所有n2的访问情况
memset(vis, false, sizeof(vis));
// 如果匹配上则匹配数+1
if (find(i))
cnt++;
}
cout << cnt << endl;
return 0;
}
本文采用 CC BY-SA 4.0 许可,本文 Markdown 源码:Haotian-BiJi