指派问题——匈牙利Hungary算法(用python实现)

注:昨天刚刚看了关于python的关于数组的简单操作,就将匈牙利算法用python实现了以下。其中可能有很多点可以用python中数组本身属性实现,但由于初学,所以不熟悉而导致步骤繁琐的望指出~

1.匈牙利算法的简单例子
(1)矩阵所表示的就是从A点到B所要付出的代价,一般目标函数都是使得代价最小,那么匈牙利算法就是一种精确算法,求解在多个出发点和多个目标点的情况下得出最小代价。约束是一个出发点只能对应一个目标点,在操作矩阵上的表现为某行某列只能选择一个数,即基于所选择的数画十字,这个十字上没有其他任何对应选择。
(2)初始矩阵

            [12  7  9  7  9]
            [ 8  9  6  6  6]
            [ 7 17 12 14  9]
            [15 14  6 11 10]
            [ 4 10  7 10  9]

(3)矩阵每行每列都减去该行该列的最小元素(此处每行每列至少出现一个0)

 [ 5  0  2  0  2]
 [ 2  3  0  0  0]
 [ 0 10  5  7  2]
 [ 9  8  0  5  4]
 [ 0  6  3  6  5]

(4)制定完全分配方案(即每个目标每个地点都被匹配)

  • 从第一行开始,依次检查各行,直到找出只有一个未标记的0元素的一行。【圆括号表示选中标记,双引号表示忽略标记,即如果某数上有符号,表示该数已标记】。对未标记的0元素进行圆括号选中标记,并对同一列上的其他0元素进行双引号忽略标记。重复这一过程,直到每行没有尚未标记的0元素或至少有2个以上的0元素。
 [  5        0       2       0  2]
 [  2        300  0]
 [ (0)       10      5       7  2]
 [  9        8      (0)      5  4]
 [ “06       3       6  5]
  • 现在,依次检查每列。规则同上。

 [ 5       (0)       202]
 [ 2        30“      (0)    “0“]
 [ (0)      10       5        7      2]
 [ 9        8       (0)       5      4]
 [ “06        3        6      5]
  • 最后如果还存在都行多列同时有两个或两个以上的尚未标记的0元素,则可将其中任意行或列的一个为标记的0元素作选中标记,并将同行同列的其他0元素作忽略标记。
  • 以上并没有做到完全分配(第四行还有进行选中),于是根据以下步骤调整矩阵
    a.检查尚未标记()的行,并且打勾☑️,得
 [ 5       (0)       2       “0“     2]
 [ 2        3       “0“      (0)    “0“]
 [ (0)      10       5        7      2]
 [ 9        8       (0)       5      4]
 [ “0“      6        3        6      5] ☑️

b.在已☑️的行中对所有有0元素的列打☑️
c.在对已☑️的列中对已有标记()的行进行☑️,得

 [ 5       (0)       2       “0“     2]
 [ 2        3       “0“      (0)    “0“]
 [ (0)      10       5        7      2] ☑️
 [ 9        8       (0)       5      4]
 [ “0“      6        3        6      5] ☑️
   ☑️

d.重复b和c步骤,直到不能打勾为止
e.对未☑️的行 和 已☑️的列 划去元素, 得

 [                                 ]
 [       10       5        7      2] ☑️
 [                                 ]
 [       6        3        6      5] ☑️
   ☑️

f.在剩余元素中找出最小元素,本例中为2,并对已☑️的行的每个元素进行减去最小元素的操作,

[[ 5  0  2  0  2]
 [ 2  3  0  0  0]
 [-2  8  3  5  0]
 [ 9  8  0  5  4]
 [-2  4  1  4  3]]

g.将出现负数的列在加上之前的最小值使得=负数变为0,得

[[ 7   0  2  0  2]
 [ 4   3  0  0  0]
 [ 0   8  3  5  0]
 [ 11  8  0  5  4]
 [ 0   4  1  4  3]]

e.如此反复,直到能作出完全分配为止

 [ 7   (0)    2    0    2]
 [ 4    3     0   (0)   0]
 [ 0    8     3    5   (0)]
 [ 11   8    (0)   5    4]
 [ (0)  4     1    4    3]

所以本例最优解为32(0对应原矩阵位置元素之和)

2.代码

import numpy as np

