【算法】Hungarian Algorithm 匈牙利算法

最近在看文章时看到了匈牙利算法,之前没怎么了解过这个算法,所以查了查资料把他这个算法总结一下写在这里了

匈牙利算法是一种在多项式时间内求解指派问题的算法,其核心思想是通过构造增广路径来寻找最大匹配,进而得到最优指派方案。

1 用匈牙利算法解决指派问题

举例一个典型的指派问题:假设现在有\(n\)个学生,以及\(n\)个任务,学生\(i\)完成任务\(j\)的代价为\(c_{ij}\)(构成成本矩阵\(C\)),现在,我希望设计一种任务指派的方式,使得每个学生完成一个任务,并且总体代价最小

匈牙利算法解决上述指派问题的四个步骤:

步骤 1:行减法(Subtract row minima)

对于成本矩阵\(C\)的每一行,找出该行中的最小元素,然后从该行的每个元素中减去这个最小元素。这一步的目的是减少成本矩阵中的非零元素,使得每一行至少有一个零元素。这不会改变最优解,因为减去一个常数不会影响相对成本。

步骤 2:列减法(Subtract column minima)

类似地,对于矩阵的每一列,找出该列中的最小元素,然后从该列的每个元素中减去这个最小元素。这一步的目的是进一步减少成本矩阵中的非零元素,使得每一列至少有一个零元素。这同样不会改变最优解。

步骤 3:用最少的线覆盖所有零(Cover all zeros with a minimum number of lines)

用最少数量的水平线和垂直线覆盖经过步骤 1 和 2 处理后的矩阵中的所有零(注:这里可以用最大流问题的Ford Fulkerson算法来解决,当然也有更简单的方法,这里使用了Ford Fulkerson算法,后面将会结合最大流问题更详细地解释一下什么是增广路)这个。如果需要n条线来覆盖所有零(n是成本矩阵的维度),则在这些零中存在最优指派方案,算法结束。

如果少于n条线就能覆盖所有零,则继续执行步骤 4。

步骤 4:创建额外的零(Create additional zeros)

找出在步骤 3 中未被线覆盖的最小元素(记为k),从所有未被覆盖的元素中减去k,同时将所有被两条线覆盖的元素加上k。

然后重复步骤 3 和步骤 4,直到找到最优指派方案。

最后步骤3中给出最优指派方案的方法是:在最终的矩阵中,从每个行和列中各选择一个零,使得这些零互不重叠(即没有两个零在同一行或同一列),这些零对应的位置即为最优指派方案。具体来说,可以按照以下步骤进行:

  • 从第一行开始,找到第一个未被标记的零,将其标记为选中,并将该零所在列的所有其他零标记为不可选。
  • 移动到下一行,重复上述操作,直到所有行都被处理完毕。
  • 对于每一列,检查是否有选中的零。如果有,则该列对应的任务被分配给了该零所在行对应的人;如果没有,则该列的任务未被分配。
    这样,就可以得到一个使得总成本最小的最优指派方案。

2 最大流问题与Ford Fulkerson算法

最大流问题是图论中的一个经典优化问题,目标是在给定一个流网络(即一个带有容量的有向图)中,找到从源点 s(source) 到汇点 t(sink) 的最大流量。

2.1 最大流算法的实际例子

想象一下,你正在管理一个城市的道路网络,这个网络由若干个交叉路口(节点)和道路(边)组成。每条道路都有一个最大容量,表示在单位时间内可以通过的最大车流量。

假设你有以下道路网络:

  • 源点A到交叉路口B的道路容量为10辆车/小时。
  • 源点A到交叉路口C的道路容量为5辆车/小时。
  • 交叉路口B到汇点D的道路容量为8辆车/小时。
  • 交叉路口B到交叉路口C的道路容量为2辆车/小时。
  • 交叉路口C到汇点D的道路容量为7辆车/小时。

最大流问题就是要找出在这一给定的道路容量限制下,从源点A到汇点D能够通过的最大车流量。

2.2 Ford Fulkerson算法与增广路

Ford Fulkerson算法是一种解决最大流问题的经典算法。它的核心思想是通过不断寻找从源点到汇点的增广路径来增加流量,直到无法再找到增广路径为止。

所谓增广路就是一条从源点到汇点的路径,路径上的每条道路都有剩余(道路)容量可以利用。每找到一条增广路,我们就从源点派出车辆数尽可能多的一队车流开去汇点(将这条增广路吃干抹净!),然后再寻找下一条依然有剩余容量的增广路,继续将其“吃干抹净”。直到这样“吃干抹净”所有的增广路,我们就找到了这张图的最大流量。

让我们用刚才的车流量例子来说明这个算法:

  • 初始化流量:一开始,所有道路的流量都为0。

  • 寻找增广路径:第一次,我们可以找到路径A -> B -> D。这条路径上的最小剩余容量是8(A到B的容量为10,B到D的容量为8)。我们将这条路径上的流量增加8。更新后的流量:A -> B: 8/10,B -> D: 8/8。

  • 再次寻找增广路径:第二次,我们可以找到路径A -> C -> D。这条路径上的最小剩余容量是5(A到C的容量为5,C到D的容量为7)。我们将这条路径上的流量增加5。更新后的流量:A -> C: 5/5,C -> D: 5/7。

  • 继续寻找增广路径:第三次,我们可以找到路径A -> B -> C -> D。这条路径上的最小剩余容量是2(A到B的剩余容量为2,B到C的容量为2,C到D的剩余容量为2)。我们将这条路径上的流量增加2。更新后的流量:A -> B: 10/10,B -> C: 2/2,C -> D: 7/7。

  • 无法再找到增广路径:此时,我们无法再找到从源点A到汇点D的增广路径,因为所有可能的路径上的某条道路的剩余容量都为0。

  • 计算最大流:最大流等于所有从源点出发的道路的流量之和,即8 + 5 + 2 = 15辆车/小时。

