图论-二分图专题

本文深入探讨了图论中的二分图概念,包括定义、性质和判定方法。重点讲解了二分图的最大匹配、匈牙利算法以及KM算法,详细阐述了增广路、交错路的概念,并介绍了最小覆盖和最大独立集。通过实例解析了匈牙利算法的匹配过程和KM算法的原理,帮助理解如何在带权二分图中求解最大权匹配问题。
摘要由CSDN通过智能技术生成

图论-二分图专题(二分图匹配、匈牙利算法、KM算法)

😊 | Powered By HeartFireY | BG
📕 | 需要的前导知识:图论基础

一、二分图-定义与基本性质

1.二分图-定义

二分图(Bipartite graph),也称作”二部图“。简单的来说,二分图由两个点集和他们之间的映射(连边)构成,点集内部的点相互独立,不存在连边。

换而言之,我们可以对一个图采取某种方案,得到一张所有节点被划分为两个集合,且两个集合内部没有边的图。

1

如上图-(示例一)所示,就是一张构造好的二分图, S E T 1 SET_1 SET1 S E T 2 SET_2 SET2集合之间存在连边关系,两个集合的内部不存在任何边关系,集合内点相互独立。

2.二分图-性质

根据二分图的定义,我们可以发现对于任意节点一定满足:

图中任意一条边一定满足起点是从 S E T 1 SET_1 SET1出发,然后到达 S E T 2 SET_2 SET2,也就是说,任意一条边的起止点一定属于不同的集合

格局上面的性质,我们可以进一步推广:二分图不存在长度为奇数的环

因为每一条边都是从一个集合走到另一个集合,只有走偶数次才可能回到同一个集合。

3.二分图-判定

显然对于给定的图,枚举答案集合是过于朴素的做法,应该考虑更高效的做法。

我们可以直接 d f s dfs dfs或者 b f s bfs bfs来遍历整张图,遍历过程中不断的查环的长度,如果查到了奇环,那么说明一定不是二分图。

二、二分图-匹配、最大匹配、完美匹配和最优匹配

1.匹配

在图论中,一个"匹配"就是一个边集,这个边集中的任意两条边都没有公共顶点

匹配点: 位于匹配(边集)中某条连接的点,也就是已经匹配的点;

匹配边: 位于匹配(边集)中的某条边,也就是已经匹配的边;

未匹配点、未匹配边: 顾名思义,尚未匹配的点和边

如下图所示就是上文二分图示例一的一个匹配。

2

显然,红色的边是匹配边,红边连接的点为匹配点…

2.最大匹配

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

3

如上图所示,两个图片都为示例一的匹配,其中Graph_2为示例一的最大匹配。

3.完美匹配

如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。图 4 是一个完美匹配。显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。但并非每个图都存在完美匹配。

显然,Graph_2并不是一个完美匹配。

我们通过一些示例继续说明完美匹配和最大匹配的区别:

4

P_1所示,假设女生和男生之间的连线表示彼此之间存在好感。现在要求求最多多少对男女匹配,使得每对匹配的男生女生相互喜欢。那么这就是一个求最大匹配的问题。

那么我们根据最大匹配的定义可以知道:P_2不是最大匹配,因为并不满足”最多“;而P_3是满足题意的一个匹配。

我们换一种问问题的方式:是否可能让所有男生和女生两两配对,使得每对男生女生之间都相互喜欢?那么这就是一个求完美匹配的问题。

通过上面的示例,我们不难发现:完美匹配一定是最大匹配,而最大匹配不一定就是完美匹配

4.最优匹配

最优匹配又称为带权最大匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大

三、二分图-增广路、交错路

这两个概念将为求解二分图最大匹配的匈牙利算法做前置知识铺垫。

1.交错路

交错路又称交替路,从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径。

2.增广路

从一个未匹配点出发,走交替路,如果途径另一个未匹配点(不能是出发的点),则这条交替路称为增广路(Agumenting Path)。

容易发现:增广路有一个重要特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的状态交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多了 1 条。

四、二分图-最小覆盖、最大独立集

1.最小覆盖

二分图的最小覆盖分为最小顶点覆盖和最小路径覆盖:

  1. 最小顶点覆盖:指最少的顶点数使得二分图G中的每条边都至少与其中一个点相关联,二分图的最小顶点覆盖数=二分图的最大匹配数

  2. 最小路径覆盖:也称为最小边覆盖,是指用尽量少的不相交简单路径覆盖二分图中的所有顶点。

  3. 二 分 图 的 最 小 路 径 覆 盖 数 = ∣ V ∣ − 二 分 图 的 最 大 匹 配 数 二分图的最小路径覆盖数=|V|-二分图的最大匹配数 =V