# 行归约
def smallizeRow(p, dim):
    min_row = np.zeros(dim)
    for i in range(0, dim):
        min_row[i] = min(p[i, :])
    for i in range(0, dim):
        p[i, :] = p[i, :] - min_row[i]
# 列归约
def smallizeCol(p, dim):
    min_col = np.zeros(dim)
    for i in range(0, dim):
        min_col[i] = min(p[:, i])
    for i in range(0, dim):
        p[:, i] = p[:, i] - min_col[i]

# 计算每行每列的0元素的个数
def countZero(p, row, col, dim):
    for i in range(0, dim):
        for j in range(0, dim):
            if( p[i, j] == 0) :
                row[i,0] = row[i, 0] + 1;
                col[0, j] = col[0, j] + 1;

# 对0元素进行标记            
def markZero(p, row, col, visited, dim):
    # 检查行
    for i in range(0, dim):
        if(row[i, 0] == 1):
                # 若该元素为0 且 未被圆括号标记 且未被双引号标记 再进行访问操作
                if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
                    visited[i, j] = 1;
                    row[i, 0] -= 1
                    col[0, j] -= 1
                    for m in range(0, dim):
                        if(p[m, j] == 0 and visited[m, j] != 1 and visited[m, j] != -1):
                            visited[m, j] = -1
                            row[m, 0] -= 1
                            col[0, j] -= 1


    # 检查列
    for j in range(0, dim):
        if(col[0, j] == 1):
            for i in range(0, dim):
                if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
                    visited[i, j] = 1
                    col[0, j] -= 1
                    row[i, 0] -= 1
                    for m in range(0, dim):
                        if(p[i, m] == 0 and visited[i, m] != 1 and visited[i, m] != -1):
                            visited[i, m] = -1
                            row[i, 0] -= 1
                            col[0, m] -= 1


    # 对多行多列存在两个及两个以上的为标记的0的操作
    for i in range(0, dim):
        if (row[i, 0] >= 2 ):
            for j in range(0, dim):
                if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
                    visited[i, j] = 1;
                    row[i, 0] -= 1
                    col[0, j] -= 1
                    for m in range(0, dim):
                        if(p[m, j] == 0 and visited[m, j] != 1 and visited[m, j] != -1):
                            visited[m, j] = -1
                            row[m, 0] -= 1
                            col[0, j] -= 1
                    for n in range(0, dim):
                        if(p[i, n] == 0 and visited[i, n] != 1 and visited[i, n] != -1):
                            visited[i, n] = -1
                            row[i , 0] -= 1
                            col[0, n] -= 1


    for j in range(0, dim):
        if(col[0, j] >= 2):
            for i in range(0, dim):
                if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
                    visited[i, j] = 1
                    col[0, j] -= 1
                    row[i, 0] -= 1
                    for m in range(0, dim):
                        if(p[i, m] == 0 and visited[i, m] != 1 and visited[i, m] != -1):
                            visited[i, m] = -1
                            row[i, 0] -= 1
                            col[0, m] -= 1
                    for n in range(0, dim):
                        if(p[n, j] == 0 and visited[n, j] != 1 and visited[n, j] != -1):
                            visited[n, j] = -1
                            row[n, 0] -= 1
                            col[0, j] -= 1

# 找出最小元素便于更新矩阵
def drawline(p, visited, marked_row, marked_col, dim):
    tempmin = 10000
    # 不相关元素进行标记,便于之后的最小元素的选择
    drawline = np.zeros((dim, dim))
    # 检查行是否有被圆括号标记的0元素
    flag = np.zeros(dim)
    for i in range(0, dim):
        for j in range(0, dim):
            if(visited[i][j] == 1):
                flag[i] = 1

    for i in range(0, dim):
        if(flag[i] == 0):
            marked_row[i, 0] =1
            for m in range(0, dim):
                if(p[i][m] == 0):
                    marked_col[0, m] = 1;
                    for n in range(0, dim):
                        if(visited[n][m] == 1):
                            marked_row[n, 0] = 1

    for i in range(0, dim):
        if marked_row[i, 0] == 0 :
            drawline[i, :] = 1
        if marked_col[0, i] == 1:
            drawline[:, i] = 1

    for i in range(0, dim):
        for j in range(0, dim):
            if drawline[i, j] != 1 and p[i, j]!= 0 and p[i, j] < tempmin:
                tempmin = p[i, j]

    return tempmin