3 匈牙利算法示例代码

from collections import deque

import numpy as np


def bfs(graph, s, t, parent):
    """
    使用广度优先搜索 (BFS) 寻找增广路径。

    :param graph: 邻接矩阵表示的图,graph[u][v] 表示从节点 u 到节点 v 的剩余容量。
    :param s: 源点。
    :param t: 汇点。
    :param parent: 用于记录路径的父节点数组。
    :return: 如果找到增广路径,返回 True;否则返回 False。
    """
    visited = [False] * len(graph)
    queue = deque()
    queue.append(s)
    visited[s] = True

    while queue:
        u = queue.popleft()
        for ind, val in enumerate(graph[u]):
            if not visited[ind] and val > 0:
                queue.append(ind)
                visited[ind] = True
                parent[ind] = u
                if ind == t:
                    return True
    return False


def ford_fulkerson(graph, source, sink):
    """
    Ford-Fulkerson 算法用于计算最大流。

    :param graph: 邻接矩阵表示的图,graph[u][v] 表示从节点 u 到节点 v 的剩余容量。
    :param s: 源点。
    :param t: 汇点。
    :return: 图的最大流和每条边上的实际流量。
    """
    parent = [-1] * len(graph)
    max_flow = 0

    while bfs(graph, source, sink, parent):
        path_flow = float("Inf")
        s = sink
        while s != source:
            path_flow = min(path_flow, graph[parent[s]][s])
            s = parent[s]

        max_flow += path_flow

        v = sink
        while v != source:
            u = parent[v]
            graph[u][v] -= path_flow
            graph[v][u] += path_flow
            v = parent[v]

    return max_flow


def hungarian_algorithm(cost_matrix):
    n = cost_matrix.shape[0]

    # 步骤1: 减去行最小值
    for i in range(n):
        cost_matrix[i, :] -= np.min(cost_matrix[i, :])

    # 步骤2: 减去列最小值
    for j in range(n):
        cost_matrix[:, j] -= np.min(cost_matrix[:, j])

    # 步骤3:通过构建含有零的矩阵所对应二分图的邻接矩阵解决“用最少的线覆盖所有零”问题
    graph = np.zeros((2 * n + 2, 2 * n + 2), dtype=int)
    for i in range(1, n + 1):
        graph[0, i] = 1  # 从源点到所有的行添加一条容量为1的路线
    for i in range(n + 1, 2 * n + 1):
        graph[i, 2 * n + 1] = 1  # 从所有的列到汇点添加一条容量为1的路线
    for i in range(n):
        for j in range(n):
            if cost_matrix[i, j] == 0:
                graph[i + 1, n + j + 1] = 1  # 每个零元素所在的行和列之间添加一条容量为1的路线

    max_flow = ford_fulkerson(graph, 0, 2 * n + 1)

    if max_flow == n:
        # 找到最优指派方案,算法成功结束,计算并返回指派方案
        assignment = np.zeros(n, dtype=int)
        for i in range(1, n + 1):
            for j in range(n + 1, 2 * n + 1):
                if graph[i, j] == 0 and graph[j, i] == 1:
                    assignment[i - 1] = j - n - 1
        return assignment
    else:
        # 步骤4:创建额外的零,然后重复步骤3
        row_cover = np.zeros(n, dtype=bool)
        col_cover = np.zeros(n, dtype=bool)

        for i in range(n):
            for j in range(n):
                if cost_matrix[i, j] == 0 and not row_cover[i] and not col_cover[j]:
                    row_cover[i] = True
                    col_cover[j] = True

        min_val = np.min(cost_matrix[~row_cover, :][:, ~col_cover])
        cost_matrix[~row_cover, :] -= min_val
        cost_matrix[:, col_cover] += min_val

        return hungarian_algorithm(cost_matrix)


# 示例
cost_matrix = np.array(
    [
        [7, 514, 114, 14, 1],
        [9, 12, 6, 8, 1],
        [13, 4, 10, 7, 1],
        [6, 8, 11, 9, 2],
        [1, 2, 3, 4, 5],
    ]
)

original_cost_matrix = cost_matrix.copy()
assignment = hungarian_algorithm(cost_matrix)
print(f"成本矩阵{original_cost_matrix}")
print(cost_matrix)
total_cost = np.sum(
    original_cost_matrix[np.arange(original_cost_matrix.shape[0]), assignment]
)

print("指派方案:")
for i, task in enumerate(assignment):
    print(f"学生 {i + 1} 被指派任务 {task + 1}")
print(f"总成本: {total_cost}")

4 参考资料:

posted @ 2025-08-06 16:00  print(alphi)  阅读(266)  评论(0)    收藏  举报