算法竞赛——二分图及应用

二分图

二分图简介

定义:

简而言之,就是顶点集V可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。——百度百科

辨析示例

区别二分图,关键是看点集是否能分成两个独立的点集。

图1

图1中U和V构造的点集所形成的循环圈不为奇数,所以是二分图。

图2

图2中U和V和W构造的点集所形成的的循环圈为奇数,所以不是二分图。

1.染色法判断二分图

判定:

二分图是这样一个图: 有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接!

无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。

判断二分图的常见方法是染色法: 开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色, 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!

算法模板

int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,0表示未染色,1表示白色,2表示黑色

// 参数: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])//如果没染过颜色
        {
            //dfs深搜 染色 1 或者 2
            //如果不可以将j成功染色
            if(!dfs(j, 3 - c)) return false;
        }
        else if(color[j] == c) return false; //如果染过颜色且和c相同
    }
    
    return true;
}

bool check()
{

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

例题

【acwing 860染色法判定二分图】

给定一个 nn 个点 mm 条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图。

输入格式

第一行包含两个整数 nn 和 mm。

接下来 mm 行,每行包含两个整数 uu 和 vv,表示点 uu 和点 vv 之间存在一条边。

输出格式

如果给定图是二分图,则输出 Yes,否则输出 No

数据范围

1≤n,m≤1051≤n,m≤105

输入样例:

4 4
1 3
1 4
2 3
2 4

输出样例:

Yes

DFS版本代码思路:

  • 染色可以使用12区分不同颜色,用0表示未染色
  • 遍历所有点,每次将未染色的点进行dfs, 默认染成1或者2
  • 由于某个点染色成功不代表整个图就是二分图,因此只有某个点染色失败才能立刻break/return
    • 染色失败相当于存在相邻的2个点染了相同的颜色

【参考代码】

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

using namespace std;

const int N = 1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], idx;
int color[N];
int n, m;

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

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])//如果没染过颜色
        {
            //dfs深搜 染色 1 或者 2
            //如果不可以将j成功染色
            if(!dfs(j, 3 - c)) return false;
        }
        else if(color[j] == c) return false; //如果染过颜色且和c相同
    }
    
    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);
    }
    
    //遍历判断所有点,染色
    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;
}

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

导读

要了解匈牙利算法必须先理解下面的概念:

匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。

最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

【匹配】

image

image

【最大匹配】

image

注:最大匹配结果可能不唯一,但最大匹配数一样!

算法模板

//match[j]=a,表示女孩j的现有配对男友是a
int match[N];
//st[]数组我称为临时预定数组,st[j]=a表示一轮模拟匹配中,女孩j被男孩a预定了。
int st[N];

//这个函数的作用是用来判断,如果加入x来参与模拟配对,会不会使匹配数增多(能不能给x男孩找到对象)
bool find(int x)
{
    //遍历自己喜欢的女孩
    for(int i = h[x] ; i != -1 ;i = ne[i])
    {
        int j = e[i];
        if(!st[j])//如果在这一轮模拟匹配中,这个女孩尚未被预定
        {
            st[j] = true;//那x就预定这个女孩了
            //如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩(原来的男朋友还有备胎选)。配对成功,更新match
            if(!match[j]||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 二分图的最大匹配】

给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

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

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

输入格式

第一行包含三个整数 n1、 n2 和 m。

接下来 m 行,每行包含两个整数 u和 vv,表示左半部点集中的点 u 和右半部点集中的点 vv 之间存在一条边。

输出格式

输出一个整数,表示二分图的最大匹配数。

数据范围

1≤n1,n2≤500,
1≤u≤n1,
1≤v≤n2,
1≤m≤105

输入样例:

2 2 4
1 1
1 2
2 1
2 2

输出样例:

2

【参考代码】

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

using namespace std;

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

int h[N], e[M], ne[M], idx;
int match[N];//匹配数组
bool st[N];//预定女友数组

int n1, n2, m;

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

bool find(int x)
{
    //遍历男孩喜欢的女孩子
    for(int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(!st[j])//如果女孩j没被预定
        {
            st[j] = true;//预定女孩j
            //如果女孩j没有男朋友,或者她原来的男朋友还有其他预定对象(还有备胎)。配对成功
            if(!match[j] || find(match[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 = 1; i <= n1; i ++ )
    {
        //因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
        memset(st, 0, sizeof st);
        if(find(i))
            res ++;
    }
    
    cout << res;
    
    return 0;
}

总结

在理解思路的基础上,学习总结代码!

学习内容源自:
百度百科
acwing算法基础课
哔哩哔哩:无权二分图匹配

注:如果文章有任何错误或不足,请各位大佬尽情指出,评论留言留下您宝贵的建议!如果这篇文章对你有些许帮助,希望可爱亲切的您点个赞推荐一手,非常感谢啦

posted @ 2022-01-15 12:45  时间最考验人  阅读(238)  评论(0编辑  收藏  举报