二分图相关算法模板

二分图判定

概念解释

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

相关性质定理

一个图是二分图 \(\Leftrightarrow\) 图中不存在奇数环 \(\Leftrightarrow\) 染色过程中不存在矛盾

  1. 对于图中不存在奇数环 \(\Leftrightarrow\) 染色过程中不存在矛盾进行以下证明:
  • 充分性
    采用反证法,假设图中不存在奇数环同时染色过程中存在矛盾
    设采用黑白染色,同时有两个白色点a和b,一条边e的存在使得a和b的染色出现矛盾
    现在假设先不考虑这条边,a和b由于已经被成功染色,那么说明一条以a和b为端点的路径,且路径上点的颜色一定满足“白(a),黑,白,黑,..., 白(b)”
    显然,路径上为奇数个点,加上最初不考虑的那条比,就构成了一个奇数环。与假设相矛盾,因此充分性得证。

  • 必要性
    采用反证法,假设染色过程中不存在矛盾,但是图中存在奇数环
    显然,当存在奇数环时,规定点a为第一个点且为白色,按照黑白交替染色的原则,最后一个点b一定为白色点,即染色过程出现了矛盾
    因此,假设不成立,必要性得证

  1. 对于一个图是二分图 \(\Leftrightarrow\) 图中不存在奇数环(或染色过程中不存在矛盾)进行以下证明
  • 充分性
    采用反证法,假设一个图是二分图,且图中存在奇数环
    设采用黑白染色,指定一白色点a作为第一个点
    由于该图是二分图,每一条边的两个端点应处于两个不同的集合,因此接下来点的颜色依次为“黑,白,黑,... 白(b),白(a)”
    即染色一定会出现矛盾,因此假设不成立,充分性得证

  • 必要性
    设采用黑白染色,由于染色过程中不存在矛盾,因此任意一条边的两端点都是一黑一白,即每一条边的两端点都处于两个集合中,符合二分图定义,必要性得证

算法思路

每个点有两种状态,对于每种状态采取不同的策略

1.未染色

将该点及其子节点染色

2.已染色

判断是否出现冲突,是则说明不是二分图

二分图就是不包含奇数环的图 或者说 有两个集合a 和 b,对于一条边的两个点一个分到a,一个分到b,所以如果想要满足二分图,一条边的两个顶点必须分到不同的集合

把分到集合a看为染色为a,分到集合b看为染色为b,包含奇数环一定有某个点即被染色为a又被染色为b,这种情况称之为冲突,二分图就是任何一个点的染色都是唯一的,不会存在冲突

所以代码实现就是判断在染色过程中是否出现了某个点的染色出现了冲突,而染色过程就是遍历过程,遍历就可以采用dfs和bfs

从数据结构来看是稀疏图,且算法对存储结构并没有明显的要求(bellman_ford明确需要遍历边,所以采用结构体存边),所以采用邻接表存储

dfs版本

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

using namespace std;

const int N = 2e5 + 10;

int n, m;
int color[N];
int head[N], e[N], ne[N], idx;

void insert(int a, int b)
{
    e[idx] = b;
    ne[idx] = head[a];
    head[a] = idx ++;
}
bool dfs(int x, int v)
{
    color[x] = v;
    
    for (int i = head[x]; i != -1; i = ne[i])
    {
        int p = e[i];
        if (color[p] == -1)
        {
            if (!dfs(p, 1 ^ v)) return false; 
        }
        else if (color[p] == color[x]) return false;
    }
    
    return true;
}
int main()
{
    cin >> n >> m;
    
    memset(head, -1, sizeof head);
    memset(color, -1, sizeof color);
    
    while (m --)
    {
        int a, b;
        cin >> a >> b;
        insert(a, b);
        insert(b, a);
    }
    
    bool flag = true;
    for (int i = 1; i <= n; ++ i)
        if (color[i] == -1)
        {
            if (!dfs(i, 0)) // 如果i号点没有染色,那么递归将这一条路上的所有点都染色,如果染色过程中不是二分图,则返回false
            {
                flag = false;
                break;
            }
        }
        
    if (flag) cout << "Yes" << endl;
    else cout << "No" << endl;
    
    return 0;
}

bfs版本

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

using namespace std;

const int N = 2e5 + 10; // 第一次写的1e5+10超时了,问题是本题是无向图,所以一条边需要2个存储空间,所以要翻倍

int n, m;
int color[N]; // -1表示未染色 0 和 1 表示两种不同的颜色
int head[N], e[N], ne[N], idx;

void insert(int a, int b)
{
 e[idx] = b;
 ne[idx] = head[a];
 head[a] = idx ++;
}
int main()
{
 cin >> n >> m;
 
 memset(head, -1, sizeof head);
 memset(color, -1, sizeof color);
 
 while (m --)
 {
     int a, b;
     cin >> a >> b;
     insert(a, b);
     insert(b, a); // 无向图,一定不要忘了
 }
 
 queue<int> q;
 bool flag = true;
 // 并没有保证一定为连通图,所以为了遍历到所有点需要从每一个点开始一次
 for (int i = 1; i <= n; ++ i)
 {
     if (color[i] != -1) continue; // 点i已经被染色就不再考虑了

     q.push(i);
     color[i] = 0;
     while (q.size())
     {
         int t = q.front();
         q.pop();
         for (int i = head[t]; i != -1; i = ne[i])
        {
            int p = e[i];
            if (color[p] == -1)
            {
                color[p] = 1 ^ color[t]; // 0 -> 1; 1 -> 0
                q.push(p);
            }
            else if (color[p] == color[t])
            {
                flag = false;
                break;
            }
        }
        if (!flag) break;
     }
     if (!flag) break;
 }

 if (flag) cout << "Yes" << endl;
 else cout << "No" << endl;
 
 return 0;
}

