二分图的判定与最大匹配

209 阅读4分钟

二分图

把无向图G=(V,E)G = (V, E)分为两个集合V1,V2V_1, V_2,所有的边都在V1,V2V_1,V_2之间,而V1V_1V2V_2的内部没有边。则该图成为称为二分图。

染色法判定二分图 O(n+m)O(n + m)

一个图是否是二分图,一般用“染色法”进行判定,用两种颜色对所有顶点进行染色,要求一条边所连接的两个相邻顶点的颜色不同,染色结束后,如果所有相邻顶点的颜色都不相同,它就是二分图。

具体步骤:

  1. 初始时,每个节点颜色为0,代表未染色,顺序访问每个节点,判断节点i是否被染过色(1或2),若染过,则继续循环
  2. 若i未染色,将该节点颜色赋值为1(color[i] = 1),并遍历该节点的所有相邻节点
  3. 若相邻节点j未染色,则color[j] = 3 - color[i],并递归给j的相邻节点染色
  4. 若相邻节点已染色,判断其与节点i颜色是否相同,若相同,则发生冲突,即不是二分图
  5. 循环上述步骤,直到发生冲突或染色结束。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>

using namespace std;
const int N = 1e5 + 10;
int h[N], ne[M], e[M], idx; //邻接表存图
int n, m, color[N]; //color取值0, 1, 2, 0代表未染色,1,2代表不同的颜色

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

bool dfs(int u){
    for (int i = h[u]; ~i; i = ne[i]) { //访问节点u的邻居,对其进行染色
        int x = e[i];
        if (!color[x]){
            //若节点x未染色,则对其染色,并递归给其邻居节点染色
            color[x] = 3 - color[u];
            if (!dfs(x)) return false; //若给x的邻居进行染色时发生冲突,则返回false
        }
        else if (color[x] == color[u]) return false; //发生冲突,x颜色与邻居u一致,返回false
    }
    return true; //染色成功
}

int main(){
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    int a, b;
    for (int i = 0; i < m; i ++ ){
        cin >> a >> b;
        add(a, b), add(b, a); //添加边操作
    }
    
    bool ans = true;
    for (int i = 1; i <= n; i ++ ){
        if (!color[i]){ //若该节点未染色
            color[i] = 1; //该节点颜色赋为1
            if (!dfs(i)) { //若在给自己的邻居递归染色时发生冲突,则不是二分图,判定结束
                ans = false;
                break;
            }
        }
    }
    
    if (ans) puts("Yes");
    else puts("No");
}

二分图的最大匹配O(nm)O(nm),一般远小于O(nm)O(nm)

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

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

给定一个二分图,其中左半部(假设是一群男生)包含 n1n_1 个点(编号 1∼n1n_1),右半部(假设是一群女生)包含 n2n_2 个点(编号 1∼n2n_2),二分图共包含 mm 条边(表示两个人认识)。数据保证任意一条边的两个端点都不可能在同一部分中。每个人都想和自己认识的异性进行匹配,求最大匹配数。

匈牙利算法具体步骤:

  1. 使用match数组标记每个女生匹配成功的男生,st数组标记每个男生在进行匹配时访问的女生的状态,st[i]=true表示第i个女生该男生已经访问过了,false代表未访问
  2. 遍历每个男生,对每个男生i,访问其所有未访问的邻居(女生),对其邻居j,设置st[j] = true,即已访问过,若j未匹配,则设置match[j] = i.
  3. 若其邻居j已匹配,则递归j匹配成功的男生match[j],访问其所有为被男生i访问过的邻居,看其是否能匹配其他女生,若能,则将match[j]的值改为i,若不能,本次匹配失败。
  4. 循环上述步骤, 每成功匹配一次,结果+1,直到所有男生匹配结束
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>

using namespace std;

const int N = 1e5 + 10;
int h[N], e[2 * N], ne[2 * N], idx; //邻接表存图
bool st[N]; // 访问状态,st[i] = true代表已访问过
int match[N], n1, n2, m; //match数组存放每个女生匹配到的男生

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

bool dfs(int u){ //对节点u(男生节点)进行匹配
    for (int i = h[u]; ~i; i = ne[i]){
        int x = e[i];
        if (!st[x]){ //若x(女生节点)还未被访问过
            st[x] = true; //无论匹配是否成功,均标记为已访问
            if (!match[x]) { //若x还未匹配,则与u进行匹配,并返回true
                match[x] = u;
                return true;
            }
            else if (dfs(match[x])) { //若x已匹配,则判断其是否能与其它女生匹配(st数组在此处起作用),若能,则x与u也可匹配成功
                match[x] = u;
                return true;
            }
        }
    }
    return false; //匹配失败
}

int main(){
    cin >> n1 >> n2 >> m; //两类节点的数量与边的数量
    int a, b;
    
    memset(h, -1, sizeof h);
    
    for (int i = 0; i < m; i ++ ){
        cin >> a >> b;
        add(a, b);
    }
    
    int ans = 0;
    
    for (int i = 1; i <= n1; i ++ ){
        memset(st, 0, sizeof st); //因为不同男生之间存在重复邻居,但匈牙利算法要对每个男生访问其所有邻居进行匹配,因此,每次都需要重置st
        if (dfs(i)) ans ++;
    }
    
    cout << ans << endl;
}