2.最大独立集

最大独立集是指寻找一个点集,使得其中任意两点在图中无对应边。对于一般图来说,最大独立集是一个NP完全问题,对于二分图来说:最大独立集=|V|-二分图的最大匹配数

五、二分图-匈牙利算法

1.简介

匈牙利算法的主要用于解决二分图的最大匹配、最小覆盖问题。

我们不难发现,对于求解最大匹配问题,实际上就是在求解在二分图中最多能找到多少条没有公共端点的边。在下面的样例中,我们会详细的分析这个过程。

OI-WIKI上对二分图的最大匹配算法中提到了"增广路算法"。从实现原理上来看,个人觉得跟匈牙利算法就是同一个算法。

2.过程分析

我们首先继续引用男女配对这个经典的例子:如图L_1所示,图中的各个男生和女生之间存在着相互的好感。我们首先从第一个女生开始配对(如图L_2所示),按照边的顺序我们会选择第一个男生,此时没有发生任何冲突,因此我们继续:

5

如图L_3所示,我们给第二个女生进行配对,此时按顺序首先选择第一个男生,此时我们可以清楚的发现:产生冲突了。那么我们该怎样找到一种通用的方法解决这个冲突呢?我们使用匈牙利算法的思路继续分析:寻找增广路

6

如上图L_5所示,按照增广路的定义,我们从第二个女生(未匹配点)出发走到第一个男生(已匹配点),再经过第一个女生(未匹配点)到达第三个男生(未匹配),这样一条路径便是一条交错路,而到达了第一个为匹配点时的路便是增广路。找到这样一条增广路后,我们对增广路进行一个"取反"操作,也就是让匹配边变为未匹配边,未匹配边变为匹配边,如图L_6所示。经此变化后,我们不难发现匹配的问题已经得到了解决。

这个过程很好的利用了增广路的性质:非匹配边比匹配边多一条,中间的匹配节点不存在其他相连的匹配边。因此我们进行取反操作不会影响匹配的性质,同时通过交换我们使得匹配边和非匹配边的数量互换,也就是匹配边比原来多出一条。匹配性质不变+匹配边多一条,一次来继续匹配的过程,便是匈牙利算法的核心思想。

我们可以继续根据以上思路匹配,直至所有的点都被匹配。当然,有些图可能并非所有的点都能够得到匹配。

在这里插入图片描述

如图L_11便是一个 最大匹配&&完美匹配

3.实现

const int MAXN = GRAPH_SIZE_L, MAXM = GRAPH_SIZE_W;

int M, N;            //M, N分别表示左、右侧集合的元素数量
int g[MAXM][MAXN]; //邻接矩阵存图
int p[MAXN];         //记录当前右侧元素所对应的左侧元素
bool vis[MAXN];      //记录右侧元素是否已被访问过
bool dfs(int i){
    for (int j = 1; j <= N; ++j)
        if (g[i][j] && !vis[j]){ //有边且未访问
            vis[j] = true;                 //记录状态为访问过
            if (p[j] == 0 || dfs(p[j])){//如果暂无匹配,或者原来匹配的左侧元素可以找到新的匹配
                p[j] = i;    //当前左侧元素成为当前右侧元素的新匹配
                return true; //返回匹配成功
            }
        }
    return false; //循环结束,仍未找到匹配,返回匹配失败
}
int Hungarian(){
    int cnt = 0;
    for (int i = 1; i <= M; ++i){
        memset(vis, 0, sizeof(vis)); //vis标记清零
        if (dfs(i)) cnt++;
    }
    return cnt;
}

注:参考匈牙利算法详解实现

六、二分图-KM算法

在使用匈牙利算法解决二分图最大匹配问题的时候,我们认为:图中男女生之间只要存现相互喜欢的关系,那么喜欢的程度就是相同的。也就是对于双方而言,只要符合关系的匹配都可以在相同程度上接受。但现在我们的问题发生了变化:男女生之间喜欢的程度是不同的,换而言之,女生对不同的男生可能存在相互喜欢的关系,但喜欢的程度时不同的。换到图论里的说法就是,图中的边是具有边权的。

1.简介

KM算法用于解决"带权最大匹配问题",也就是带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大

可以认为KM算法是基于匈牙利算法的一种拓展延深算法,其核心思想之一仍是不断地寻找增广路、取反,直至实现最大匹配。

