Acwing 二分图

二分图

二分图:可以将一个图中的所有点吗,分成左右两个部分,使得图中的所有边,都是从左边集合中的点连到右边集合中的点。而左右两个集合内部没有边。

在这里插入图片描述
再通俗点理解:一个图是二分图当且仅当图中不含奇数环(奇数环:边数为奇数的环)

1.染色法–判断二分图

对每一个点进行染色操作,只用黑白两种颜色;用dfs和bfs两种方式去实现,对图进行遍历并染色。时间复杂度为O(n+m)

定理:二分图一定不含奇数环,不含奇数环的图一定为二分图

充分性:如果图中存在奇数环(构成环的顶点数量是奇数),那一定不是二分图),下图可以看到,依次选一个点,进行染色(原则是相邻的点要染于该点不同色),奇数环的染色结果会出现矛盾;
在这里插入图片描述必要性:如果没有奇数环,那么剩下的点的关系就是:偶数环or单链。这两种情况都能保证同一条边上相邻顶点在不同集合中,所以也是成立的;
在这里插入图片描述

综上:只要在染色过程中不存在矛盾(这里用黑白进行染色,即一个点不能即为黑色,又为红色),整个图遍历完成之后,所有顶点都顺利染上色。就说明这是一个二分图。
板子:

//时间复杂度是 O(n+m) n表示点数,m表示边数
int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色

// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (color[j] == -1)
        {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;
    }

    return true;
}

bool check()
{
    memset(color, -1, sizeof color);
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
        if (color[i] == -1)
            if (!dfs(i, 0))
            {
                flag = false;
                break;
            }
    return flag;
}

Acwing 860 .染色法判定二分图
在这里插入图片描述
实现思路:这是使用深度优先遍历DFS实现(代码比BFS短点)

  • 使用邻接表存储图;
  • 使用一个数组color代表当前点的染色,若为-1则未染色,若为0则为白色,若为1则为黑色;
  • 设置一个标志变量flag判断是否矛盾。然后循环遍历各点,若未染色则进行染色2(染为0或1),对该点进行深度优先遍历,对其连通的点进行判断。
    • 若未染色,则染上与该点相反的颜色;若已染色,判断是否与当前点的颜色相同;
      • 若相同则出现矛盾,提前返回false,得到结果不是二分图,否则继续遍历判断、染色
  • 直到循环结束,判断标志变量flag,为真则为二分图。

具体实现代码(详解版):

#include <iostream>
#include <cstring>  // 用于 memset 函数
#include <algorithm>  // 可选的库,实际代码中未使用

using namespace std;

const int N = 100010, M = 200010;  // N 为最大节点数,M 为最大边数的两倍(无向图需要存储双向边)

int h[N], e[M], ne[M], idx;  // h[] 是邻接表的头节点数组,e[] 存储边的终点,ne[] 存储下一条边的下标,idx 为边的下标
int color[N];  // 颜色数组,-1 表示未染色,0 和 1 表示两种颜色
int n, m;  // n 为节点数,m 为边数

// 添加一条从节点 a 到节点 b 的边
void add(int a, int b) {
    e[idx] = b;  // 将 b 节点存储在边数组 e[] 中
    ne[idx] = h[a];  // 将边的下一条边设置为 h[a] 所指向的边
    h[a] = idx++;  // 更新 h[a] 为新加入的这条边的下标,idx 自增
}

// 深度优先搜索函数,u 为当前遍历的节点,c 为当前节点的颜色
// 若图中出现矛盾(即相邻节点颜色相同),返回 false
bool dfs(int u, int c) {
    color[u] = c;  // 给当前节点 u 染上颜色 c
    // 遍历所有与节点 u 相连的边
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];  // j 为与 u 相连的节点
        if (color[j] == -1) {  // 如果节点 j 未被染色
            if (!dfs(j, !c)) return false;  // 递归对 j 进行染色,颜色与 u 相反
        }
        else if (color[j] == c) return false;  // 如果 j 已染色且颜色与 u 相同,则图中有矛盾
    }
    return true;  // 没有发现矛盾,返回 true
}

int main() {
    cin >> n >> m;  // 输入节点数 n 和边数 m
    memset(h, -1, sizeof h);  // 初始化邻接表头节点数组 h[],全设为 -1
    memset(color, -1, sizeof color);  // 初始化颜色数组,所有节点初始为未染色状态

    // 读取 m 条边
    while (m--) {
        int a, b;
        cin >> a >> b;
        add(a, b);  // 添加从 a 到 b 的边
        add(b, a);  // 因为是无向图,也需要添加从 b 到 a 的边
    }

    bool flag = true;  // 用来标记是否是二分图
    // 遍历每个节点,检查是否有未染色的节点
    for (int i = 1; i <= n; i++) {
        if (color[i] == -1) {  // 如果当前节点未被染色
            if (!dfs(i, 1)) {  // 尝试以颜色 1 开始对该节点进行染色
                flag = false;  // 如果染色过程中发现矛盾,说明不是二分图
                break;  // 不需要继续检查,直接退出循环
            }
        }
    }

    // 输出结果
    if (flag) puts("Yes");  // 如果 flag 仍为 true,则图是二分图
    else puts("No");  // 如果 flag 为 false,则图不是二分图

    return 0;
}

