初见 | 图论 | 二分图初步

前言

数据结构学累了,于是决定回归图论一下,大概的目标就先是二分图,然后过网络流初步。

缺省源

因为挺长的所有下面就不再放了罢。

#include <iostream>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <string.h>
#define Heriko return
#define Deltana 0
#define Romano 1
#define S signed
#define LL long long
#define R register
#define I inline
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
#define ON std::ios::sync_with_stdio(false)
using namespace std;

template<typename J>
I void fr(J &x)
{
    short f=1;
    char c=getchar();
    x=0;
    while(c<'0' or c>'9')
    {
        if(c=='-') f=-1;
        c=getchar();
    }
    while (c>='0' and c<='9') 
    {
        x=(x<<3)+(x<<1)+c-'0';
        c=getchar();
    }
    x*=f;
}

template<typename J>
I void fw(J x,bool k)
{
    if(x<0) putchar('-'),x=-x;
    static short stak[35];
    short top=0;
    do
    {
        stak[top++]=x%10;
        x/=10;
    }
    while(x);
    while(top) putchar(stak[--top]+'0');
    if(k) putchar('\n');
    else putchar(' ');
}

二分图

什么是二分图?

实际上蓝书上的定义看的我挺晕的,这里就采用 OI-Wiki 上的解释:

节点由两个集合组成,且两个集合内部没有边的图。[1]

换句话说,即:

如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。[2]

这里给出一张图来帮助理解一下~

