二分图匹配 2016.8.6

来源:互联网 发布:刀剑少女2网络出错 编辑:程序博客网 时间:2024/04/30 14:16

参考:《算法竞赛入门经典:训练指南》刘汝佳 陈锋 编著

《图论算法理论、实现及应用》

一、边独立集(匹配)

设无向图为 G(V, E) ,边的集合 E* ⊆ E,若 E* 中的任何两条边均不相邻,则称 E* 为 G 的边独立集(edge independent set),也称 E* 为 G 的匹配(matching)

所谓任何两条边均不相邻,通俗的讲,就是任何两条边都没有公共顶点

例如,在图(a)中,取 E* = {e1, e4, e7},则 E* 就是图 G 的一个边独立集,因为 E* 中每两条边都没有公共顶点

注意

在无向图中存在将尽可能多的、相互独立的边包含到边的集合 E* 中的问题,所以边独立集有极大和最大的概念

最小边独立集的概念是没有意义的,因为对任何一个无向图 G(V, E) ,取 E* = ∅ (空集),总是满足边独立集的定义

若在 E* 中加入任意一条边所得到的集合都不是匹配,则称 E* 为极大匹配

边数最多的匹配称为最大匹配

最大匹配的边数称为边独立数匹配数,记为 β1(G),简记为 β1

在图(a)中,{e2, e6},{e3, e5} 和 {e1, e4, e7} 都是极大匹配,{e1, e4, e7} 是最大匹配。因此 β1 = 3

在图(b)中,{e1, e4},{e2, e3} 和 {e4, e8} 都是极大匹配,也都是最大匹配,因此 β1 = 2


以下几个概念都是针对无向图 G(V, E) 中一个给定的匹配 M 而言的

在无向图 G 中,若边 (u, v) ∈ M,则称顶点 u 与 v 被 M 所匹配

设 v 是图 G 的一个顶点,如果 v 与 M 中的某条边关联,则称 v 为 匹配 M 的盖点

如果 v 不与任意一条属于匹配 M 的边关联,则称 v 为匹配 M 的未盖点

所谓盖点,就是被匹配中的边盖住了,而未盖点就是没有被匹配 M 中的边“盖住” 的顶点

例如,在图(a)所示的无向图中,取定 M = {e1, e4},M 中的边用粗线标明,则顶点 v1 与 v2 被 M 所匹配

v1、v2、v3 和 v4 是 M 的盖点,v5 和 v6 是 M 的未盖点

而在图(b)中,取定 M = {e1, e4, e7},则 G 中不存在未盖点



例 1 飞行员搭配问题 1 -- 最大匹配问题

飞行大队有若干个来自各地的飞行员,专门驾驶一种型号的飞机,这种飞机每架有两个飞行员

由于种种原因,例如互相配合的问题,有些飞行员不能在同一架飞机上飞行,问如何搭配飞行员,才能使出航的飞机最多

为简单起见,设有 10 个飞行员,图中的 v1, v2, … v10 就代表这 10 个飞行员

如果两个人可以同机飞行,就在他们之间连一条线,否则就不连

图中的 3 条粗线代表一种搭配方案

由于一个飞行员不能同时派往两架飞机,因此任何两条粗线不能有公共端点

因此该问题就转化为:如何找一个包含最多边的匹配,这个问题就是图的最大匹配问题



二、二分图(二部图)匹配问题

例 2 飞行员搭配问题 2 -- 二分图的最大匹配问题

在例 1 中,如果飞行员分成两部分,一部分是正驾驶员,一部分是副驾驶员

如何搭配正副驾驶员才能使得出航飞机最多的问题可以归结为一个二分图上的最大匹配问题

例如,假设有 4 个正驾驶员,有 5 个副驾驶员,飞机必须要有一名正驾驶员和一名副驾驶员才能起飞

正驾驶员和副驾驶员之间存在搭配的问题

图中, x1, x2, x3, x4 表示 4 个正驾驶员, y1, y2, y3, y4, y5 表示 5 个副驾驶员

正驾驶员之间不能搭配,副驾驶员之间也不能搭配,所以这是一个二分图

图中的 4 条粗线代表一种搭配方案。这个问题实际上是求一个二分图的最大匹配

为方便叙述,我们总是把二分图的两个结点集称为 X 和 Y,有时也称为左边和右边,其中左边是 X 集,右边是 Y 集,如图所示

这个问题可以用网络流解决,但用增广路算法更加简洁


1、完美匹配

对于一个图 G 与给定的一个匹配 M,如果图 G 中不存在 M 的未盖点,即所有点都是匹配点(匹配中的某一条边的端点),则称匹配 M 为图 G 的完美匹配(perfect matching)

