二分图 学习笔记
二分图学习笔记
前言
注:经过ai修改版
什么是二分图?二分图,又称二部图,是一类结构特殊的图。它的顶点集可以划分为两个互不相交的子集(记为U和V),使得图中的每条边都连接这两个集合之间的一对点,而不会连接同一集合内部的点。简单讲就是一个图的节点被分为两个集合,每个集合内的节点之间没有边,仅不同集合间有边的图。
二分图具有诸多特殊性质,本篇笔记着重讲述二分图的核心知识点:染色法判定二分图、二分图最大匹配(匈牙利算法)、二分图最小点覆盖、二分图最大独立集,搭配例题与代码,便于理解和应用。

常见的二分图例子:树(任意树都是二分图,无环自然无奇环)、偶环(环中节点数为偶数)、网格图(如棋盘)等。
一、染色法判定二分图
核心原理
染色法的核心思想的是用两种颜色(如1和2)对所有节点进行染色,通过DFS或BFS遍历图,遵循“相邻节点颜色不同”的规则:
-
从一个未染色的节点开始,将其染为颜色1;
-
遍历该节点的所有相邻节点,将其染为颜色2;
-
递归/迭代处理相邻节点的相邻节点,交替染为1和2;
-
若遍历过程中,发现某个相邻节点已染色,且颜色与当前节点颜色相同,则说明图中存在奇数环(环中节点数为奇数);
-
结论:存在奇环的图不是二分图,反之,无奇环的图是二分图。
参考代码(DFS版)
// col数组:0=未染色,1和2为两种颜色
int col[N];
vector<int> g[N]; // 邻接表存图
inline bool dfs(int u, int c) {
col[u] = c; // 当前节点染为颜色c
for (int j : g[u]) { // 遍历所有相邻节点
if (col[j] == c) { // 相邻节点颜色相同,不是二分图
return false;
}
// 相邻节点未染色,递归染色,颜色取反(1<->2)
else if (col[j] == 0 && !dfs(j, c ^ 2)) {
return false;
}
}
return true;
}
// 主函数中调用(遍历所有节点,处理非连通图)
bool is_bipartite = true;
for (int i = 1; i <= n; i++) {
if (col[i] == 0) { // 未染色节点
if (!dfs(i, 1)) {
is_bipartite = false;
break;
}
}
}
例题:P1525 [NOIP 2010 提高组] 关押罪犯
思路分析
题目核心:将罪犯分到两个监狱,使同一监狱中罪犯的最大怨气值最小。采用「二分答案 + 染色法判定二分图」的思路:
-
二分答案:二分最小的最大怨气值mid,范围为[0, 1e9](题目中怨气值的最大可能);
-
建图:对于每对罪犯,若其怨气值 > mid,则说明这两个罪犯不能关押在同一监狱,在两者之间建一条无向边;
-
判定:若建出的图是二分图,则说明可以将罪犯分成两组(两个监狱),满足“怨气值>mid的罪犯不在同一组”,此时mid可行,尝试缩小范围;否则mid不可行,扩大范围;
-
最终二分得到的最小可行mid,即为答案。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
typedef pair<int, int> PII;
int n, m;
int col[N];
vector<int> g[N];
struct Edges {
int a, b, c; // a和b是罪犯,c是怨气值
} e[N];
// 染色判定二分图(DFS版)
inline bool dfs(int u, int c) {
col[u] = c;
for (int j : g[u]) {
if (col[j] == c) {
return false;
} else if (col[j] == 0 && !dfs(j, c ^ 2)) {
return false;
}
}
return true;
}
// 检查当前mid是否可行
inline bool check(int mid) {
// 重置邻接表和染色数组(每次check都要重置)
for (int i = 1; i <= n; i++) {
g[i].clear();
col[i] = 0;
}
// 建边:怨气值>mid的罪犯对建边
for (int i = 1; i <= m; i++) {
int a = e[i].a, b = e[i].b, c = e[i].c;
if (c <= mid) continue; // 怨气值<=mid,可同监狱,不建边
g[a].push_back(b);
g[b].push_back(a);
}
// 遍历所有节点,判定二分图
for (int i = 1; i <= n; i++) {
if (!col[i] && !dfs(i, 1)) return false;
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); // 加速输入输出
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int a, b, c;
cin >> a >> b >> c;
e[i] = {a, b, c};
}
// 二分答案
int l = 0, r = 1e9;
while (l < r) {
int mid = l + r >> 1; // 等价于(mid + r)/2,避免溢出
if (check(mid)) r = mid; // 可行,缩小范围
else l = mid + 1; // 不可行,扩大范围
}
cout << r; // 最终l=r,即为最小的最大怨气值
return 0;
}
二、二分图最大匹配
核心概念
-
匹配:图中若干条边,任意两条边没有公共顶点,这样的边集称为匹配;
-
最大匹配:边数最多的匹配,即为二分图最大匹配;
-
增广路:从一个未匹配的点出发,走“交错路”(交替经过未匹配边和匹配边),最终到达另一个未匹配的点,这条路径称为增广路;
-
关键性质:找到一条增广路,将路径上的边的匹配状态取反(未匹配→匹配,匹配→未匹配),匹配数会增加1;当不存在增广路时,当前匹配即为最大匹配。
补充定理(Kőnig 定理):二分图中,最小点覆盖数 = 最大匹配数(最小点覆盖后续讲解)。
匈牙利算法(求最大匹配)
算法思路
匈牙利算法的核心是“找增广路”,步骤如下:
-
依次枚举二分图左部的每个节点;
-
对于当前节点,遍历其所有相邻的右部节点,尝试找到一条增广路;
-
用一个vis数组标记右部节点是否被访问过(避免同一轮中重复访问,导致死循环);
-
若相邻节点未被匹配,或其匹配的左部节点能找到另一条增广路,则当前节点与该相邻节点匹配,匹配数加1;
-
枚举完所有左部节点后,累计的匹配数即为最大匹配数。
注:二分图的左右部是人为划分的,只要满足“无同部边”即可,一般将其中一组作为左部,另一组作为右部。
算法演示

