分配问题与Hungarian算法
匈牙利方法是一种能够在多项式时间内解决分配问题(assignment problem)的组合优化算法。它由Harold Kuhn 与1955年发展并提出,由于该算法很大程度上依赖于先前两位匈牙利数学家:Denes Konig 和 Jeno Egervary,所以被命名为“匈牙利方法”。
1957年James Munkres重新审视了这个方法,证明发现该方法是严格polynomial的,所以之后该方法也被称为Kuhn-Munkres 算法或者Munkres分配算法。原始的匈牙利算法的时间复杂度是,然而之后Edmonds和Karp,以及Tomizawa独立发现经过一定的修改,该算法能改达到的时间复杂度。 Ford和Fulkerson将该方法扩展到一般运输问题的求解上。 2006年,研究发现Carl Custav Jacobi在19实际就解决了assignment问题,并且在其逝世后的1890年求解过程被以拉丁语形式发表。。。
指派问题
匈牙利法解决的指派问题应该具有两个约束条件
workes 和tasks的数目应该相同,即o2o问题。
求解的是最小化问题,如工作时间的最小化、费用的最小化等等
指派问题示例:
有三个workers: Jim, Steve和Alan,现在有3个工作:clean the bathroom, sweep the floors和wash the windows需要交给这三个人,每个人只能完成一个任务,对应的cost matrix如下
--- | Clean bathroom | Sweep floors | Wash windows |
---|---|---|---|
Jim | $2 | $3 | $3 |
Steve | $3 | $2 | $3 |
Alan | $3 | $3 | $2 |
那么如何分配任务是开销最小就是一个指派问题
匈牙利法步骤
问题: 假定某单位有甲、乙、丙、丁、戊五个员工,现需要完成A、B、C、D、E五项任务,每个员工完成某项任务的时间如下图所示,应该如何分配任务,才能保证完成任务所需要的时间开销最小?
解:
写出系数矩阵
更新系数矩阵,使系数矩阵的每一行每一列都减去该行该列的最小值,保证每一行每一列都有0元素出现,参见定理2.
选择只有一个0元素的行或列将该0元素标注为独立0元素,并将该0元素所在的列或行中0元素划掉,直至找不到满足条件的行或列,需要注意的是在循环时,划掉的0元素不再视为0元素。比如我们找下面这个系数矩阵的标注0元素和划掉的0元素
那么我们用1表示0元素,0表示非0元素,用2标注独立0元素,-2表示划掉0元素,依次得到的中间结果为
可以发现我们在标注第一行第2个0元素时,并没有把已经划掉的第一个0当作0元素看待。
划盖0线。
a. 首先找到不含有独立0元素的行,将行标注 (4)
b. 找到标注行中划掉的0元素所在的列,将列标注 (2,3)
c. 将标注列中独立0元素所在的行标注 (4, 1, 2)
d. 重复b,c步骤知道所有的标注行中不再存在划掉的0元素
(行:4 ,1, 2, 3 ; 列: 2, 3, 1)
e. 在所有标注的列上做盖0线,在所有未被标注的行上做盖0线,我们在矩阵中用3表示被盖0线覆盖,则可以发现所有的0元素都被覆盖掉,所以称为盖0线。
根据定理1,如果此时盖0线的个数等于矩阵的维数,相当于找到n个独立0元素,则跳到步骤7,否则步骤5更新系数矩阵。更新系数矩阵。
a. 找到未被盖0线覆盖的元素中的最小值
b. 所有未被盖0线覆盖的元素减去最小值
c. 所有盖0线交叉处的元素值加上最小值重复步骤4,5
计算最优解。
类似步骤3中找独立0元素的方法在C中找到n个不同行不同列的0元素所对应的位置。
定理1. 系数矩阵C中独立零元素的最多个数等于能覆盖所有零元素的最少线数。 —— D. Konig
定理2. 若将分配问题的系数矩阵每一行及每一列分别减去各行及各列的最小元素,则新的分配问题与原分配问题有相同的最优解,只是最优值差一个常数。
当然,注意找盖〇线的方法并不是唯一的,比如下述方法
同上一方法
同上一方法
在C中寻找未被盖〇线覆盖的存在0元素且数组最少的行(列),标注一个0元素作为独立0元素,该0元素所在的列(行)做盖0线。重复此步骤,直至找不到符合条件的行或列,已经被找过的行(列)就不再找了!
若是盖0线数组等于矩阵维度,则跳到7
同上一方法
重复3,4,5
同上一方法
当然还有别的方法,比如从包含最多0元素的行或列开始做盖0线直到将所有的0元素覆盖掉等
这里代码实现了上面两个方法,注释掉的是第二个方法的核心
- function [cost,CMatrix]=Assignment(C,ismin)
- % Assignment problem solved by hungarian method.
- %
- % input:
- % C - 系数矩阵,可以适应workers和tasks数目不同的情形
- % ismin - 1表示最小化问题,0表示最大化问题
- % ouput:
- % cost - 最终花费代价
- % CMatrix - 对应的匹配矩阵,元素1所在位置c_{ij}表示j task分配给 i worker。
- %
-
- [m,n]=size(C);
- if ismin==0
- C=max(C(:))-C;
- end
-
- %workes 和tasks数目不相同
- if m<n
- C=[C;zeros(n-m,n)];
- elseif m>n
- C=[C zeros(m,m-n)];
- end
- copyC=C;
- d=max(m,n);% 最终系数矩阵的维度
- C=C-repmat(min(C,[],2),1,d);
- C=C-repmat(min(C,[],1),d,1);
-
- %% 方法一
- while 1
- A=int8((C==0));
- nIZeros=0; % 独立0元素的个数
- while 1
- r=sum(A==1,2); % 每一行0元素的个数
- [~,idr]=find(r'==1);%找到只有一个0元素的行
- if ~isempty(idr) % 如果找到这样的行
- tr=A(idr(1),:);
- [~,idc]=find(tr==1);%找到0元素所在列
- A(idr(1),idc)=2;%标注独立元素
- tc=A(:,idc);
- tc(idr(1))=2;
- [~,idr]=find(tc'==1);%找到独立0元素所在列的其他0元素
- A(idr,idc)=-2;%划掉独立0元素所在列的其余0元素
- nIZeros=nIZeros+1;
- else
- c=sum(A==1,1); % 每一列0元素的个数
- [~,idc]=find(c==1);%找到只含有一个0元素的列
- if ~isempty(idc)% 找到这样的列
- tc=A(:,idc(1));
- [~,idr]=find(tc'==1);%0元素所在的行
- A(idr,idc(1))=2;%标注独立0元素
- tr=A(idr,:);
- tr(idc(1))=2;
- [~,idc]=find(tr==1);%独立0元素所在行的其他0元素
- A(idr,idc)=-2;%划掉独立0元素所在行的其余0元素
- nIZeros=nIZeros+1;
- else
- break;
- end
- end
- end
-
- if nIZeros==d
- %计算最优解
- CMatrix=(A==2);
-
- if ismin==1
- cost=sum(copyC(:).*CMatrix(:));
- else
- cost = sum((max(copyC(:))-copyC(:)).*CMatrix(:));
- end
- CMatrix=CMatrix(1:m,1:n);
- break;%找到d个独立0元素,则跳出循环
- else% 独立0元素个数不足,就要找盖0线了
- r=sum(A==2,2);
- [~,idr]=find(r'==0);%不含有独立0元素的行
- idrr=idr;
- idcc=[];
- while 1
- tr=A(idrr,:);
- [~,idc]=find(tr==-2);%不含独立0元素的行中划掉的0元素所在列
- if isempty(idc),break;end
- tc=A(:,unique(idc));
- [idrr,~]=find(tc==2);%这些列中标注的0元素所在行
- idr=[idr,idrr'];
- idcc=[idcc,idc];
- end
- idry=1:d;
- idry(idr)=[];%盖0线所在的行索引
- TempC=C;%存储非覆盖元素
- TempC(idry,:)=[];
- TempC(:,idcc)=[];
- minUnOverlap=min(TempC(:));
- %更新系数矩阵
- C=C-minUnOverlap;
- C(idry,:)=C(idry,:)+minUnOverlap;
- C(:,idcc)=C(:,idcc)+minUnOverlap;
- end
- end
- %% 方法二
- % while 1
- % CMatrix=zeros(d);
- % nLines=0;
- % A=(C==0);
- % idx=[];
- % idy=[];
- % sr=[];
- % sc=[];
- % while 1
- % r=sum(A,2);
- % c=sum(A,1);
- % r(sr)=0;
- % c(sc)=0;
- % trc=[r(:);c(:)];
- % [trc,idtrc]=sort(trc,1,'ascend');
- % [~,idn0]=find(trc'>0);
- % if ~isempty(idn0)
- % id=idtrc(idn0(1));
- % if id>d
- % tc=A(:,id-d);
- % [~,idr]=find(tc'==1);
- % A(idr(1),:)=0;
- % nLines=nLines+1;
- % idy=[idy,idr(1)];
- % CMatrix(idr(1),id-d)=1;
- % sc=[sc,id-d];
- % else
- % tr=A(id,:);
- % [~,idc]=find(tr==1);
- % A(:,idc(1))=0;
- % nLines=nLines+1;
- % idx=[idx,idc(1)];
- % CMatrix(id,idc(1))=1;
- % sr=[sr,id];
- % end
- % else
- % break;
- % end
- % end
- % if nLines==d
- % if ismin
- % cost=sum(copyC(:).*CMatrix(:));
- % else
- % cost=sum((max(copyC(:))-copyC(:)).*CMatrix(:));
- % end
- % CMatrix=CMatrix(1:m,1:n);
- % break;
- % else
- % tempC=C;
- % tempC(idy,:)=[];
- % tempC(:,idx)=[];
- % minUnOverlap=min(tempC(:));
- % C=C-minUnOverlap;
- % C(idy,:)=C(idy,:)+minUnOverlap;
- % C(:,idx)=C(:,idx)+minUnOverlap;
- % end
- % end
-
- end
-
匈牙利算法的拓展
workers数小于tasks数目
此时可以虚拟出若干个workers使个数相等,对于虚拟出的workers对于tasks的代价是相同的都是0.workers数目大于tasks数目
可以和上述类似,增加虚拟tasks,在系数矩阵中对应列元素都为0最大值问题
找到系数矩阵中最大的元素,然后使用最大元素分别减去矩阵中元素,这样最大化问题就化为最小化问题,同样可以使用匈牙利算法。
测试
现在使用代码求解上述例题
注意方法一中存在不足之处是必须找到仅含一个0元素的行或列,这并不科学,比如下面这个系数矩阵,就会出现死循环,所以可以寻找含有最少0元素的行或列,这就和方法二类似了,所以我没再实现。方法二可以解决下面这个极端例子
我们来使用方法二(将代码中注释掉的部分反注释,方法一注释掉)测试下这个极端的例子
测试下面这个系数矩阵,匈牙利算法拓展1
结果
对上个测试用例计算最大代价
综上,相较于方法一,方法二能够处理各种状况。