笔记:二分图
概念
- 二分图:又称作二部图,设
是一个无向图,如果顶点集 可分割为两个互不相交的子集 ,并且图中的每条边 所关联的两个顶点 分别属于这两个顶点集 ,则称图 为一个二分图。也就是说一个图被划分成了两个不相交的集合,集合内部没有边相连。 - 匹配:在图
中,边集 被称为 的⼀一个匹配当 且仅当对于 中的每个点, 中与其关联的边不超过一条。 - 最大匹配:二分图中匹配的边数最多的匹配。但最大匹配的边数是确定的,而且不可能超过图中定点数的一半。
- 极大匹配:无法再增加匹配边的匹配。不一定是最大匹配。
- 最大权匹配:加权图中,权值和最大的匹配。
- 最大权最大匹配:匹配数最多的前提下,边权和最大的匹配。即所有最大匹配中,边权和最大的匹配。
- 完美匹配:二分图中所有的点都为匹配点的匹配。
- 交错路:始于非匹配点且由匹配边与非匹配边交错而成。
- 增广路:始于非匹配点且终于非匹配点(除了起始的点)的交错路。增广路中边的数量是奇数。
- 增广路上非匹配边比匹配边数量多
,如果将增广路上的匹配边和未匹配边反转,则匹配数量会增加 且依然是交错路。 - 根据增广路定理,当找不到增广路的时候,得到最大匹配。
- 简证一下:因为增广操作每次只会使得两端的未匹配点变成匹配点,不会出现增广后有匹配点变成未匹配点的情况,所以每个点作为未匹配点只需要增广一次。
- 增广路上非匹配边比匹配边数量多
- 二分图最小顶点覆盖:
- 定义:假如选了一个点就相当于覆盖了以它为端点的所有边。最小顶点覆盖就是选择最少的点来覆盖所有的边。
- 定理:最小顶点覆盖等于二分图的最大匹配。
- 最大独立集:
- 定义:选出一些顶点使得这些顶点两两不相邻,则这些点构成的集合称为独立集。找出一个包含顶点数最多的独立集称为最大独立集。
- 定理:最大独立集 = 所有顶点数 - 最小顶点覆盖 = 所有顶点数 - 最大匹配
二分图的判定
一般用染色法判断一个图是否为二分图。染色法的基本思想是,如果一个图是二分图,那么任何一个顶点的邻居都应该属于与该顶点不同的集合,因此可以用两种颜色对图中的顶点进行染色,以检验是否满足这个条件。
模板:P1330 封锁阳光大学 - 洛谷
裸的二分图判定,注意图不是连通的,所以每一个连通块选择的颜色可能不一样,我们应该选两种颜色中出现次数较小者。
#include <bits/stdc++.h>
using namespace std;
// 图不是连通的,所以对于每一个连通块,所选点的颜色可能不一样。
// 因此需要用一个桶来记录同一个连通块中两种颜色出现的次数,并选择其中的较小者。
const int maxn = 1e4 + 5;
const int maxm = 1e5 + 5;
int n, m;
int head[maxn], color[maxn], counter[maxn];
struct Edge {
int to, next;
} edge[maxm << 1];
inline void insertEdge(int u, int v) {
static int edgecnt;
edge[++edgecnt].to = v;
edge[edgecnt].next = head[u];
head[u] = edgecnt;
}
bool dfs(int u, int col) {
color[u] = col;
counter[color[u]]++;
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if (!color[v]) {
if (!dfs(v, 3 - col)) {
return false;
}
} else if (color[v] == color[u]) {
return false;
}
}
return true;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
insertEdge(u, v);
insertEdge(v, u);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (!color[i]) {
counter[1] = counter[2] = 0; // 对每一个联通块都要先清空。
if (!dfs(i, 1)) {
printf("Impossible");
return 0;
}
ans += min(counter[1], counter[2]);
}
}
printf("%d", ans);
return 0;
}
二分图最大匹配:匈牙利算法
原理就是寻找增广路,在上文有提到。
算法步骤:
- 初始化:选择一个未匹配的左半部顶点作为起始点,尝试与其相连的右半部
顶点进行匹配。 - 深度优先搜索:从起始点出发,沿着增广路径进行深度优先搜索,直到找到
一个未匹配的右半部顶点或者路径不通为止。 - 匹配调整:如果找到了未匹配的右半部顶点,就将起始点和这个顶点之间的
边加入到匹配中,并将路径上的所有非匹配边改为匹配边,将匹配边改为非
匹配边。 - 重复步骤:如果路径不通,回退到前一个未匹配的左半部顶点,尝试与其相
连的其他右半部顶点进行匹配,直到找到一个可行的匹配或者所有可能的匹
配都尝试完毕。 - 终止条件:当所有左半部的顶点都尝试过匹配,且没有找到新的增广路径
时,算法结束。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 505;
int n, m, e;
int graph[maxn][maxn]; // graph[u][v] 表示左边的 u 与右边的 v 是否有连边。
int match[maxn]; // match[v] = u 表示右部节点 v 被左部的 u 匹配。
bool visited[maxn]; // visited[v] 表示右部节点 v 是否被左部匹配。
bool dfs(int u) {
for (int v = 1; v <= m; v++) { // 枚举右部节点
// 寻找增广路
if (graph[u][v] && !visited[v]) {
visited[v] = true; // 标记为已匹配,接下来不会再被匹配。
// 如果 v 没有匹配对象,或 v 的匹配对象能找到新的匹配对象
// 则 v 与 u 相匹配(找到增广路)
if (!match[v] || dfs(match[v])) {
match[v] = u;
return true;
}
}
}
// 尝试了所有匹配,没有找到新的增广路。
return false;
}
int main() {
scanf("%d%d%d", &n, &m, &e);
for (int i = 1; i <= e; i++) {
int u, v;
scanf("%d%d", &u, &v);
graph[u][v] = 1;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans += dfs(i);
memset(visited, 0, sizeof(visited));
}
printf("%d", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效