二分图

二分图:

定义:

二分图的定义就是:所有节点由两个集合组成,而且两个集合内部没有边的图.

换言之,就是存在一种方案让节点划分成满足以上性质的两个集合.

二分图判定:

因为希望两个集合内部没有边,所以试着用黑白两种颜色标记图中的节点,相邻节点标记不同颜色,判断是否会有冲突即可.

二分图最大匹配

定义:

任意两条边没有公共顶点的边 的集合叫做图的一组匹配,在二分图中, 包含边数 最多的一组匹配叫做二分图的最大匹配.

解决:

我们希望匹配数量最多,所以尽量使更多的点能够与二分图另一半相连.

考虑这样一种情况: \(AB,CD\) 形成一个二分图,其中 \(A\) 连接 \(C,D\), \(B\) 连接 \(C\)

一开始,因为遍历的顺序,肯定先 \(A\) 连接 \(C\), 那么 \(B\) 怎么办? 这就需要使用一些算法:

经常使用的是 匈牙利算法.

匈牙利算法:

过程参考了 <<算法竞赛进阶指南>>.

过程为:

  1. 设 \(S=\emptyset\), 一开始所有边都是非匹配边.
  2. 寻找增广路,把路径上所有边的匹配状态 取反 得到一个更大的匹配,重复这个操作,直到找到增广路.