y在本部分,我们依然通过匈牙利算法中男女生匹配的例子进行说明,不同于匈牙利算法中的样例,我们需要给图加边权便于说明。

2.详解-过程

如图K_1所示是一个男女配对例子的延申,我们对男女之间喜欢的程度进行了定义和标注:我们首先对图中的点赋一个点权,由于我们以左侧的女生为基准进行匹配,于是我们对左侧点集中的每个点赋值为其所连接的最大边权,右侧的点集中的每个点都赋初始值为 0 0 0,如图L_2所示。

8

然后我们开始进行匹配,我们匹配的原则是:只与权重相同的边匹配,若是找不到边匹配,对此条路径的所有左边顶点 − 1 -1 1,右边顶点 + 1 +1 +1,再进行匹配,若还是匹配不到,重复 + 1 +1 +1 − 1 -1 1操作。如上图K_3所示,我们首先给 a a a进行配对,匹配到男生 D D D。没有发生什么冲突。然后我们继续给 b b b执行匹配。如图K_4所示,我们继续给 b b b分配对象,按照权重,我们给她分男生 C C C,仍然没有产生冲突。

于是我们继续给 c c c匹配对象,按照最大权值应该给他分配 C C C,如图K_5所示,显然此时产生了冲突。

如何解决冲突?我们首先需要来考虑寻找最优匹配的过程,也就是尽可能让每个女生都找到她们各自喜欢程度最高的男生,但显然这样是会产生冲突的,就像这里情形一样, b b b c c c心中最心仪的对象都是 C C C,这时候我们只能退而求其次,让其中一个女生选择另外的对象。这样的操作肯定会降低整体的喜欢程度累加和(图的边权和),但为了整体的喜欢程度累加和最大,我们需要选择一个合适的策略,使得这个累加和降低的幅度最小,换而言之,对于二分图中产生冲突的点,我们需要考虑如何更换匹配使边权和降低的更少一些。

KM算法的核心之一便是基于这个思想:如图K_6所示,我们让产生冲突的左侧点权 − 1 -1 1,让产生冲突的右侧点权 + 1 +1 +1,现在我们再继续进行分析:

若是左侧集合有 n n n个顶点参与运算,则右侧就有 n − 1 n-1 n1个顶点参与运算,整体累加和下降了 1 × ( n − ( n − 1 ) ) = 1 1 \times (n - (n - 1)) = 1 1×(n(n1))=1,而对于 b b b来说, b C bC bC本来为可匹配的边 ( 3 + 0 = 3 ) (3 + 0 = 3) (3+0=3),现在仍为可匹配边 ( 2 + 1 = 3 ) (2 + 1 = 3) (2+1=3),对于 c c c来说, c C cC cC本来为可匹配的边 ( 6 + 0 = 0 ) (6 + 0 = 0) (6+0=0),现在仍为可匹配的边 ( 5 + 1 = 6 ) (5 + 1 = 6) (5+1=6),我们通过上述操作,为 b b b增加了一条可匹配的边 b A bA bA,为 c c c增加了一条可匹配的边 c B cB cB

9

于是我们给 c c c更换匹配边,换到 c B cB cB上,如图K_7所示,此时的 c c c的匹配问题就得到了解决。

我们继续给 d d d匹配,但是发现选择最大边权不满足匹配规则 ( 4 + 1 ≠ 4 ) (4 + 1 \neq 4) (4+1=4),因此将 d d d的点权值 − 1 -1 1变为 3 3 3(图K_8所示),此时再与 C C C匹配,仍然冲突。那么此时我们需要采取匈牙利算法的策略:找交错路,走增广路至为匹配点如图K_9所示,然后取反,可完成匹配。

10

值得一提的是,如果 b b b无法匹配(假设顶点 A A A被占用),那么我们应该继续换到 a a a上匹配,如果 a a a没有符合条件的匹配点则再执行 − 1 -1 1操作…直至匹配完成。这便是KM算法的完全步骤。

3.详解-原理

我们需要引入一些概念来解释算法的原理,这里引用OI-WIKI的解释。

原文地址:二分图最大权匹配 - OI Wiki

(1).可行顶标

给每个节点 i i i 分配一个权值 l ( i ) l(i) l(i),对于所有边 ( u , v ) (u,v) (u,v) 满足 w ( u , v ) ≤ l ( u ) + l ( v ) w(u,v) \leq l(u) + l(v) w(u,v)l(u)+l(v)

