二分图算法及模板

gao79138 / 2023-09-01 / 原文

二分图算法及模板

1. 二分图算法要解决的问题

img

    二分图算法要解决的问题有两个:
        1.  用dfs来判断二分图(也称为染色法)。
        2.  求二分图的最大匹配。(采用匈牙利算法)。

2. 染色法的基本思想

    二分图的定义:
        如果某一个图存在如下性质:
            1.  可以将节点分为两个集合.
            2.  节点之间的边都在集合之间,集合内部是没有边的。
        满足如上性质的图,就称为二分图。
    二分图的基本性质:
        如果一个图是二分图,当且仅当图中不含奇数环。其中,奇数环的定义为:环路,边数为奇数。

img

    下面对如上的性质进行简单的证明:
        如果一个图中含有奇数环,那么边数一定为奇数,节点数一定为偶数个。又因为,这个图是一个环,所以在偶数点当中一定有两个点相等。因此,实际存在的点只有奇数个。那么在进行染色的过程中,假设我们选取x号点作为1颜色。遍历x号点的所有邻接点,只能将其设定为2颜色。(根据二分图的定义,如果存在a-b这条边,a属于1集合,b只能属于2集合。集合之间可以存在边,集合内不允许有边)再遍历x号点的邻接点的邻接点,将其设定为1颜色....。由于节点个数只有奇数个,边数也为奇数,构成环路,那么在对其进行染色的过程中,一定会有一个点既被染成1颜色也被染成2颜色。这样的话,就矛盾了。请见上图。
        上图中,第一个点既被染成了1颜色,也被染成了2颜色。这就是冲突的情况。因此,如果图中含有奇数环,那么这个图一定不是二分图。
        我们根据上述的例子,观察到:如果在染色的过程中,出现了冲突的情况,那么这个图一定不是二分图。同理,如果在染色的过程中,没有出现冲突,那么这个图一定就是二分图。同时,我们也可以观察到:在进行染色的过程中,如果这个图是二分图的话,那么一个点与其邻接点的颜色一定相反,如果相同的话,一定冲突。(上述的图示也清楚的说明了这一点)因此,我们可以根据这样的性质来进行代码的编写。

img
img

    染色法的基本流程:
        1.  首先遍历每一个点。
        2.  在遍历点的过程中,利用dfs来对点进行染色。这样做的效果就是:该点所在的连通块都会进行染色。
        3.  如果该点(a)没有进行染色的话,那么为其指定一个初始颜色(假设这里是1)。然后通过dfs找到它的邻接点(b)。
        4.  如果a的临界点b也没有进行染色的话,那么为其指定颜色。(如果a是1,那么b就是2)指定颜色之后,再通过dfs进行遍历,直到将其连通块都染上色为止。
        5.  在染色的过程中,可能会出现如下情况:
            5.1 所有节点都正常进行染色,没有发生冲突,那么就代表此图为二分图。
            5.2 如果在染色的过程中,发生了冲突,那么代表此图不是二分图。

3. 染色法的模板

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

// 参数: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] == -1)
        {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;
    }

    return true;
}

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

4. 染色法的例题

https://www.acwing.com/problem/content/862/
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 100010;
const int M = 200010;
int e[M],ne[M],h[N],idx = 0;
int n,m;
//代表该点的颜色
int color[N];

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

//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]){
            //进行染色,如果染色过程中,没有发生冲突,返回true,否则返回false
            //染色过程中,发生了冲突
            if(!dfs(j,3-c)){
                return false;
            }
        }else{
            //如果该点已经染色了,判断:
            //如果某点与其邻接点的颜色相同,那么冲突了
            if(color[u] == color[j]){
                return false;
            }
        }
    }
    return true;
}

