用Octave计算指派问题(匈牙利算法)
广告
{{v.name}}
匈牙利法是指派问题的专用最优解法,核心逻辑为:通过对成本矩阵做行、列消去(减去最小值),得到含尽可能多0元素的矩阵,0元素表示“最优潜在指派”,当能选出n个独立的0元素(每行每列仅1个)时,即为最优指派方案。
该方法仅需对矩阵做行/列变换、画盖0线、调整矩阵三步循环,手工计算简单,适用于\(n≤10\)的中小规模问题,且仅适用于n阶方阵的平衡指派问题(非平衡需先转化为平衡)。
适用前提
1. 成本矩阵为n阶方阵(执行者数=任务数);
2. 目标为最小化总成本(最大化需先转化);
3. 所有\(c_{ij}≥0\)(消去后自然满足)。
完整计算步骤(最小化成本)
以4阶成本矩阵为例,演示匈牙利法的全流程,核心分为初始化矩阵→找独立0元素→画盖0线→调整矩阵,循环至找到n个独立0元素。
步骤1:初始化成本矩阵——行消去+列消去
目的:让矩阵的每行、每列至少有1个0元素,且所有元素非负。
1. 行消去:对成本矩阵的每一行,减去该行的最小元素;
2. 列消去:对行消去后的矩阵的每一列,减去该列的最小元素。
步骤2:寻找独立0元素,判定是否为最优解
独立0元素:每行、每列仅能选1个的0元素,若能选出n个,则直接得到最优指派方案;若不足n个,进入步骤3。
手工找独立0元素的贪心方法:
1. 从含0元素最少的行/列开始,选定1个0元素,标记为独立0(如圈出\(\bigcirc\));
2. 划去该0元素所在的行和列,在剩余矩阵中重复步骤1;
3. 若最终圈出的独立0元素数=\(n\),停止;否则需画盖0线。
步骤3:画最少的盖0线,覆盖矩阵中所有0元素
盖0线为水平或垂直线,要求线的数量最少,且覆盖所有0元素,手工画法:
1. 对无独立0元素的行打√;
2. 对打√行中的0元素所在列打√;
3. 对打√列中的独立0元素所在行打√;
4. 重复2-3,直到无新的行/列可打√;
5. 对未打√的行画水平线,对打√的列画垂直线,得到的即为最少盖0线。
盖0线的数量 = 当前找到的独立0元素数,若盖0线数=\(n\),则已找到最优解。
步骤4:调整成本矩阵,增加0元素数量
目的:在未被盖0线覆盖的区域生成新的0元素,同时保持已有的0元素不变,调整方法:
1. 找到未被盖0线覆盖的所有元素中的最小值,记为\(\theta\);
2. 对未被覆盖的区域的所有元素,减去\(\theta\);
3. 对两条盖0线的交点处的元素,加上\(\theta\);
4. 仅被1条盖0线覆盖的元素,保持不变。
步骤5:循环判定
调整后得到新的成本矩阵,返回步骤2,重新寻找独立0元素,直到选出n个独立0元素,得到最优解。
例子:
4个工人(\(A_1,A_2,A_3,A_4\))分配4个任务(\(B_1,B_2,B_3,B_4\)),单位成本矩阵如下(单位:元),求总成本最小的指派方案。
\( C = \begin{bmatrix} 8 & 5 & 9 & 7 \\ 6 & 7 & 8 & 9 \\ 5 & 8 & 4 & 6 \\ 7 & 6 & 8 & 5 \end{bmatrix} \)
步骤1:行消去+列消去
1. 行消去:每行减该行最小值
- 行1:min=5 → [3,0,4,2]
- 行2:min=6 → [0,1,2,3]
- 行3:min=4 → [1,4,0,2]
- 行4:min=5 → [2,1,3,0]
行消去后矩阵:\(\begin{bmatrix}3&0&4&2\\0&1&2&3\\1&4&0&2\\2&1&3&0\end{bmatrix}\)
2. 列消去:每列最小值均为0,无需消去,矩阵不变。
步骤2:寻找独立0元素
- 行2仅有1个0(列1),圈出\(\bigcirc\),划去行2、列1;
- 行1仅有1个0(列2),圈出\(\bigcirc\),划去行1、列2;
- 行3仅有1个0(列3),圈出\(\bigcirc\),划去行3、列3;
- 行4仅有1个0(列4),圈出\(\bigcirc\),划去行4、列4;
最终圈出4个独立0元素(等于n=4),直接得到最优指派方案。
步骤3:确定最优指派方案与总成本
独立0元素的位置为:
- \(A_1→B_2\)(\(c_{12}=5\))、\(A_2→B_1\)(\(c_{21}=6\))、\(A_3→B_3\)(\(c_{33}=4\))、\(A_4→B_4\)(\(c_{44}=5\));
总最小成本:\(5+6+4+5=20\)元。
代码如下:
function [assign, opt_val] = hungarian_assign(C, type)
    % 匈牙利法求解指派问题(n阶方阵)
    % 输入:
    %   C: n×n矩阵,成本矩阵(type='min')或利润矩阵(type='max')
    %   type: 求解类型,'min'=最小化成本,'max'=最大化利润
    % 输出:
    %   assign: 1×n向量,assign(i)=j表示将第i个执行者指派给第j个任务
    %   opt_val: 最优值(最小成本/最大利润)
    
    % 输入校验
    [n, m] = size(C);
    if n ~= m
        error('成本/利润矩阵必须为n阶方阵!');
    end
    if ~ismember(type, {'min', 'max'})
        error('type必须为''min''或''max''!');
    end
    
    % 保存原始矩阵,用于最终计算最优值
    origC = C;
    % 最大化利润转化为最小化成本(用cost变量,不覆盖origC)
    if strcmp(type, 'max')
        M = max(origC(:));
        cost = M - origC;
    else
        cost = origC;
    end
    
    % 匈牙利法核心步骤(行消去+列消去+找独立0+调整矩阵)
    row_mask = false(1, n);
    col_mask = false(1, n);
    assign = zeros(1, n);
    row_assign = zeros(1, n); % row_assign(j)=i 列j指派给行i
    
    % 行消去
    for i = 1:n
        cost(i,:) = cost(i,:) - min(cost(i,:));
    end
    
    % 列消去
    for j = 1:n
        cost(:,j) = cost(:,j) - min(cost(:,j));
    end
    
    % 寻找独立0元素并重复调整(核心循环)
    while true
        % 标记独立0并获得列->行的指派(row_assign)
        [row_mask, col_mask, row_assign] = mark_zero(cost, n);
        % 画盖0线:线穿过所有未标记的行和所有已标记的列
        line_num = (n - sum(row_mask)) + sum(col_mask);
        if line_num == n
            break;
        end
        % 调整矩阵:找到未被任何线覆盖的最小值并调整
        % 未被覆盖的元素位于被标记的行且列未被标记
        uncovered = row_mask' & ~col_mask;
        vals = cost(uncovered);
        if isempty(vals)
            break;
        end
        theta = min(vals);
        cost(uncovered) = cost(uncovered) - theta;
        % 将theta加到被两条线覆盖的元素(即未被标记的行与已标记的列的交点)
        cost(~row_mask', col_mask) = cost(~row_mask', col_mask) + theta;
    end
    
    % 确定指派方案
    for j = 1:n
        assign(row_assign(j)) = j;
    end
    
    % 计算最优值(直接使用原始矩阵origC)
    opt_val = sum(origC(sub2ind([n,n], 1:n, assign)));