例如,在图(a)所示的无向图,取 M = {e1, e4, e7},则 M 是 G 的一个完美匹配

而在图(b)中,不可能有完美匹配,因为对任何匹配都存在未盖点


2、二分图的完备匹配与完美匹配

设无向图 G(V, E) 为二分图,它的两个顶点集合为 X 和 Y,且 |X| ≤ |Y|,M 为 G 中的一个最大匹配,且 |M| = |X|,则称 M 为 X 到 Y 的二分图 G 的完备匹配

若 |X| = |Y|,该完备匹配覆盖住 G 的所有顶点,则该完备匹配也是完美匹配

例如,如图所示的 3 个二分图,其中图(a)和图(b)中取定的匹配 M 都是完备匹配,而图(c)中不存在完备匹配

二分图完备匹配的一个应用例子是:某公司有工作人员 x1, x2, …, xm,他们去做工作 y1, y2,y3, …, yn, n>m

每个人适合做其中一项或几项工作,问能否恰当地安排使得每个人都分配到一项合适的工作

3、最佳匹配

继续对上面的应用例子进行深化:

工作人员可以做各项工作,但效率未必一致,现在需要制定一个分工方案,使公司的总效益最大,这就是最佳分配问题

设 G(V, E) 为加权二分图,它的两个顶点集合反别为 X = {x1, x2, ..., xm}、Y = {y1, y2, ..., yn}

W(xi, yk) ≥ 0 表示工作人员 xi 做工作 yk 时的效益,权值总和最大的完备匹配称为二分图的最佳匹配

4、交错路

设 P 是图 G 的一条路径,M 是图 G 中一个给定的匹配,如果 P 的任意两条相邻的边一定是一条属于匹配 M 而另一条不属于 M,则称 P 是关于 M 的一条交错路

例如,在图(a)所示的图中,取定 M = {e4, e6, e10},则图(b)、(c)所示的路径都是交错路


特别地,如果路径 P 仅含一条边,那么无论这条边是否属于匹配 M,P 一定是一条交错路

5、增广路

对于一个给定的图 G 和匹配 M,两个端点都是未盖点的交错路称为关于 M 的增广路

例如,上面的图(b)所示的交错路的两个端点 v2、v11 都是匹配 M 的未盖点,所以这条路是增广路,而上图(c)所示的交错路不是增广路

特别地,如果两个未盖点之间仅含一条边,那么单单这条边也组成一条增广路

在增广路中,非匹配边比匹配边多一条


增广路的作用是改进匹配

假设我们已经找到一个匹配,如何判断它是不是最大匹配呢?

看增广路

如果有一条增广路,那么把此路上的匹配边的非匹配边互换,得到的匹配比刚才多一条边

反过来,如果找不到增广路,则当前匹配就是最大匹配

这就是增广路定理,即一个匹配是最大匹配的充分必要条件是不存在增广路

注意:这个充要条件适合于任意图,不仅仅是二分图


有个很有意思的游戏可以加深对增广路算法的理解

这是一个无向图上的游戏,Alice和 Bob 轮流操作,Alice 先走

第一次可以任选一个点放一枚棋子,以后每次把棋子移动到一个相邻点上,并把棋子原先所在的点删除,谁不能移动就算输

若双方都采取最优策略, 谁将取胜?

解:如果有完美匹配,则 Alice 输,因为 Bob 只需沿着匹配边走即可

否则 Alice 赢,因为任意求一个最大匹配,Alice 把棋子放在任一个未盖点上,Bob 都只能把它移动到已盖点上(否则得到增广路)

Alice 沿着匹配边移动,下一步 Bob 又只能把它移到另一个已盖点上,只要 Bob 能移动,Alice 就能移动

6、增广路算法

根据增广路定理,最大匹配可以通过反复找增广路来求解

如何找增广路呢?

根据定义,首先需要选一个未盖点 u 作为起点。不失一般性,设这个 u 是 X 的结点

接下来,需要选一个从 u 出发的非匹配边 (u, v) ,到达 Y 结点 v

如果 v 是未盖点,说明我们成功地找到了一条增广路,否则说明 v 是匹配点,下一步得走匹配边

因为一个匹配点恰好和一个匹配边邻接,这一步没得选择

设匹配点 v 邻接的匹配边的另一端为 left[v] ,则可以理解为从 u 直接走到了 left[v],而这个 left[v] 也是一个 X 结点

如果始终没有找到未盖点,最后会扩展出一颗所谓的匈牙利树


这样,我们得到了一个算法,即每次选一个未盖点 u 进行 dfs

注意,如果找不到以 u 开头的增广路,则换一个未盖点进行 dfs ,且以后再也不从 u 出发找增广路

换句话说,如果以后存在一个从 u 出发的增广路,那么现在就找得到