2.匈牙利法–二分图的最大匹配

  • 匹配(本质是一个边的集合!)
    • 给定一个二分图S,在S的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配;
  • 极大匹配
    • 极大匹配是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。(也就是说,再加入任意一条不在匹配集合中的边,该边肯定有一个顶点已经在集合的边中了)
  • 最大匹配
    • 所有极大匹配当中边数最多的开匹配;
      下图是一个最大匹配(黄色边):
      在这里插入图片描述

通俗一点的理解:

现实比喻
me月老
左侧蓝色集合点男嘉宾组
右侧红色集合点女嘉宾组
中间的边(考虑所有)有恋爱的可能(可以成为一对)
匹配一个男嘉宾只能牵手一位女嘉宾(女嘉宾也只能选中一个男嘉宾);一对 一
最大匹配根据场上的恋爱可能关系,最多牵手多少对!

匈牙利算法:两个集合中都存在一些点,以左集合出发,查找两个集合之中匹配成功的点的个数。如果左集合的某一个点发现与自己相连的节点已经被占有,则查询占有该节点的左集合的点是否有其他可配对的点,若有则两全其美,否则继续寻找,若仍未找到,则配对失败。时间复杂度理论上是O(nm),但实际运行时间一般远小于O(nm)。

通俗点说:从左边开始,左边a喜欢右边b(即两者有连线),若b此时没有已经匹配的人或者即便有匹配的人,那个匹配的对象可以找到其他人(下家)来配对,即把b让给a,则a与b匹配成功。若两种情况下都不满足,则a只能找下一个有好感的人尝试匹配,直至匹配成功或已经没有喜欢的了。进行下一个人的匹配,最终得到的匹配对数即为二分图的最大匹配对。
板子:

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}

// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}

Acwing 861.二分图的最大匹配
在这里插入图片描述
实现思路:

  • 使用邻接表存储图;
  • 设置一个判重数组s[],s[i]为真表示i节点已经被当前节点尝试过,每次换一个人都要把数组s[]重置为false;设置一个匹配数组match[],为0表示当前节点还没有匹配的对象,否则为匹配对象的编号;
  • find函数(找匹配对象):每次对相连的点尝试匹配,若当前连接点未尝试过,则尝试;若该点没有匹配对象或者它的匹配对象可以把该节点让给我,则与之匹配,退出匹配成功,下一个继续。

具体实现代码(详解版):

#include <cstring>  // 用于 memset 函数
#include <iostream>  // 用于输入输出

using namespace std;

const int N = 510, M = 100010;  // N 表示左侧集合中的最大点数,M 表示边的最大数量
int h[N], ne[M], e[M], idx;  // 邻接表的实现
int match[N];  // 匹配数组,记录右侧集合中的点匹配到的左侧集合中的点
bool s[N];  // 判重数组,用于标记在每次匹配中右侧的节点是否已经被访问过
int n1, n2, m;  // n1: 左侧集合中的点数,n2: 右侧集合中的点数,m: 边的数量

// 添加一条从 a 到 b 的边
void add(int a, int b) {
    e[idx] = b;  // b 是与 a 相连的节点
    ne[idx] = h[a];  // 当前 a 的邻接表头节点为 h[a]
    h[a] = idx++;  // 将 b 添加到 a 的邻接表中,并将 idx 自增
}

// 寻找左侧点 x 的匹配对象
bool find(int x) {
    // 遍历与 x 相连的所有点
    for (int i = h[x]; i != -1; i = ne[i]) {
        int j = e[i];  // j 是 x 相连的右侧集合中的一个点
        if (!s[j]) {  // 如果右侧点 j 没有被访问过
            s[j] = true;  // 标记右侧点 j 已访问
            // 如果 j 还没有匹配,或者 j 的现有匹配对象可以重新匹配
            if (match[j] == 0 || find(match[j])) {
                match[j] = x;  // 将 j 匹配到 x
                return true;  // 匹配成功
            }
        }
    }
    return false;  // 匹配失败
}

int main() {
    cin >> n1 >> n2 >> m;  // 输入左侧集合的点数、右侧集合的点数和边的数量
    memset(h, -1, sizeof h);  // 初始化邻接表,h[i] 为 -1 表示该点没有相连的边

    // 输入所有边
    while (m--) {
        int a, b;
        cin >> a >> b;
        add(a, b);  // 在邻接表中添加边 a -> b
    }

    int cnt = 0;  // 记录最大匹配数
    // 遍历左侧集合中的每个点,尝试为其找到匹配
    for (int i = 1; i <= n1; i++) {
        memset(s, false, sizeof s);  // 每次尝试为一个左侧点匹配时,重置访问标记数组 s
        if (find(i)) cnt++;  // 如果找到匹配,匹配数加 1
    }

    // 输出最大匹配数
    cout << cnt << endl;

    return 0;
}

下面是二分图简单的一个总结:

类别描述
定义二分图是可以将节点划分为两个不相交的子集,使得图中所有边都连接两个不同子集的节点。
性质- 无奇数长度的环
判定方法- 染色法:通过DFS/BFS染色判断相邻节点颜色是否相同,若相邻节点颜色相同则不是二分图。
算法- 染色法:DFS或BFS遍历图,给节点染色。
- 匈牙利算法:解决二分图最大匹配问题。
应用场景- 最大匹配问题:从左集和右集之间找到最多的配对。
- 任务分配:将工人与任务匹配。
- 网络流问题:通过最大流求解。
判定代码示例使用深度优先搜索(DFS)进行染色判定是否是二分图。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值