int main(){
    memset(h,-1,sizeof(h));
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
        add(b,a);
    }
    //代表图是否为二分图(1颜色和2颜色)
    bool flag = true;
    for(int i=1;i<=n;i++){
        //如果该点没有进行染色的话
        if(!color[i]){
            //dfs的返回值代表染色过程中,是否发生了冲突,如果没有发生冲突,true,反之为false。
            if(!dfs(i,1)){
                //发生冲突,该图不是二分图
                flag = false;
                break;
            }
        }
    }
    if(flag == true){
        printf("Yes");
    }else{
        printf("No");
    }
    
    return 0;
}

5. 匈牙利算法的基本思路

img
img

    匈牙利算法用于求二分图的最大匹配。所谓二分图的最大匹配就是指:
        在给定的二分图下,寻找到这样的一条边(匹配)。边上的两个节点均满足除了这条边(匹配)之外,没有别的边(匹配)指向自己或从自己发出。这样的边就称之为二分图的一个匹配。匈牙利算法的目的就是求二分图中最大能有多少个这样的边。这就是二分图的最大匹配问题。
        对应于上述的图,红色就是匹配,绿色或没有颜色就是不匹配。上图的最大匹配为4。
        举一个例子:如果节点a和节点b邻接,节点a与节点c邻接。意味着有两条边:a-b,a-c。那么,如果a和b构成了匹配,那么a-c这条边就不是匹配。因为,a已经和b匹配了,a不能在跟c匹配。
    匈牙利算法的证明是非常困难的,这里不进行讲述。想知道的,可以参考算法导论这本书。
    给定一个二分图,匈牙利算法的执行过程如下:
        1.  对某一个集合的节点进行遍历。
        2.  在遍历的过程中,假设遍历的点为x,找到x所邻接的点a。这时会有如下情况:
            3.1 所邻接的点a还没有匹配,那么构成匹配,遍历下一个点(x+1)即可。
            3.2 所邻接的点a已经匹配了,那么查看a所匹配的点b是否还有其余的点可以跟它构成匹配。
                3.2.1   如果存在这样的点c可以与b匹配,那么进行b与c匹配,x与a匹配即可。
                3.2.2   如果不存在这样的点c可以与b匹配,那么维持a与b的匹配。
                        继续查看x是否还有其余邻接的点,反复进行上述操作。如果到最后仍是没有节点
                        与x匹配,那么放弃x的匹配,遍历下一个点(x+1)即可。
        3.  在构成匹配的时候,需要进行计数操作,当算法执行完毕之后,输出累加的数量,这就是二分图的最大匹配。我们利用匈牙利算法就可以求出一个二分图的最大匹配。

6. 匈牙利算法的模板

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || 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 ++ ;
}

7. 匈牙利算法的例题

https://www.acwing.com/problem/content/863/
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 510;
const int M = 100010;

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

//代表某个节点所匹配的点
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){
    //遍历x的所有邻接点
    for(int i=h[x];i!=-1;i=ne[i]){
        int j = e[i];
        //如果邻接点j没有考虑过
        if(!st[j]){
            //考虑这个点
            st[j] = true;
            //如果j没有进行匹配或j进行了匹配,但j所匹配的点找到了其他的匹配。
            if(match[j] == 0 || find(match[j])){
                //将x和j进行匹配
                match[j] = x;
                return true;
            }
        }
    }
    //如果遍历了x的所有邻接点,发现都无法跟x匹配
    //x匹配失败
    return false;
}


int main(){
    memset(h,-1,sizeof(h));
    scanf("%d%d%d",&n1,&n2,&m);
    for(int i=1;i<=m;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    //代表最大匹配数量
    int res = 0;
    //从n1集合中,开始匹配
    for(int i=1;i<=n1;i++){
        //由于每次进行匹配的时候,情况都不一样,因此每次都要重置st数组。具体可以看博客的图例。
        memset(st,false,sizeof(st));
        //从节点i开始进行匹配,如果i匹配成功(find(i)的返回值为true),那么将匹配的数量+1。如果为false,则不进行操作。
        if(find(i)){
            res++;
        }
    }
    printf("%d",res);
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。