Bipartite Matching and Hungarian Algorithm.

1. 二分图的基本概念

G=(V,E)是一个无向图,如果顶点集合V可分割为两个互不相交的子集X,Y,并且边集合E中每条边关联的两个顶点分别属于这两个子集,则称图G为一个二分图(bipartite graph)

下图给出了一个二分图G的例子,其顶点集合V={X1,X2,X3,X4,Y1,Y2,Y3,Y4},边集合E={(X1,Y1),(X1,Y2),(X2,Y2),(X2,Y3),(X3,Y1),(X3,Y2),(X4,Y3)}

2. 图的匹配

对于二分图G的一个子图M,如果子图M的边集合E中的任意两条边都不依附于同一个顶点,则称M是一个匹配(matching)。匹配建立了二分图的两个子集中部分顶点的一一对应关系。

下图给出了二分图G的两个匹配M1M2:

M1={(X1,Y2),(X3,Y1),(X4,Y3)}M2={(X1,Y1),(X2,Y3)}

匹配M的边集合所关联的点称为饱和点,其余点称为非饱和点。如上图中:

在图G的所有匹配中,所含匹配边数最多的匹配称为图G最大匹配。 如果图G的某个匹配使得图G的所有顶点都是饱和点,则该匹配是一个完美匹配。完美匹配一定是最大匹配,但并非每个图都存在完美匹配。

定义图G中的一条路径,如果该路径中的边在属于匹配M和不属于匹配M中交替出现,则称其为交错路。下图分别给出了匹配M3={(X2,Y2),(X4,Y3)}M4={(X1,Y2),(X2,Y3)}的一条交错路:

对于匹配M的一条交错路,如果该交错路的起点和终点都是匹配M的非饱和点,则该交错路为增广路。下图给出了匹配M3={(X2,Y2),(X4,Y3)}的一条增广路。

增广路具有以下结论:

  1. 增广路的路径边数为奇数,且第一条边和最后一条边都不属于匹配M
  2. 增广路中匹配边的数量总是比非匹配边的数量多1条;
  3. 将增广路中边的匹配方式取反,会得到一个更大的匹配M,匹配数+1
  4. 匹配M是图G的最大匹配等价于不存在M的增广路。

3. 二分匹配与匈牙利算法

二分匹配(Bipartite Matching)问题即寻找二分图G中的最大匹配M

匈牙利算法(Hungarian Algorithm)最早是由匈牙利数学家D.Kőnig用来求矩阵中0元素个数的一种方法,由此他证明了“矩阵中独立0元素的最多个数等于能覆盖所有0元素的最少直线数”。

1955年W.W.Kuhn在求解指派问题时引用了这一结论, 并提出根据二分图的增广路寻找最大匹配的算法,仍然称为“匈牙利算法”。

指派问题是人员调度问题中的经典问题:由m个人完成n项工作,第i个人完成第j项工作的成本为cij,由此构造成本矩阵C。现需确定指派方案使得完成任务的总成本最低。特别地,若工作成本cij=1,则成本矩阵C可以padding为图的连接矩阵,这等价于图论中的最大匹配问题。

匈牙利算法的思路为:

  1. 设二分图G的初始匹配M为空Φ
  2. 寻找一条增广路,通过取反操作构造一个更大的匹配M代替匹配M
  3. 重复上述步骤直至寻找不到增广路。

在实现时可以通过深度优先搜索进行增广路径的选择。

下面给出匈牙利算法的简单实现:

def Hungarian(G):
    N, M = G.shape # G[u,v]=1表示结点u,v之间有连接
    matched = [None] * N # 记录已匹配的结点

    def found(x): # 通过DFS寻找增广路
        for m in range(M):
            if G[x, m] != 1 or visited[m]:
                continue
            visited[m] = 1
            if not matched[m] or found(matched[m]):
                matched[m] = x
                return True
        return False

    for n in range(N):
        visited = [0] * N # 记录已访问的结点
        found(n)
    return [*enumerate(matched)] # 返回(v,u)

使用scipy库可以方便地实现匈牙利算法:

from scipy.optimize import linear_sum_assignment
match_index_list = linear_sum_assignment(cost_matrix)
# ([X1, X2, ...], [Y1, Y2, ...])