二分图最大匹配

使用的算法叫做匈牙利算法

算法思想

把匹配过程做一个比喻

左侧集合为男生,右侧集合为女生,男生和女生之间存在联系,我们要在他们之间选择一些联系,通俗地讲我们的目标是使得所有人都不会脚踩多条船

遍历所有男生,对于每一个男生,遍历和他存在联系的女生

  1. 如果找到的女生还没有匹配,那么直接匹配

  2. 如果找到的女生已经得到匹配了,我们找到和该女生匹配的男生b,看一看这个男生b能不能放弃这个女生去选择其他人,如果可以则把该女生和目前的男生a进行匹配

通俗地讲就是男生a知道女生c已经和男生b匹配了,但是a并不会放弃,如果b有备胎,则让b选择备胎,a则可以选择c

思想说难也不难,但是落实到代码上就有点困难了

代码实现

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

using namespace std;

const int N = 510, M = 1e5 + 10;

int n1, n2, m;
int match[N];
bool st[N];
int head[N], e[M], ne[M], idx;

void insert(int a, int b)
{
    e[idx] = b;
    ne[idx] = head[a];
    head[a] = idx ++;
}
bool find(int x) // 返回x是否可以找到匹配
{
    for (int i = head[x]; i != -1; i = ne[i])
    {
        int p = e[i];
        /**
         * 递归的难点就在于我们很难想到递归层面的问题,所谓递归层面的问题就是
         * 当前状态的一些值会不会对递归造成影响,这是我们很难也一般不会去想的
         * 
         * 按照语言描述,遍历所有女生,如果她满足匹配条件那么就可以发生匹配,这就完事了
         * 但是样例的运行结果是MLE,出现问题的原因是
         * 假设男生ba先匹配到了女生ga,男生bb也想匹配ga,所以和ga匹配的ba就去看有没有其他人可以匹配,按照我们的程序
         * 他一定会从和他相关的第一个人开始看,结果看到的是和他已经匹配的ga,然后和ga匹配的人去看有没有其他人可以匹配
         * 显然这就是个死循环。
         * 
         * 所以这个问题是挺难想到的,因为这实际上是递归层面的问题了。也就是我们无论从语言描述上还是从代码上都很难想到这种问题
         * 
         * 问题出现在我们判定女生能够匹配的前提有缺失,之前我们的判断依据是如果一个女生没有匹配或者和他匹配的男生能够换一个人那么说明这个女生可以匹配
         * 还有一个条件是目前判断的女生不能是其它男生想要匹配的人,因为是递归,所以这种描述发生的场景只会是男生b想和女生匹配,之前和该女生匹配
         * 的男生a查看能不能换人,所以男生a就不应该再看b选中的这个女生了,应该看其他人,所以在男生b那一层就应该把该女生标记,然后
         * 在判断和女生匹配的男生能不能换人时就不会再看这个女生了
         * 
         * 还有一个需要注意的点是标记必须在判断能不能换人之前做
         */
         if (!st[p])
         {
             st[p] = true;
             if (!match[p] || find(match[p]))
             {
                 match[p] = x;
                 return true;
             }
         }
        // 下面的问题就是标记放在了判断能不能换人之后,其实根本没起到该有的作用
        // if (!st[p] && (!match[p] || find(match[p])) ) // 此时找到的女生如果还没有发生匹配或者发生了匹配,但是可以让和女生匹配的男生换一个人,那么当前的男生都可以和这个女生匹配
        // {
        //     match[p] = x;
        //     st[p] = true;
        //     return true;
        // }
    }
    
    // 如果说遍历了和男生x有关联的所有女生都没有找到匹配的,那么说明无法匹配成功返回false
    return false;
}
int main()
{
    cin >> n1 >> n2 >> m;
    
    memset(head, -1, sizeof head);
    
    while (m --)
    {
        int a, b;
        cin >> a >> b;
        insert(a, b); // 因为只需要从男生找女生,从女生找到对应的男生通过match数组即可
    }
    

    // 遍历每一个男生,判断其是否可以找到匹配
    int res = 0;
    for (int i = 1; i <= n1; ++ i)
    {
        // 这里的memset也是很难理解的,st的含义可以看为当前男生想要和哪个女生匹配,对于每个人来说,最初的状态都应该是false
        memset(st, false, sizeof st); // memset居然能赋值true和false,第一次知道
        if (find(i)) 
        ++ res;
    }
    
    cout << res << endl;
}
posted @ 2021-01-30 23:31  0x7F  阅读(85)  评论(0编辑  收藏  举报