二分图

关于二分图

二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。

通过百度百科定义可以理解为,一个图中的全部节点可以分为两个集合,集合内部节点无连通,集合与集合之间的点可以任意连通。

image

当图中有环时,环的边数为偶数个才能构造出二分图,奇数个边无法成为连通图。比如:

image

当图中无环时,显然可以构造出二分图。

二分图一般有两类问题,判定二分图和最大匹配数量,其中常用染色法判定一个图是否为二分图,时间复杂度为O(m + n),求最大匹配数量使用匈牙利算法,时间复杂度为O(nm)。

染色法判定二分图

利用节点被划分为两个集合的特性,我们可以使用黑色和白色对节点染色,对应黑色节点集合和白色节点集合。为满足二分图性质,一条边两头的节点必须是不同颜色(集合内部不能有连通)。

因此当我们确定一个点的颜色后,该点所属的连通块的所有节点颜色都将被确定,当出现回路,染色发生冲突(例如一个白色节点需要被染为黑色),表示该图不是二分图。

例题:https://www.acwing.com/problem/content/862/

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010, M = 200010;

int h[N], e[M], ne[M], idx; // 稀疏图使用邻接矩阵存储

int color[N]; // 0表示未染色,1表示染白色,2表示染黑色

int n, m;

bool dfs(int n, int c) {
    
    // 回路节点染色冲突
    if (color[n] == 3 - c) return false; // 3-c: 黑色边白色,白色变黑色
    
    // 连通块染色
    if (color[n] == 0) {
        color[n] = c;
        for (int i = h[n]; i != -1; i = ne[i]) {
            int j = e[i];
            if (!dfs(j, 3 - c)) return false;
        }
    }
    
    // 允许回路节点染色相同
    return true;
}

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

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m -- ) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    
    bool res = true;
    
    // 对所有节点及其连通块染色
    for (int i = 1; i <= n; i ++ ) {
        if (!color[i]) {
            if (!dfs(i, 1)) res = false; // 当某连通块染色失败,表示不是二分图
        }
    }
    
    if (res) printf("Yes");
    else printf("No");
    
    return 0;
}

二分图的最大分配

在二分图中,如果左边节点和右边节点一一对应,则可认为成功匹配一对,那么二分图中最多能有多少对节点呢?这里用到了匈牙利算法,其核心思想是“想尽一切办法让别的节点让出”,下面通过示例演示:

图中表示一个二分图,我们可以假设左边集合是“求偶者”,右边集合是“被求偶者”。求偶的前提是必须相连,并且只能一一配对,不能“脚踏两只船”。

  • 节点1只能和节点5配对。
  • 节点2虽然心里既喜欢6又喜欢7,但是现实生活只能和其中一个配对,我们假设2和6牵手成功。
  • 节点3只喜欢6,但是6已经有所属。此时节点3践行匈牙利算法的思想,想尽一切办法和6在一起,于是3通过6找到6所属的2,发现2还喜欢7,于是和2说:“我只喜欢6,请你让给我吧!”,于是2放弃6,和7重新配对。这样节点3和2都可以成功配对。
  • 节点4只喜欢7,但是7已经和2配对。节点4也不愿放弃,通过7找到求偶者2,希望2可以把7让给4。2表示可以,于是2找到了6,发现6已经和3配对,于是2找到3,希望把6让给2,但是3表示自己只喜欢6,无法让给你。于是3通知2不可以,2通知4不可以,因此4无法成功配对。

image

每个节点都想尽一切办法配对成功,哪怕去拆散原配。当节点要被拆散,如果没有其他选择,则坚持不被拆散,如果有“备胎”则放弃原配,“成他人之美”。比喻可能不太恰当hhh,总之就是资源尽量平衡就是了~

例题:https://www.acwing.com/problem/content/863/

#include <iostream>
#include <cstring>

using namespace std;

const int N = 510, M = 100010;

int h[N], e[M], ne[M], idx;

int match[N]; // match数组存放“被求偶”节点配对的节点

bool st[N];

int n1, n2, m;

// 给节点a找到配对节点
bool find(int a) {
    
    for (int i = h[a]; i != -1; i = ne[i]) {
        int j = e[i];
        
        if (!st[j]) {
            
            // 尝试过的节点打上标记,防止重复尝试
            st[j] = true;
            
            // 如果对方没有原配或者原配能让出,则可以配对成功
            if (match[j] == 0 || find(match[j])) {
                match[j] = a;
                return true;
            }
        }
    }
    
    return false;
}

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

int main() {
    
    scanf("%d%d%d", &n1, &n2, &m);
    
    memset(h, -1, sizeof h);
    
    while (m -- ) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b); // 只遍历“求偶者”集合,因此只需要一条有向边
    }
    
    int res = 0;
    
    for (int i = 1; i <= n1; i ++ ) {
        memset(st, false, sizeof st); // 重置
        if (find(i)) res ++ ;
    }
    
    printf("%d", res);
    
    return 0;
}
posted @ 2022-03-02 23:03  moon_orange  阅读(266)  评论(0编辑  收藏  举报