[图+二分图+模板] 两大二分图常用模板

0. 前言

重点在于代码实现。
在这里插入图片描述
一些好的博文总结:


图论中二分图指的是我们可以将所有点划分成两个集合,集合内部没有边,所有边起终点均在不在同一个集合。 基于图论中一个重要性质:一个图是二分图当且仅当图中不含奇数环(环中的边的数量为奇数)。

一般有两种算法最为常用,如下:

  • 染色法:基于 d f s dfs dfs,线性时间复杂度 O ( n + m ) O(n +m) O(n+m)
  • 匈牙利算法:用来求解二分图的最大匹配,理论最坏时间复杂度为 O ( n m ) O(nm) O(nm),但实际运行时间远小于 O ( n m ) O(nm) O(nm)

1. 染色法判定二分图

860. 染色法判定二分图

在这里插入图片描述
思路:

  • d f s dfs dfs 染色,用 1 和 2 区分不同颜色,0 表示未染色
  • 遍历所有点(没有说明是连通图),对未染色点进行 d f s dfs dfs一点颜色确定,则该点所在的连通块中的所有点颜色均确定。默认颜色无所谓。
  • 当两个点颜色相同则染色失败

d f s dfs dfs 代码:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 1e5+5;

int n, m;
int h[N], ne[N*2], e[N*2], idx;     // 无向图存边数量是两倍
int color[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool dfs(int u, int c) {                        // u当前点,c当前点的颜色
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i]) {    // 遍历当前点的所有邻点
        int j = e[i];
        if (!color[j]) {                        // 如果该邻点未被染色
            if (!dfs(j, 3 - c)) return false;   // 给它染色,1号点变成2,2号点变成1,则统一为3-c
        } 
        else if (color[j] == c) return false;   // 如果当前邻点已被染色,仅判断是否与自己颜色相同即可
    }
    return true;
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    
    while (m --) {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    
    bool flag = true;
    for (int i = 1; i <= n; ++i) {  // 图可能不连通,每个点从前往后染色即可
        if (!color[i]) {            // 如果当前点未被染色
            if (!dfs(i, 1)) {       // dfs返回false则染色出现问题,在此初始颜色随便定义
                flag = false;
                break;
            }
        }
    }
    
    if (flag) puts("Yes");
    else puts("No");
    
    return 0;
}

b f s bfs bfs 思路:

  • 颜色 1 和 2 表示不同颜色, 0 表示 未染色
  • 定义 queue 是存 PII,表示 <点编号, 颜色>
  • 同理,遍历所有点(防止不连通), 将未染色的点都进行 bfs
  • 队列初始化将第 i 个点入队, 默认颜色可以是 1 或 2
    • while (队列不空)
    • 每次获取队头 t, 并遍历队头 t 的所有邻边
      • 若邻边的点未染色则染上与队头 t 相反的颜色,并添加到队列
      • 若邻边的点已经染色且与队头 t 的颜色相同,则返回 false

b f s bfs bfs 代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e5+5;

int n, m;
int e[N*2], ne[N*2], h[N], idx;
int st[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool bfs(int u) {               // 相当于染一个连通块
    int hh = 0, tt = 0;
    PII q[N];                   // 定义队列,<点编号,颜色>
    q[0] = {u, 1};
    st[u] = 1;
    
    while (hh <= tt) {          // 队列不空
        auto t = q[hh ++];
        int ver = t.first, c = t.second;
        
        for (int i = h[ver]; i != -1; i = ne[i]) {      // 遍历队头临边,染色
            int j = e[i];
            
            if (!st[j]) {
                st[j] = 3 - c;
                q[++ tt] = {j, 3 - c};
            }
            else if (st[j] == c) return false;
        }
    }
    
    return true;
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m --) {
        int a, b;
        cin >> a >> b;
        
        add(a, b), add(b, a);       // 无向图
    }
    
    int flag = true;
    for (int i = 1; i <= n; ++i) {      // 遍历所有点,防止不连通
        if (!st[i]) {
            if (!bfs(i)) {
                flag = false;
                break;
            }
        }
    }
    
    if (flag) puts("Yes");
    else puts("No");
    
    return 0;
}