寻找增广路:尝试给每一个左部节点 \(x\) 找到一个匹配的右部节点 \(y\). 过程为:

  1. \(y\) 本身为非匹配点,直接匹配即可.
  2. \(y\) 本身为 \(x'\) 匹配,但从 \(x'\) 出发能找到另一个 \(y'\) 与之匹配,此时路径的 \(x\rightarrow y\) , \(x'\rightarrow y'\) 就是一条增广路.

增广路过程:找 \(y\) 对应连接的 \(x'\) ,看是否能和其他点相匹配就行.

代码:

//P3386 【模板】二分图最大匹配
#include<bits/stdc++.h>

using namespace std;
const int N=1e5+5;
int n,m,e;
vector<int> g[N];
int match[N],vis[N],ans;
bool dfs(int x){
    for(auto y:g[x]){
        if(vis[y]) continue;
        vis[y]=1;
        if(!match[y]||dfs(match[y])){//向下探索对应连接点是否能有增广路
            match[y]=x; return true; 
        }
    }
    return false;
}
int main(){
    cin>>n>>m>>e;
    for(int i=1,x,y;i<=e;i++)scanf("%d%d",&x,&y),g[x].push_back(y);
    for(int i=1;i<=n;i++){memset(vis,0,sizeof(vis)); if(dfs(i)) ans++;}
    cout<<ans<<endl;
    system("pause");
    return 0;
}

例题:[SHOI2002]舞会

题意:

给定 \(n\) 人,有 \(m\) 个限制条件要求 \(A,B\) 不能同时出现,求最多出现人的个数.

分析:

观察数据范围,显然暴力的连接不连通的边肯定过不去。因此,有一个定理:

二分图最大独立集 \(=\) (\(n-\) 最小点覆盖) \(=\) (\(n-\) 二分图最大匹配数)

我们首先把左右两边的限制条件建立无向边,然后进行二分图的染色。

这样染色后,我们得到了颜色为 \(1\) 和颜色为 \(2\) 的点。二分图最大匹配要求 \(1\) 和 \(2\) 颜色的点相互连接的最大个数。

利用匈牙利定理。将颜色为 \(1\) 的点跑匈牙利算法即可。得到最大匹配数量为 \(ans\),,答案即为 \(n-ans\).

代码

#include<bits/stdc++.h>

using namespace std;
const int N=4e4+5;
int n,m;
vector<int> g[1005],a;
int col[N],match[N],vis[N],ans;

void getcol(int x,int c){
    col[x]=c;  
    if(c==1) a.push_back(x);
    for(auto y:g[x]){
       if(!col[y]) getcol(y,3-c);
    }
}
bool dfs(int x){
    for(auto y:g[x]){
        if(vis[y]) continue; vis[y]=1;
        if(!match[y]||dfs(match[y])){
            match[y]=x; return true;
        }
    }
    return false;
}
int main(){
    cin>>n>>m;
    for(int i=1,x,y;i<=m;i++){
        scanf("%d%d",&x,&y); x++,y++;
        g[x].push_back(y); g[y].push_back(x);
    }
    for(int i=1;i<=n;i++) if(!col[i]) getcol(i,1);
    for(int i=0;i<a.size();i++){
        memset(vis,0,sizeof(vis)); if(dfs(a[i])) ans++;
    }
    cout<<n-ans<<endl;

    system("pause");
    return 0;
}

[NOI2009] 变换序列

题解

模型:

二分图匹配有两个要素:

  1. 节点能分成独立的两个集合,每个集合内部没有边
  2. 每个节点只能与一条匹配边连接。

我们需要寻找题目中这种性质,就能利用二分图匹配解决问题。

二分图的最小点覆盖——\(konig\) 定理

找不到那个 \(o\) 上带两个点的先用 \(o\) 吧...

问题:

给定一张二分图,求出一个最小的点集 \(S\) ,使得图中任意一条边都有至少一个端点属于 \(S\) 。这个问题被称为二分图的最小点覆盖。

定理:

二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数

最大匹配是原来二分图边集的一个子集,并且所有边都不相交,所以至少需要从每条匹配边中选出一个端点。

因此,最小点覆盖包含的点数不可能小于最大匹配包含的边数。

如果能对二分图构造出一组点覆盖,其包含的点数等于最大匹配包含的边数,那么就能证明这个定理。

构造方法:

  1. 求出来二分图最大匹配
  2. 从左部每个非匹配点出发,再执行一次 \(dfs\) 寻找增广路的过程(一定会失败),标记访问过的节点。
  3. 左部没有被标记的点和右部被标记的点,就得到了二分图最小点覆盖。

例题:

UVA1194 Machine Schedule

题意:

有两台机器分别有 \(n,m\) 种模式,现在有 \(k\) 个任务,对于每个任务 \(i\) ,给定两个整数 \(a_i,b_i\) ,如果任务在 \(A\) 执行,模式设置为 \(a_i\), \(B\) 为 \(b_i\).

需要求出来 \(a,b\) 转换次数最小值。

分析:

二分图最小覆盖的模型特点则是:每条边有两个端点,两者至少选择一个端点。

在这道题中,每个任务要么在 \(A\) 上使用 \(a_i\), 要么在 \(B\) 上使用 \(b_i\)

我们可以把机器 \(A\) 的 \(m\) 种模式作为 \(m\) 个左部点,\(B\) 作为右部点。

每个任务作为无向边,连接左部的 \(a[i]\) 个节点和右部的第 \(b[i]\) 个节点。

用尽量少的模式,处理出来所有的任务,就是 二分图的最小覆盖

题意:

//UVA1194 Machine Schedule
#include<bits/stdc++.h>

using namespace std;
const int N=1e5+5;
int n,m,k;
vector<int> g[N];
int a[N],b[N],ans,vis[N],match[N];

bool dfs(int x){
    for(auto y:g[x]){
        if(vis[y]) continue;
        vis[y]=1;
        if(!match[y]||dfs(match[y])){
            match[y]=x; return true;
        }
    }
    return false;
}
void init(){
    for(int i=0;i<=1005;i++) g[i]=g[N-1];
    for(int i=0;i<=1005;i++) vis[i]=match[i]=0; ans=0;
}

int main(){
    while(1){
        cin>>n;if(n==0) break; cin>>m>>k;
        for(int i=1,t,x,y;i<=k;i++){
            scanf("%d%d%d",&t,&x,&y); if(x==0||y==0) continue;
            g[x].push_back(y+n); g[y+n].push_back(x);
        }
        for(int i=0;i<n;i++){
            memset(vis,0,sizeof(vis)); if(dfs(i)) ans++;
        }
        printf("%d\n",ans);
        init(); 
    }
    // system("pause");
    return 0;
}

二分图最大独立集:

定义:

给定一张无向图 \(G=(V,E)\),满足以下条件的点集 \(S\) 叫做图的独立集。

  1. \(S\in V\)
  2. \((x,y)\notin E\)

定理:

二分图的点数-二分图最小点覆盖=二分图最大独立集。

这个其实很好证明,直接用就行了。

有向无环图的最小点覆盖

定义:

给定一个有向无环图,要求用尽量少的不相交的简单路径,覆盖无向图的所有顶点,这个问题被称为无向图的 最小路径覆盖

定理:

首先需要把这个有向无环图转化一下:把第 \(i\) 个点拆成 \(i\) 和 \(i+n\),对于每个连边 \((x,y)\) ,连接 \((x,y+n)\),最终得到的图叫做 拆点二分图

最小点覆盖包含的路径条数,等于 \(n\) 减去拆点二分图的最大匹配数。

上一篇:第二章 2.4 可以复用的代码 p43_2_4_match.py


下一篇:JavaScript中的模式匹配