两个框框圈起来的点就是上面所说的两个集合啦(

可以清晰的发现,即使两个集合的点之间有很多边,但是每个集合内部的点都是互相没有连边哒。

如果还是不能理解的话,先大约记住这张图的样子,到了下面的判定部分就会让你恍然大明白。(因为我就是这样ww)

如何判定二分图?

前置芝士

我们先来介绍一个二分图性质,因为这对我们下面的判定非常重要:

二分图不存在长度为奇数的环。[1]

于是我们用性质就反过来判定,即:

存在长度为奇数的环的图一定不是二分图。

不过要注意一下,这里说的长度是构成一个环的边数和,

如果边有不同的权值,那么我们称这些值的和为这个环的权值和,别搞混了。

当然你也可以理解为这里的边都默认长度为 1 ~

这条性质的证明太显然了不会描述 QwQ,不过为了帮助理解,这里就简单口胡一下罢。

口胡

大约是反证的思想(?)

我们假设我们有一个二分图,并且已经分为了两个集合。

假设每一条边都是在两个集合之间的,且完全符合二分图的定义(两个集合内部没有边),并且我们有长度为奇数的环。

然后我们在这个奇数环上任选一个结点开遍历这张图,考虑如何才能回到这个起始集合。

显然的是我们走偶数次才能够回到原来的集合(或者说是回到起始的点),但是这个环的长度是奇数,因此我们是走不回原来的集合的。

那么要不然这个图不是二分图,要不然这不是环......

于是我们推出了矛盾,口胡毕。

然后我们就可以愉快的判定了(

具体实现

一个显而易见的暴力做法是枚举答案集合,但也很显然的是这玩意复杂度爆炸,于是我们考虑 DFS。

我们利用刚才的性质,加以染色的思想进行 DFS:

假如我们在 DFS 染色的过程中遇不到矛盾的情况,那么这就是一个二分图;反之此图有奇数环,不是二分图。

在这里演示的时候我们使用蓝色红色来对结点进行染色。

过程演示

我们先选取一个结点对他进行染色:

然后我们对和它相连的结点涂上相反的颜色,目前还没有矛盾:

然后对下一个同集合的结点进行同样的染色过程:

发现 DFS 到的是个新的点,染上不同的颜色:

重复上面的操作,发现这是一个二分图(即没有矛盾):

有矛盾的图就不放了(因为我不想画了ww)

最后用动图再展示一边全过程罢:

Code

有了上面的图,代码就不难理解了,这个 DFS 的时间复杂度是 \(O(nm)\) 的。


bool DFS(int x,int c)
{
    color[x]=c;
    for(int i=head[x];i;i=r[i].nex)//邻接表存图。
    {
        int y=r[i].to;
        if(!color[y]) 
            {
                if(!DFS(y,(c==1?2:1))) Heriko Deltana;//这里 1 代表蓝色,2代表红色。
            }
        else if(color[y]==color[x]) return 0;
    }
    return 1;
}

这里推荐一个例题,虽然不是很板子。

P1330 封锁阳光大学

P1330 封锁阳光大学 [普及+/提高]

思路简述

这道题的数据如果是个联通的无向图,那么可以直接用二分图的判定 DFS 染色来做,但是数据并没有保证图是联通的。

但是我们可以仍然去参考染色的思想,实际上 DFS 根本不用改太多,只不过每次染色的时候加上当前是染了什么颜色即可。

然后每次 ans 加上两个颜色中比较小的那个即可(因为我们要最少的河蟹)。

(以上摘自我的杂题录 [简单记录 | #99]

Code

template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}

CI MXX=1e5+5;

int n,m;

struct node
{
    int to,nex;
}

r[MXX];

int head[MXX],cnt,ans;

int color[MXX],c1,c2;

I void add(int x,int y)
{
    r[++cnt].to=y;
    r[cnt].nex=head[x];
    head[x]=cnt;
}

bool DFS(int x,int c)
{
    color[x]=c;
    if(color[x]==1) ++c1;
    else ++c2;
    for(R int i=head[x];i;i=r[i].nex)
    {
        int y=r[i].to;
        if(!color[y]) 
            {if(!DFS(y,(c==1?2:1))) Heriko Deltana;}
        else if(color[y]==color[x]) Heriko Deltana;
    }
    Heriko Romano;
}

bool flag;

int u,v;

S main()
{
    fr(n),fr(m);
    for(R int i=1;i<=m;++i)
    {
        fr(u),fr(v);
        add(u,v);
        add(v,u);
    }
    for(R int i=1;i<=n;++i)
    {
        if(!color[i]) 
        {
            c1=c2=0;
            if(!DFS(i,1))
            {
                puts("Impossible");
                Heriko Deltana;
            }
            ans+=Hmin(c1,c2);
        }
    }
    fw(ans,1);
    Heriko Deltana;
}

二分图最大匹配

前备芝士

为了理解二分图最大匹配,我们需要先定义一些东西。

匹配

在图论中,一个「匹配」(matching)是一个边的集合 \(S\),其中任意两条边都没有公共顶点。例如,图 3、图 4 中红色的边就是图 2 的匹配。[2]

[2]

匹配边 & 匹配点

所有的边 \(u\in S\),就是匹配边;同理,所有匹配边的端点即为匹配点。

举个栗子,对于图 4,红边都是对于图 2 的匹配边,而红边所连的结点都是匹配点。

非匹配边 & 非匹配点

所有不是匹配边的边是非匹配边;同理,不是匹配点的都是非匹配点。

一句非常废话的废话,但是感觉我不管怎么描述都是说废话(

最大匹配

一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 4 是一个最大匹配,它包含 4 条匹配边。[2]

增广路

如果在二分图中存在一条连接两个非匹配点的路径 \(path\),使得非匹配边与匹配边在 \(path\) 上交替出现,那么称 \(path\) 是匹配 \(S\) 的一条增广路,也称交错路。[3]

增广路显然是有如下的性质:

  1. 长度为奇数。

  2. \(1,3,5,\cdots,len\) 条边是非匹配边,反之则都是匹配边。

由性质 2 可得,当我们把所有的边的状态取反之后,这仍是一组匹配,并且匹配的边数++,于是我们可以想到利用这个性质去找最大的匹配。

于是就有推论:

二分图的一组匹配 \(S\) 是最大匹配,当且仅当图中不存在 \(S\) 的增广路。[3]

然后我们就可以引出我们求二分图最大匹配的算法。

匈牙利算法

因为利用了上面那个推论,所以又被称为增广路算法。

思路简述

这个算法就是利用上面的推论和性质来不断的接近最大匹配,主要过程如下:

  1. \(S\) 为空集,即现在图上的所有边都为非匹配边。

  2. 寻找增广路 \(path\),然后把路径上的边的状态取反,得到一个更大的匹配 \(S'\)

  3. 重复步骤 2 直到图上不存在增广路。

其实找增广路的算法本质上很暴力:DFS 每个结点,不断尝试,如果存在更优的解法就改变当前的决策。

匈牙利算法的正确性是基于贪心的,这个贪心算法的重要特性是:在一次寻找增广路的 DFS 过程中,当一个结点成为匹配点后,它最多会因为后面寻找增广路时被更改匹配对象,即:

如果某个选择已经被搭配,就尝试将原有搭配拆散重新搭配,直到找到第一个可行的搭配。

下面放上一张 \(\texttt{Dfkuaid}\) 的图来大约描述一下这个过程:

[4]

Code

是板子题的 Code 呢:[ 洛谷 P3386 ]

CI MXX=5e4+5,NXX=1005;

struct node
{
    int to,nex;
}

r[MXX];

int head[NXX],ans,match[NXX],cnt,y;

bool vis[NXX];

I void add(int x,int y)
{
    r[++cnt].to=y;
    r[cnt].nex=head[x];
    head[x]=cnt;
}

bool DFS(int x)
{
    for(R int i=head[x];i;i=r[i].nex)
        if(!vis[y=r[i].to])
        {
            vis[y]=1;
            if(!match[y] or DFS(match[y]))
            {
                match[y]=x;
                Heriko Romano;
            }
        }
    Heriko Deltana;
}

int n,m,e,u,v;

S main()
{
    fr(n),fr(m),fr(e);
    for(R int i=1;i<=e;++i)
    {
        fr(u),fr(v);
        add(u,v);
    }
    for(R int i=1;i<=n;++i) 
    {
        mst(vis,0);
        if(DFS(i)) ++ans;
    }
    fw(ans,1);
    Heriko Deltana;
}

转为网络最大流模型

爬了,还不会网络流,学完回来填个坑 🕳 罢。

来填坑啦~

二分图匹配建模

二分图匹配的建模有以下两个要素:

  • “0 要素”:结点能分为两个独立的集合,且每个集合内有 0 条边。(也就是满足这是一个二分图)

  • “1 要素”:每个结点只能与 1 条匹配边相连

下面给出一道例题:

P1129 矩阵游戏 [ZJOI2007]

P1129 矩阵游戏 [ZJOI2007][提高+/省选-]

思路简述

是一道显然的二分图最大匹配!考虑到我们每次能够调换的是行和列,于是我们在输入时遇到 1 时就把 i 和 j+n 建边,然后正常的跑匈牙利,最后判断一下 ans>=n 是否成立即可。

(以上摘自我的杂题录 [简单记录 | #100]

Code

CI MXX=20005;

struct node
{
    int nex,to;
}

r[MXX<<1];

int T,n,head[MXX],cnt,match[MXX];

I void add(int x,int y)
{
    r[++cnt].to=y;
    r[cnt].nex=head[x];
    head[x]=cnt;
}

bool vis[MXX];

bool DFS(int x)
{
    for(R int i=head[x];i;i=r[i].nex)
    {
        int y=r[i].to;
        if(!vis[y])
        {
            vis[y]=1;
            if(!match[y] or DFS(match[y]))
            {
                match[y]=x;
                Heriko Romano;
            }
        }
    }
    Heriko Deltana;
}

int x,ans;

S main()
{
    fr(T);
    while(T--)
    {
        mst(head,0);cnt=0;  mst(match,0);ans=0;
        fr(n);
        for(R int i=1;i<=n;++i)
            for(R int j=1;j<=n;++j)
            {
                fr(x);
                if(x) add(i,j+n);
            }
        for(R int i=1;i<=n;++i)
        {
            mst(vis,0);
            if(DFS(i)) ++ans;
        }
        if(ans>=n) puts("Yes");
        else puts("No");
    }
    Heriko Deltana;
}

End

芜湖,写完了,欢迎各位大佬纠错捉虫!

参考资料

posted @ 2021-06-29 21:16  HerikoDeltana  阅读(215)  评论(2编辑  收藏  举报