参考代码(核心模板)
const int N = 1e5 + 10;
vector<int> e[N]; // 左部节点指向右部节点的邻接表
bool vis[N]; // 标记右部节点是否被访问(每轮重置)
int match[N]; // 右部节点匹配的左部节点(match[右部] = 左部)
// 尝试为左部节点u找到匹配
inline bool find(int u) {
for (int j : e[u]) { // 遍历u的所有相邻右部节点j
if (!vis[j]) { // j未被访问
vis[j] = true; // 标记访问
// 若j未匹配,或j的匹配节点能找到其他增广路
if (!match[j] || find(match[j])) {
match[j] = u; // j匹配u
return true; // 找到增广路,匹配成功
}
}
}
return false; // 未找到增广路,匹配失败
}
// 求最大匹配数
int max_matching(int n1) { // n1是左部节点总数
int ans = 0;
memset(match, 0, sizeof match); // 初始化匹配数组
for (int i = 1; i <= n1; i++) {
memset(vis, 0, sizeof vis); // 每轮重置vis数组
if (find(i)) ans++; // 找到匹配,计数加1
}
return ans;
}
例题1:P3386【模板】二分图最大匹配
思路分析
纯模板题,直接按照匈牙利算法思路实现即可:
-
输入左部节点数n1、右部节点数n2、边数m;
-
根据输入的边,构建左部到右部的邻接表;
-
调用匈牙利算法,枚举所有左部节点,累计匹配数,即为答案。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int n1, n2, m; // n1左部节点数,n2右部节点数,m边数
vector<int> e[N]; // 邻接表:左部节点->右部节点
bool st[N]; // 对应vis数组,标记右部节点是否被访问
int match[N]; // 右部节点匹配的左部节点
bool find(int u) {
for (int j : e[u]) {
if (!st[j]) {
st[j] = true;
if (!match[j] || find(match[j])) {
match[j] = u;
return true;
}
}
}
return false;
}
int main() {
cin >> n1 >> n2 >> m;
while (m--) {
int a, b;
cin >> a >> b;
e[a].push_back(b); // 左部a指向右部b
}
int ans = 0;
for (int i = 1; i <= n1; i++) {
memset(st, 0, sizeof st); // 每轮重置
if (find(i)) ans++;
}
cout << ans;
return 0;
}
例题2:P2825 [HEOI2016/TJOI2016] 游戏 & 放置机器人
思路分析
题目核心:在棋盘上放置炸弹,要求每行、每列(被硬石头分割后的段)只能放一个炸弹,软石头可忽略,硬石头分割行/列,求最多可放置的炸弹数。
核心转化:将问题转化为二分图最大匹配,关键是“拆点处理硬石头”:
-
棋盘元素:*(可放置)、x(软石头,不可放置,忽略)、#(硬石头,分割行/列);
-
行拆分:对于每一行,遇到硬石头就将当前行拆分为一个新的“行段”,每段内只能放一个炸弹(同一行段的*属于同一个左部节点);
-
列拆分:同理,对于每一列,遇到硬石头就拆分为一个新的“列段”,每段内只能放一个炸弹(同一列段的*属于同一个右部节点);
-
建边:每个可放置的*(i,j),对应其所在的行段和列段,在行段(左部)和列段(右部)之间建边;
-
求最大匹配:匹配数即为最多可放置的炸弹数(每行段、每列段最多匹配一次,对应“每行段、每列段最多一个炸弹”)。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
typedef long long LL;
typedef pair<int, int> PII;
int n, m, ntot, mtot; // ntot:行段总数(左部节点数),mtot:列段总数(右部节点数)
char s[N][N]; // 棋盘
int row[N][N]; // row[i][j]:(i,j)所在的行段编号
int col[N][N]; // col[i][j]:(i,j)所在的列段编号
vector<int> e[N]; // 邻接表:行段->列段
bool vis[N];
int match[N];
inline bool find(int u) {
for (int j : e[u]) {
if (!vis[j]) {
vis[j] = true;
if (!match[j] || find(match[j])) {
match[j] = u;
return true;
}
}
}
return false;
}
int main() {
cin >> n >> m;
// 第一步:处理行,拆分行段
bool flag = true; // 标记是否需要新建行段
for (int i = 1; i <= n; i++) {
flag = true;
for (int j = 1; j <= m; j++) {
cin >> s[i][j];
if (s[i][j] == '#') { // 硬石头,拆分行段
flag = true;
continue;
}
if (s[i][j] == 'x') continue; // 软石头,忽略
// 可放置的*,分配行段编号
if (flag) {
row[i][j] = ++ntot;
flag = false;
} else {
row[i][j] = ntot; // 同一行段,编号相同
}
}
}
// 第二步:处理列,拆分列段
flag = true;
for (int j = 1; j <= m; j++) {
flag = true;
for (int i = 1; i <= n; i++) {
if (s[i][j] == '#') { // 硬石头,拆分列段
flag = true;
continue;
}
if (s[i][j] == 'x') continue; // 软石头,忽略
// 可放置的*,分配列段编号
if (flag) {
col[i][j] = ++mtot;
flag = false;
} else {
col[i][j] = mtot; // 同一列段,编号相同
}
}
}
// 第三步:建边,可放置的*对应行段和列段建边
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i][j] == '*') {
int a = row[i][j], b = col[i][j];
e[a].push_back(b);
}
}
}
// 第四步:匈牙利算法求最大匹配
int ans = 0;
for (int i = 1; i <= ntot; i++) {
memset(vis, 0, sizeof vis);
if (find(i)) ans++;
}
cout << ans;
return 0;
}
三、二分图最小点覆盖
核心定义
点覆盖:图中一个点集,使得图中的每条边至少有一个端点在这个点集中;最小点覆盖:满足条件的点集中,点数最少的那个。
注意:一般图的最小点覆盖问题是NP难的,无法在多项式时间内求解,但二分图的最小点覆盖有简洁的解法,依赖Kőnig定理。
Kőnig 定理(二分图专用)
二分图中,最小点覆盖的顶点数 = 最大匹配的边数。
补充说明:该定理仅适用于二分图,是二分图最小点覆盖的核心求解依据,无需复杂计算,只需求出最大匹配数,即可得到最小点覆盖数。
应用场景
例如:在一个二分图中,需要选择最少的点,使得所有边都被覆盖(每条边至少一个端点被选中),此时直接求最大匹配数即可。
四、二分图最大独立集
核心定义
独立集:图中一个点集,使得点集中任意两个顶点之间没有边;最大独立集:满足条件的点集中,点数最多的那个。
与最小点覆盖类似,一般图的最大独立集是NP难的,但二分图的最大独立集有简洁的结论。
核心定理与推论
-
图 G=(V,E) 中,点集 C⊆V 是点覆盖,当且仅当它的补集 V∖C 是独立集;
-
图 G=(V,E) 中,最小点覆盖数 + 最大独立集数 = 顶点总数 |V|。
二分图最大独立集公式
结合Kőnig定理(最小点覆盖数 = 最大匹配数),可推出:
二分图最大独立集数 = 总顶点数 - 最大匹配数
应用场景
例如:在二分图中,选择最多的点,使得这些点之间没有边,此时用总顶点数减去最大匹配数即可得到答案。
五、相关习题(按知识点分类)
1. 染色法判断二分图
- P1525 [NOIP 2010 提高组] 关押罪犯(二分答案+染色判定)
2. 二分图最大匹配
-
P3386【模板】二分图最大匹配
-
P2825 [HEOI2016/TJOI2016] 游戏 & 放置机器人(拆点+最大匹配)
-
文理分班(经典二分图匹配应用)
-
导弹防御塔(二分图匹配拓展)
3. 最小点覆盖(结合最大匹配)
-
Machine Schedule
-
[USACO05JAN] Muddy Fields G
-
Air Raid
-
[USACO05NOV] Asteroids G

浙公网安备 33010602011771号