# 更新矩阵便于第二次迭代寻找完全分配
def updata(p, marked_row, tempmin, dim):

    for i in range(0, dim):
        if marked_row[i] == 1:
            p[i, :] = p[i, :] - tempmin

    # print(p)

    for i in range(0, dim):
        for j in range(0, dim):
            if p[i, j] < 0 :
                p[:, j] = p[:, j] + tempmin

if __name__ == '__main__':
    # 数组维度
    dim = 5
    # 原始数组
    p = np.array([12, 7, 9, 7, 9, 8, 9, 6, 6, 6, 7, 17, 12, 14, 9, 15, 14, 6, 11, 10, 4, 10, 7, 10, 9])
    p = p.reshape((dim, dim))
    # 记录原始数组值
    q = p.copy()
    print("原始矩阵为:\n", p)

    # 标记是否已找到完全分配
    flag = 0

    #行列归约
    smallizeRow(p, dim)
    smallizeCol(p, dim)
    print("归约后矩阵为:\n", p)

    while(flag == 0):
        # 统计每行0的个数
        row = np.zeros((dim, 1))
        # 统计每列0的个数
        col = np.zeros((1, dim))
        # 标记0元素的被访问类型,当访问次数标记为1时,表示括号,-1为双引号
        visited = np.zeros((dim, dim))
        # 标记打勾的行与列
        marked_row = np.zeros((dim, 1))
        marked_col = np.zeros((1, dim))

        # 标记是否完全分配, 当count=5时表示已完全分配
        count = 0
        solution = 0

        countZero(p, row, col, dim)
        # print(row)
        # print(col)
        markZero(p, row, col, visited, dim)
        # print(p)
        print("迭代标记矩阵为:\n", visited)
        # print(row)
        # print(col)
        tempmin = drawline(p, visited, marked_row, marked_col, dim)
        # print(marked_row)
        # print(marked_col)
        # print(tempmin)
        updata(p, marked_row, tempmin, dim)
        print("迭代后的矩阵为:\n", p)

        for i in range(0, dim):
            for j in range(0, dim):
                if visited[i, j] == 1:
                    count += 1
                    solution += q[i, j]

        if count == dim:
            flag = 1
            print("the best solution is : ", solution )
            break

        print("再次迭代求完全分配")

运行结果为:

初始矩阵为:
 [[12  7  9  7  9]
 [ 8  9  6  6  6]
 [ 7 17 12 14  9]
 [15 14  6 11 10]
 [ 4 10  7 10  9]]
归约后的矩阵:
 [[ 5  0  2  0  2]
 [ 2  3  0  0  0]
 [ 0 10  5  7  2]
 [ 9  8  0  5  4]
 [ 0  6  3  6  5]]
迭代后的标记矩阵为:
 [[ 0.  1.  0. -1.  0.]
 [ 0.  0. -1.  1. -1.]
 [ 1.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.]
 [-1.  0.  0.  0.  0.]]
迭代之后的矩阵为: 
 [[ 7  0  2  0  2]
 [ 4  3  0  0  0]
 [ 0  8  3  5  0]
 [11  8  0  5  4]
 [ 0  4  1  4  3]]
再次迭代求最优解
迭代后的标记矩阵为:
 [[ 0.  1.  0. -1.  0.]
 [ 0.  0. -1.  1. -1.]
 [-1.  0.  0.  0.  1.]
 [ 0.  0.  1.  0.  0.]
 [ 1.  0.  0.  0.  0.]]
迭代之后的矩阵为: 
 [[ 7  0  2  0  2]
 [ 4  3  0  0  0]
 [ 0  8  3  5  0]
 [11  8  0  5  4]
 [ 0  4  1  4  3]]
the best solution is :  32

3.思考
关于其时空复杂度分别为O(n^3), O(n), n为节点个数。在面对目标过多的情况下,效率不高,考虑贪心算法。即从第一行开始每行选择最小的数,然后划区所选数的当前行当前列,不列入选择范围,然后依次重复下面每行,获得次优解。然后对列进行同样的操作得到结果。将行列得到的结果比较选择更优解作为解。显然这样的时空复杂度为O(n),O(1),且结构和最优解接近。同以上例子的贪心结果为:32。

你可能感兴趣的:(新手入门,算法)