2. 匈牙利算法求二分图最大匹配

861. 二分图的最大匹配

在这里插入图片描述

二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

可以将其想象成搞对象,左边集合男生,右边集合女生,存在可能男女 1 对多有备胎的情况。若是大家都能男女全部牵手成功则是一个完美匹配,若你喜欢的妹子已经心有所属,那么你可以和那个哥们协商,在他有备胎的情况下换一个,然后你俩都皆大欢喜,匹配数量加 1。可能,那位兄弟的备胎妹子也已经心有所属,那么递归的协商就可以了。在尝试了所有可能都无法匹配了,那就放弃。

时间复杂度: O ( n m ) O(nm) O(nm),所有男生,最坏情况每个男生遍历所有边。实际时间复杂度远小于此。

注意:

  • 枚举男生点集的话,那么边的存储就只是从男生指向女生。因为只会找男生对应的所有女生,而不会寻找女生对应的所有男生。
  • ,虽然是无向边,但是也仅存一个方向
  • st 数组用来保证本次匹配过程中,第二个集合中的每个点只被遍历一次,防止死循环。match 存的是第二个集合中的每个点当前匹配的点是哪个。
  • 枚举每次男生时都需要重新将 st 数组置为 false。此时 match 数组可能已经有值了,即女孩已经有男友了,但又有什么关系呢?你跟他和跟我不都是只贡献了一条匹配数吗?所以可以直接将其预定了,然后如果她没男友则直接匹配成功,她有男友,你需要给那位兄弟重新找个备胎,但是由于当前这个妹子已经被你预定了,所以那位兄弟只能另寻他人,如果找到了则皆大欢喜,如果找不到那么你就赶紧滚蛋,别缠着这妹子了。 尝试完所有的心上人妹子,都不行,那么你就拜拜了~返回 false 即可。
  • 所以,每次枚举男生时初始化 st 数组是因为每个男生预定的妹子都不一样,且保证在调整时,每个妹子只能被选一次,不能重复选择。也可以理解为,后选的人优先级会高,他可以先预定了这妹子,管她有没有男友。没男友,直接拿下。有男友,让男友去递归的预定他的备胎,重复这个过程。所以每次初始化是为了在递归过程中保证每个女生只被选一遍。

大佬证明链接挺不错的

代码:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 505, M = 1e5+5;

int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];           // 妹子牵手的男生
bool st[N];             // 判重不要重复搜一个点

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

// 将男生x加入,如果x来加入匹配,是否能让匹配数增多
bool find(int x) {
    for (int i = h[x]; i != -1; i = ne[i]) {    // 遍历自己的妹子
        int j = e[i];                           // j为妹子编号
        if (!st[j]) {                           // 如果妹子j还没匹配
            st[j] = true;                       // x预定j成功,调整时前男友就别看了
            if (match[j] == 0 || find(match[j])) { // 如果j没男友,或者可以给那个男生找到备胎,则匹配成功
                match[j] = x;                      // 即便匹配失败,也不更改结果,匹配总数不减小
                return true;
            }
        }
    }
    return false;
}

int main() {
    cin >> n1 >> n2 >> m;
    memset(h, -1, sizeof h);
    
    while (m --) {
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
                
    int res = 0;                        // 当前匹配数量
    for (int i = 0; i <= n1; ++i) {     // 分析下每个男生应该找哪个妹子
        memset(st, false, sizeof st);   // 每次的预定情况都不一样,所有每轮都清空,保证在递归过程中每个女生只能被选一次
        if (find(i)) res ++;
    }
    cout << res << endl;
    return 0;
}

3. 总结

二分图问题也是图论中的秀儿问题,很多题目建图时藏匿的很深,尤其是二分图。很不好抽象和转化。

但是,万事开头难。理解算法思想,背好模板,从简单题入手,增加熟练度是最为重要的。

染色法比较简单,很容易理解,相邻不相同

匈牙利也很形象,我抢一手,你让一手,我们皆大欢喜。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值