三、二分图最佳完美匹配

假定有一个完全二分图 G,每条边有一个权值(可以是负数)

如何求出权值和最大的完美匹配?

Kuhn - Munkres 算法(KM 算法)可以解决这个问题


为了学习这个算法,我们先来看两个概念

首先定义可行顶标(feasible vertex labling),它是一个结点函数 l,使得对于任意弧 (x, y),有 l(x) + l(y) ≥ w(x, y)

相等子图(equality subgraph)是图 G 的生成子图,包含所有点,但只包含满足 l(x) + l(y) = w(x, y) 的所有边 w(x, y)

有一个极为重要的定理

如果相等子图有完美匹配,则该匹配是原图的最大权匹配


这个定理其实很容易证明

设 M* 是相等子图的完美匹配,根据定义有 M* 的权和等于所有点的顶标之和

另一方面,任取 G 的一个完美匹配 M ,由于 M 中的边只满足不等式 w(x, y) ≤ l(x) + l(y),M 的权值不超过所有顶标之和,也就是 M* 的权和

这样看来,问题的关键就是寻找好的可行顶标,使相等子图有完美匹配


算法的思路是这样的:

任意构造一个可行顶标(比如,X 结点的顶标为它出发所有边的最大权值, Y 结点的顶标为 0),然后求相等子图的最大匹配

如果存在完美匹配,算法终止,否则修改顶标使得相等子图的边变多,有更大机会存在完美匹配


如何修改顶标呢?

我们仍然从匈牙利树下手

设匈牙利树中 X 结点集为 S,Y 结点集为 T,则 S 到 T‘ 没有边(否则匈牙利树可以继续生长);S’ 到 T 的边都是非匹配边

如果把 S 中所有点的顶标同时减少一个相同的整数 a,则 S 到 T‘ 中可能会有新边进入相等子图

为了保证 S-T 的匹配边不离开相等子图,还要把 T 中所有点的顶标同时增加 a


不难发现,应取 a = min{l(x) + l(y) - w(x, y) | x in S, y in T’},因为如果 S 中每个顶标的实际减少值比这个值小,则不会有新边进入;如果比这个值大顶标将变得不可行


设边 (x, y) 进入相等子图,有以下两种情况:

情况一:y 是未盖点,则找到增广路

情况二:y 和 S 中的点 z 匹配,则把 z 和 y 分别加入 S 和 T 中


不难发现,每次修改顶标要么找到增广路,要么使匈牙利树增加两个结点,因此一共需要 O(n^2) 次修改顶标操作。这样,问题关键就在于快速修改顶标


最朴素的做法,是枚举 S 和 T 中的每个元素,根据定义计算最小值,每次修改的时间为 O(n^2),总时间为 O(n^4),实际效果并没有理论上那么糟糕


HDU 1533 Going Home

POJ 2195 Going Home

KM 算法的变形,求权值和最小的完美匹配

将可行顶标定义为:w(x, y) ≤ l(x) + l(y)

