复习心得 - 二分图

· · 算法·理论

以前写过一篇匈牙利(增广路)算法,现在系统复习一下二分图。

定义

就是两排点,同一排之间两两不连边的图。

判断

可能不完全连通,从每一个新的点开始黑白染色判断即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m,col[N];
vector<int>e[N];
bool is;
bool dfs(int p,int pre){
    if(!pre)
        col[p]=1;
    else col[p]=3-col[pre];
    bool res=1;
    for(auto v:e[p]){
        if(!col[v])
            res&=dfs(v,p);
        else if(col[v]==col[p])
            return 0;
    }
    return res;
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    is=1;
    for(int i=1;i<=n;i++)if(!col[i])
        is&=dfs(i,0);
    cout<<(is?"Yes":"No");
    return 0;
}

最大匹配

详见我写的匈牙利算法。

这里介绍一些别的方法。

这里补充一点概念:

二分图 G 的顶点集分为两部分 V_1 与 V_2,其中满足 |V_1|\leq |V_2|,并选择边 E'\in E。

  1. 如果 E' 中任意两条边没有相同节点,则 E' 为 G 的匹配,设其为 M。
  2. 我们取 \max |E'| 的 E' 为最大匹配,其中 |E'| 为 G 的匹配数。
  3. 要是 |E'|=|V_1|,即 V_1 中所有点都找到匹配了,那么这个匹配为完备匹配。

当然我们还有概念(我之前看不懂的):

  1. 交错路径:在 G 上的一条路径 P 其中 P_i\to P_{i+1} (1<i< |P|) 边与 P_{i-1}\to P_i 边其中一条 \in M,一条 \notin M。

    • 例子:当前有匹配 \{1_1\to 1_2,2_1\to 3_2\},那么当前路径 P=1_1\to 1_2\to 2_1\to3_2 是交错路径。
  2. 【重要】增广路径:首先 P 是交错路径。然后 P_1 与 P_{|P|}(起点与终点)都未被匹配。 这个东西的作用是 M\oplus P(集合异或)更优。

    • 例子:P=3_1\to3_2{\color{red}\to}2_1\to1_2{\color{red}\to}1_1\to 2_2,你会发现这其实是上面例子加上了两个新的点两条边,其中原有匹配就是 \{1_1\to 1_2,2_1\to 3_2\}。
    • 证明 M\oplus P 更优:
    • 在增广路径 P 中,是匹配的边比不是匹配的边多 1 条(匹配边间隔一个非匹配边)被影响的边是在路径上的匹配边({\color{red}\to})它们不再是匹配边,而其他的边(\to)成为了匹配边。
    • 原先的匹配边为 n 条,则非匹配边为 n-1 条,增加新的两条边让非匹配边变成 n-1+2=n+1 条,比原先的匹配边多了 1 条。
    • 综上,增广路径 P 可以替换原先 M,变为 M\oplus P,会有 1 贡献。

我们发现了增广路径的性质之后,自然想到如何匹配。

我们发现匈牙利本质就是找增广路。虽然这很慢,lol。

考虑枚举一个点作为增广路的起点(p\in V_2),然后你神奇的发现我们的匈牙利(增广路算法)本质就是不停扩展增广路。

就结束了。

代码

水文尼蜡。

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10;
int n,m,ee,U,V,npy[N],ncnt;
int found[N];
bool e[N][N];
bool dfs(int p){
    for(int v=1;v<=m;v++)if(e[p][v]&&!npy[v]){
        npy[v]=1;
        if(!found[v]||dfs(found[v])){
            found[v]=p;
            return 1;
        }
    }
    return 0;   
}
int main(){
    cin>>n>>m>>ee;
    for(int i=1;i<=ee;i++){
        cin>>U>>V;
        e[U][V]=1;
    }
    for(int i=1;i<=n;i++){
        memset(npy,0,sizeof(npy));
        if(dfs(i))++ncnt;
    }
    cout<<ncnt;
    return 0;
}

最后,祝愿广大 OIer 早日找到 NPY (虽然这不可能

——From Anguei

trick: 把有向图 n 拆 2n 个点一个入一个出跑二分图