(2).相等子图

在一组可行顶标下原图的生成子图,包含所有点但只包含满足 w ( u , v ) = l ( u ) + l ( v ) w(u,v) = l(u) + l(v) w(u,v)=l(u)+l(v) 的边 ( u , v ) (u,v) (u,v)

(3).定理与推广

对于某组可行顶标,如果其相等子图存在完美匹配,那么,该匹配就是原二分图的最大权完美匹配。"

这里引用OI-WIKI的证明:

考虑原二分图任意一组完美匹配 M M M,其边权和为

v a l ( M ) = ∑ ( u , v ) ∈ M w ( u , v ) ≤ ∑ ( u , v ) ∈ M l ( u ) + l ( v ) ≤ ∑ i = 1 n l ( i ) val(M) = \sum_{(u,v)\in M} {w(u,v)} \leq \sum_{(u,v)\in M} {l(u) + l(v)} \leq \sum_{i=1}^{n} l(i) val(M)=(u,v)Mw(u,v)(u,v)Ml(u)+l(v)i=1nl(i)

任意一组可行顶标的相等子图的完美匹配 M ′ M' M 的边权和

v a l ( M ′ ) = ∑ ( u , v ) ∈ M l ( u ) + l ( v ) = ∑ i = 1 n l ( i ) val(M') = \sum_{(u,v)\in M} {l(u) + l(v)} = \sum_{i=1}^{n} l(i) val(M)=(u,v)Ml(u)+l(v)=i=1nl(i)

即任意一组完美匹配的边权和都不会大于 v a l ( M ′ ) val(M') val(M),那个 M ′ M' M 就是最大权匹配。

有了这个定理,我们的目标就是透过不断的调整可行顶标使得相等子图是完美匹配

因为两边点数相等,假设点数为 n n n l x ( i ) lx(i) lx(i) 表示左边第 i i i 个点的顶标, l y ( i ) ly(i) ly(i) 表示右边第 i i i 个点的顶标, w ( u , v ) w(u,v) w(u,v) 表示左边第 u u u 个点和右边第 v v v 个点之间的权重。

首先初始化一组可行顶标,例如 l x ( i ) = max ⁡ 1 ≤ j ≤ n { w ( i , j ) } ,   l y ( i ) = 0 lx(i) = \max_{1\leq j\leq n} \{ w(i, j)\},\, ly(i) = 0 lx(i)=max1jn{w(i,j)},ly(i)=0

然后选一个未匹配点,如同最大匹配一样求增广路。找到增广路就增广,否则,会得到一个交错树。

S S S T T T 表示二分图左边右边在交错树中的点, S ′ S' S T ′ T' T 表示不在交错树中的点。

在相等子图中:

  • S − T ′ S-T' ST 的边不存在,否则交错树会增长。
  • S ′ − T S'-T ST 一定是非匹配边,否则他就属于 S S S

假设给 S S S 中的顶标 − a -a a,给 T T T 中的顶标 + a +a +a,可以发现

  • S − T S-T ST 边依然存在相等子图中。
  • S ′ − T ′ S'-T' ST 没变化。
  • S − T ′ S-T' ST 中的 l x + l y lx + ly lx+ly 有所减少,可能加入相等子图。
  • S ′ − T S'-T ST 中的 l x + l y lx + ly lx+ly 会增加,所以不可能加入相等子图。

所以这个 a a a 值的选择,显然得是 S − T ′ S-T' ST 当中最小的边权,

a = min ⁡ { l x ( u ) + l y ( v ) − w ( u , v ) ∣ u ∈ S , v ∈ T ′ } a = \min \{ lx(u) + ly(v) - w(u,v) | u\in{S} , v\in{T'} \} a=min{lx(u)+ly(v)w(u,v)uS,vT}

当一条新的边 ( u , v ) (u,v) (u,v) 加入相等子图后有两种情况

  • v v v 是未匹配点,则找到增广路
  • v v v S ′ S' S 中的点已经匹配

这样至多修改 n n n 次顶标后,就可以找到增广路。

每次修改顶标的时候,交错树中的边不会离开相等子图,那么我们直接维护这棵树。

我们对 T T T 中的每个点 v v v 维护

s l a c k ( v ) = min ⁡ { l x ( u ) + l y ( v ) − w ( u , v ) ∣ u ∈ S } slack(v) = \min \{ lx(u) + ly(v) - w(u,v) | u\in{S} \} slack(v)=min{lx(u)+ly(v)w(u,v)uS}

所以可以在 O ( n ) O(n) O(n) 算出顶标修改值 a a a

a = min ⁡ { s l a c k ( v ) ∣ v ∈ T ′ } a = \min \{ slack(v) | v\in{T'} \} a=min{slack(v)vT}

交错树新增一个点进入 S S S 的时候需要 O ( n ) O(n) O(n) 更新 s l a c k ( v ) slack(v) slack(v)。修改顶标需要 O ( n ) O(n) O(n) 给每个 s l a c k ( v ) slack(v) slack(v) 减去 a a a。只要交错树找到一个未匹配点,就找到增广路。

一开始枚举 n n n 个点找增广路,为了找增广路需要延伸 n n n 次交错树,每次延伸需要 n n n 次维护,共 O ( n 3 ) O(n^3) O(n3)

4.实现

同样借用一下OI-WIKI的模板~,后面会更新一下自己写的。

template <typename T>
struct hungarian {  // km
  int n;
  vector<int> matchx;  // 左集合对应的匹配点
  vector<int> matchy;  // 右集合对应的匹配点
  vector<int> pre;     // 连接右集合的左点
  vector<bool> visx;   // 拜访数组 左
  vector<bool> visy;   // 拜访数组 右
  vector<T> lx;
  vector<T> ly;
  vector<vector<T> > g;
  vector<T> slack;
  T inf;
  T res;
  queue<int> q;
  int org_n;
  int org_m;

  hungarian(int _n, int _m) {
    org_n = _n;
    org_m = _m;
    n = max(_n, _m);
    inf = numeric_limits<T>::max();
    res = 0;
    g = vector<vector<T> >(n, vector<T>(n));
    matchx = vector<int>(n, -1);
    matchy = vector<int>(n, -1);
    pre = vector<int>(n);
    visx = vector<bool>(n);
    visy = vector<bool>(n);
    lx = vector<T>(n, -inf);
    ly = vector<T>(n);
    slack = vector<T>(n);
  }

  void addEdge(int u, int v, int w) {
    g[u][v] = max(w, 0);  // 负值还不如不匹配 因此设为0不影响
  }

  bool check(int v) {
    visy[v] = true;
    if (matchy[v] != -1) {
      q.push(matchy[v]);
      visx[matchy[v]] = true;  // in S
      return false;
    }
    // 找到新的未匹配点 更新匹配点 pre 数组记录着"非匹配边"上与之相连的点
    while (v != -1) {
      matchy[v] = pre[v];
      swap(v, matchx[pre[v]]);
    }
    return true;
  }

  void bfs(int i) {
    while (!q.empty()) {
      q.pop();
    }
    q.push(i);
    visx[i] = true;
    while (true) {
      while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v = 0; v < n; v++) {
          if (!visy[v]) {
            T delta = lx[u] + ly[v] - g[u][v];
            if (slack[v] >= delta) {
              pre[v] = u;
              if (delta) {
                slack[v] = delta;
              } else if (check(v)) {  // delta=0 代表有机会加入相等子图 找增广路
                                      // 找到就return 重建交错树
                return;
              }
            }
          }
        }
      }
      // 没有增广路 修改顶标
      T a = inf;
      for (int j = 0; j < n; j++) {
        if (!visy[j]) {
          a = min(a, slack[j]);
        }
      }
      for (int j = 0; j < n; j++) {
        if (visx[j]) {  // S
          lx[j] -= a;
        }
        if (visy[j]) {  // T
          ly[j] += a;
        } else {  // T'
          slack[j] -= a;
        }
      }
      for (int j = 0; j < n; j++) {
        if (!visy[j] && slack[j] == 0 && check(j)) {
          return;
        }
      }
    }
  }

  void solve() {
    // 初始顶标
    for (int i = 0; i < n; i++) {
      for (int j = 0; j < n; j++) {
        lx[i] = max(lx[i], g[i][j]);
      }
    }

    for (int i = 0; i < n; i++) {
      fill(slack.begin(), slack.end(), inf);
      fill(visx.begin(), visx.end(), false);
      fill(visy.begin(), visy.end(), false);
      bfs(i);
    }

    // custom
    for (int i = 0; i < n; i++) {
      if (g[i][matchx[i]] > 0) {
        res += g[i][matchx[i]];
      } else {
        matchx[i] = -1;
      }
    }
    cout << res << "\n";
    for (int i = 0; i < org_n; i++) {
      cout << matchx[i] + 1 << " ";
    }
    cout << "\n";
  }
};
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HeartFireY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值