二分图 学习笔记

二分图学习笔记

前言

注:经过ai修改版

什么是二分图?二分图,又称二部图,是一类结构特殊的图。它的顶点集可以划分为两个互不相交的子集(记为U和V),使得图中的每条边都连接这两个集合之间的一对点,而不会连接同一集合内部的点。简单讲就是一个图的节点被分为两个集合,每个集合内的节点之间没有边,仅不同集合间有边的图。

二分图具有诸多特殊性质,本篇笔记着重讲述二分图的核心知识点:染色法判定二分图、二分图最大匹配(匈牙利算法)、二分图最小点覆盖、二分图最大独立集,搭配例题与代码,便于理解和应用。

二分图

常见的二分图例子:树(任意树都是二分图,无环自然无奇环)、偶环(环中节点数为偶数)、网格图(如棋盘)等。

一、染色法判定二分图

核心原理

染色法的核心思想的是用两种颜色(如1和2)对所有节点进行染色,通过DFS或BFS遍历图,遵循“相邻节点颜色不同”的规则:

  1. 从一个未染色的节点开始,将其染为颜色1;

  2. 遍历该节点的所有相邻节点,将其染为颜色2;

  3. 递归/迭代处理相邻节点的相邻节点,交替染为1和2;

  4. 若遍历过程中,发现某个相邻节点已染色,且颜色与当前节点颜色相同,则说明图中存在奇数环(环中节点数为奇数);

  5. 结论:存在奇环的图不是二分图,反之,无奇环的图是二分图。

参考代码(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 提高组] 关押罪犯

思路分析

题目核心:将罪犯分到两个监狱,使同一监狱中罪犯的最大怨气值最小。采用「二分答案 + 染色法判定二分图」的思路:

  1. 二分答案:二分最小的最大怨气值mid,范围为[0, 1e9](题目中怨气值的最大可能);

  2. 建图:对于每对罪犯,若其怨气值 > mid,则说明这两个罪犯不能关押在同一监狱,在两者之间建一条无向边;

  3. 判定:若建出的图是二分图,则说明可以将罪犯分成两组(两个监狱),满足“怨气值>mid的罪犯不在同一组”,此时mid可行,尝试缩小范围;否则mid不可行,扩大范围;

  4. 最终二分得到的最小可行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 定理):二分图中,最小点覆盖数 = 最大匹配数(最小点覆盖后续讲解)。

匈牙利算法(求最大匹配)

算法思路

匈牙利算法的核心是“找增广路”,步骤如下:

  1. 依次枚举二分图左部的每个节点;

  2. 对于当前节点,遍历其所有相邻的右部节点,尝试找到一条增广路;

  3. 用一个vis数组标记右部节点是否被访问过(避免同一轮中重复访问,导致死循环);

  4. 若相邻节点未被匹配,或其匹配的左部节点能找到另一条增广路,则当前节点与该相邻节点匹配,匹配数加1;

  5. 枚举完所有左部节点后,累计的匹配数即为最大匹配数。

注:二分图的左右部是人为划分的,只要满足“无同部边”即可,一般将其中一组作为左部,另一组作为右部。

算法演示

匈牙利算法演示

参考代码(核心模板)

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【模板】二分图最大匹配

思路分析

纯模板题,直接按照匈牙利算法思路实现即可:

  1. 输入左部节点数n1、右部节点数n2、边数m;

  2. 根据输入的边,构建左部到右部的邻接表;

  3. 调用匈牙利算法,枚举所有左部节点,累计匹配数,即为答案。

参考代码

#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] 游戏 & 放置机器人

思路分析

题目核心:在棋盘上放置炸弹,要求每行、每列(被硬石头分割后的段)只能放一个炸弹,软石头可忽略,硬石头分割行/列,求最多可放置的炸弹数。

核心转化:将问题转化为二分图最大匹配,关键是“拆点处理硬石头”:

  1. 棋盘元素:*(可放置)、x(软石头,不可放置,忽略)、#(硬石头,分割行/列);

  2. 行拆分:对于每一行,遇到硬石头就将当前行拆分为一个新的“行段”,每段内只能放一个炸弹(同一行段的*属于同一个左部节点);

  3. 列拆分:同理,对于每一列,遇到硬石头就拆分为一个新的“列段”,每段内只能放一个炸弹(同一列段的*属于同一个右部节点);

  4. 建边:每个可放置的*(i,j),对应其所在的行段和列段,在行段(左部)和列段(右部)之间建边;

  5. 求最大匹配:匹配数即为最多可放置的炸弹数(每行段、每列段最多匹配一次,对应“每行段、每列段最多一个炸弹”)。

参考代码

#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难的,但二分图的最大独立集有简洁的结论。

核心定理与推论

  1. 图 G=(V,E) 中,点集 C⊆V 是点覆盖,当且仅当它的补集 V∖C 是独立集;

  2. 图 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

posted @ 2026-03-20 20:13  神烦doge  阅读(75)  评论(0)    收藏  举报