修改顶标时,a 取 min{w(x, y) - l(x) - l(y) | x in S, y in T'}

S 中所有点的顶标同时增加 a,T 中所有点的顶标同时减少 a


这道题数据好像有点弱,怎么感觉像是水过去的啊 QAQ,如果有朋友发现问题还望不吝赐教,thx

#include <iostream>#include <cstdio>#include <cstring>#include <cstdlib>#include <algorithm>#include <queue>#include <vector>#include <stack>#include <map>#include <cmath>#include <cctype>#include <bitset>using namespace std;typedef long long ll;typedef unsigned long long ull;typedef unsigned int uint;typedef pair<int, int> Pair;const ull mod = 1e9 + 7;const int INF = 0x7fffffff;const int maxn = 250;int W[maxn][maxn];      //边权int Count = 0;int Lx[maxn], Ly[maxn]; //顶标int Left[maxn];         //Left[i] 为右边第 i 个点的编号bool S[maxn], T[maxn];  //S[i] 和 T[i] 为左/右第 i 个点是否已标记int N, M;char s[110][110];struct cordinate {    int x, y;    cordinate(int x_t, int y_t) : x(x_t), y(y_t) {}};vector<cordinate> men, house;bool Match(int i);void Update(void);void KM(void);int main(){#ifdef __AiR_H    freopen("in.txt", "r", stdin);#endif // __AiR_H    while (scanf("%d%d", &N, &M) != EOF && !(N == 0 && M == 0)) {        men.clear(), house.clear();        Count = 0;        for (int i = 0; i < N; ++i) {            scanf("%s", s[i]);            for (int j = 0; j < M; ++j) {                if (s[i][j] == 'm') {                    ++Count;                    men.push_back(cordinate(i, j));                } else if (s[i][j] == 'H') {                    house.push_back(cordinate(i, j));                }            }        }        for (int i = 0; i < Count; ++i) {            for (int j = 0; j < Count; ++j) {                W[i+1][j+1] = abs(men[i].x - house[j].x) + abs(men[i].y - house[j].y);            }        }        KM();        int ans = 0;        for (int i = 1; i <= Count; ++i) {            ans += Lx[i] + Ly[i];        }        printf("%d\n", ans);    }    return 0;}bool Match(int i){    S[i] = true;    for (int j = 1; j <= Count; ++j) {        if (Lx[i]+Ly[j] == W[i][j] && !T[j]) {            T[j] = true;            if (Left[j] == 0 || Match(Left[j])) {                Left[j] = i;                return true;            }        }    }    return false;}void Update(void){    int Min = INF;    for (int i = 1; i <= Count; ++i) {        if (S[i]) {            for (int j = 1; j <= Count; ++j) {                if (!T[j]) {                    Min = min(Min, W[i][j] - Lx[i] - Ly[j]);                }            }        }    }    for (int i = 1; i <= Count; ++i) {        if (S[i]) {            Lx[i] += Min;        }        if (T[i]) {            Ly[i] -= Min;        }    }}void KM(void){    for (int i = 1; i <= Count; ++i) {        Left[i] = Lx[i] = Ly[i] = 0;        for (int j = 1; j <= Count; ++j) {            Lx[i] = min(Lx[i], W[i][j]);        }    }    for (int i = 1; i <= Count; ++i) {        while (1) {            for (int j = 1; j <= Count; ++j) {                S[j] = T[j] = false;            }            if (Match(i)) {                break;            }            Update();        }    }}

#include <iostream>#include <cstdio>#include <cstring>#include <cstdlib>#include <algorithm>#include <queue>#include <vector>#include <stack>#include <map>#include <cmath>#include <cctype>#include <bitset>using namespace std;typedef long long ll;typedef unsigned long long ull;typedef unsigned int uint;typedef pair<int, int> Pair;const ull mod = 1e9 + 7;const int INF = 0x7fffffff;const int maxn = 250;int W[maxn][maxn];      //边权int Count = 0;int Lx[maxn], Ly[maxn]; //顶标int Left[maxn];         //Left[i] 为右边第 i 个点的编号bool S[maxn], T[maxn];  //S[i] 和 T[i] 为左/右第 i 个点是否已标记int N, M;char s[110][110];int Min = INF;struct cordinate {    int x, y;    cordinate(int x_t, int y_t) : x(x_t), y(y_t) {}};vector<cordinate> men, house;bool Match(int i);void Update(void);void KM(void);int main(){#ifdef __AiR_H    freopen("in.txt", "r", stdin);#endif // __AiR_H    while (scanf("%d%d", &N, &M) != EOF && !(N == 0 && M == 0)) {        men.clear(), house.clear();        Count = 0;        for (int i = 0; i < N; ++i) {            scanf("%s", s[i]);            for (int j = 0; j < M; ++j) {                if (s[i][j] == 'm') {                    ++Count;                    men.push_back(cordinate(i, j));                } else if (s[i][j] == 'H') {                    house.push_back(cordinate(i, j));                }            }        }        for (int i = 0; i < Count; ++i) {            for (int j = 0; j < Count; ++j) {                W[i+1][j+1] = abs(men[i].x - house[j].x) + abs(men[i].y - house[j].y);            }        }        KM();        int ans = 0;        for (int i = 1; i <= Count; ++i) {            ans += Lx[i] + Ly[i];        }        printf("%d\n", ans);    }    return 0;}bool Match(int i){    S[i] = true;    for (int j = 1; j <= Count; ++j) {        if (!T[j]) {            int t = W[i][j] - Lx[i] - Ly[j];            if (t == 0) {                T[j] = true;                if (Left[j] == 0 || Match(Left[j])) {                    Left[j] = i;                    return true;                }            } else {                Min = min(Min, t);            }        }    }    return false;}void Update(void){    for (int i = 1; i <= Count; ++i) {        if (S[i]) {            Lx[i] += Min;        }        if (T[i]) {            Ly[i] -= Min;        }    }}void KM(void){    for (int i = 1; i <= Count; ++i) {        Left[i] = Lx[i] = Ly[i] = 0;        for (int j = 1; j <= Count; ++j) {            Lx[i] = min(Lx[i], W[i][j]);        }    }    for (int i = 1; i <= Count; ++i) {        while (1) {            for (int j = 1; j <= Count; ++j) {                S[j] = T[j] = false;            }            if (Match(i)) {                break;            }            Update();        }    }}


0 0