end

% 辅助函数:标记独立0元素
function [row_mask, col_mask, row_assign] = mark_zero(cost, n)
    % 使用标准匈牙利流程:先贪婪找独立0(starred zeros),然后标记行/列
    row_star = zeros(1, n); % row_star(i) = j 表示行i的star列
    col_star = zeros(1, n); % col_star(j) = i 表示列j的star行
    for i = 1:n
        for j = 1:n
            if cost(i,j) == 0 && row_star(i) == 0 && col_star(j) == 0
                row_star(i) = j;
                col_star(j) = i;
            end
        end
    end
    row_assign = col_star; % 返回列->行的指派(col j 指派给 row col_star(j))

    % 标记:先标记所有没有star的行
    row_mask = false(1, n);
    col_mask = false(1, n);
    for i = 1:n
        if row_star(i) == 0
            row_mask(i) = true;
        end
    end

    % 反复:在被标记的行中标记含0的列;在被标记的列中标记含star的行,直到不再改变
    while true
        changed = false;
        for i = 1:n
            if row_mask(i)
                for j = 1:n
                    if cost(i,j) == 0 && ~col_mask(j)
                        col_mask(j) = true;
                        changed = true;
                    end
                end
            end
        end
        for j = 1:n
            if col_mask(j)
                if col_star(j) ~= 0 && ~row_mask(col_star(j))
                    row_mask(col_star(j)) = true;
                    changed = true;
                end
            end
        end
        if ~changed
            break;
        end
    end
end
结果如下:
>> [assign1, val1] = hungarian_assign([8 5 9 7;6 7 8 9;5 8 4 6;7 6 8 5], 'min')
assign1 =

   2   1   3   4

val1 = 20
友链