在上一篇文章中,介绍了二分图的最大匹配问题及其算法,但是分配问题和二分图最大匹配似乎关联不大,为什么要详细介绍二分图最大匹配呢? 其实如果给二分图中的边加上权重,那么这个二分图就是分配问题的图形式,如果分配问题的目标是最大值目标,那么对应的图论问题就是求二分图的最大权重匹配,具体来说:
因为分配问题大多数情况是要让总的cost最小,所以本文还是以二分图最小完美匹配作为对象进行叙述(分配的结果应该是让所有的对象都有分配值,所以要强调是完美匹配)
二分图最大匹配其实是权重都是1的特殊情况,我们可以使用搜索增广路径的算法来求解二分图最大匹配问题;那么对于更为一般的权重二分图,该方法还可以使用吗? 其实算法的核心还是和增广路径相关,不过需要在原始的权重二分图上扩充一下
需要介绍两个非常关键的算法术语:Feasible Labeling(不知道怎么用中文描述,有的资料里会称对偶变量)和Equality Graph
我们用一个例子来帮助理解:假设一个二分图的权重矩阵如下表所示:
y1 | y2 | y3 | |
---|---|---|---|
x1 | 0 | 1 | 0 |
x2 | 2 | 1 | 3 |
x3 | 0 | 1 | 2 |
对于一个权重二分图,我们如果设定了 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling,并得到了相应的 E q u a l i t y G r a p h Equality\ Graph Equality Graph,我们有下面这个定理:
其证明过程如下:
通过设定 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling和 E q u a l i t y G r a p h Equality\ Graph Equality Graph及上面的定理,我们把问题又转化到了一个寻找最大匹配的问题,只不过是通过一些手段在子图上搜索,并且我们希望最大的匹配一定是完美匹配
用一句话来概括最小权重匹配算法的思想:通过合适的 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling来构建 E q u a l i t y G r a p h Equality\ Graph Equality Graph,使我们能够在这个子图中找到一个完美匹配。
显然我们很难直接找到最合适的 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling及其 E q u a l i t y G r a p h Equality\ Graph Equality Graph,所以我们的策略还是一个迭代过程,先初始化一个 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling,看看当前的 E q u a l i t y G r a p h Equality\ Graph Equality Graph上能不能搜索到完美匹配,如果有了,那已经找到最优解了;如果没有,我们改进 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling,使得新的 E q u a l i t y G r a p h Equality\ Graph Equality Graph在保留上一次的边的情况下再多出一些边,这样匹配数可以增加,继续判断是否达到完美匹配
我们还是用一个例子来辅助说明,假设下表是一个4x4的分配问题的cost矩阵:
Task \ Person | y1 | y2 | y3 | y4 |
---|---|---|---|---|
x1 | 210 | 90 | 180 | 160 |
x2 | 100 | 70 | 130 | 200 |
x3 | 175 | 105 | 140 | 170 |
x4 | 80 | 65 | 105 | 120 |
初始化 l l l很简单,只需要让:
∀ x ∈ X , l ( x ) = 0 ∀ y ∈ Y , l ( y ) = m i n x ∈ X { w ( x , y ) } \begin{aligned} &\forall x\in X,l(x)=0\\ &\forall y\in Y,l(y)=min_{x\in X}\{w(x,y)\} \end{aligned} ∀x∈X,l(x)=0∀y∈Y,l(y)=minx∈X{w(x,y)}
对于我们的例子,初始化结果如下图所示
对于这个初始的 l l l, e q u a l i t y g r a p h equality\ graph equality graph为:
显然这四条边中任选一条都是一个最大匹配,假设选择的是 ( x 4 , y 2 ) (x_4,y_2) (x4,y2),它不是完美匹配,所以我们需要更新 l l l,在保留当前 e q u a l i t y equality equality边的基础上扩展一些新的边
更新 l a b e l i n g labeling labeling用到另一个推论。假设 l l l是一个 f e a s i b l e l a b e l i n g feasible\ labeling feasible labeling,定义点 u ∈ V u\in V u∈V的邻点(neighbor)为 N l ( u ) = { v : ( u , v ) ∈ E l } N_l(u)=\{v:(u,v)\in E_l\} Nl(u)={v:(u,v)∈El},意思就是另一个点集上和它连接且在图 G l G_l Gl上的点的集合;定义集合 S ⊆ V S\subseteq V S⊆V为 N l ( S ) = ∪ u ∈ S N l ( u ) N_l(S)=\cup _{u\in S}N_l(u) Nl(S)=∪u∈SNl(u),意思就是多个点的邻点的集合;则有推论:
这个推论表明我们可以采用上面描述的 α \alpha α值来更新 f e a s b i l e l a b e l i n g feasbile\ labeling feasbile labeling,能够不改变当前的 e q u a l i t y g r a p h equality\ graph equality graph又能引入新的 e q u a l i t y equality equality边;问题在于点集 S S S和 T T T的如何选择,根据推论,任意选择 S S S和 T T T都是可以的,但是回到我们的算法,我们的目的是让新的 E l E_l El中出现更多的匹配,所以必须采取合适的策略来选择 S S S和 T T T;这就又要牵扯到最大匹配中的交替路径和增广路径了,在二分图最大匹配算法中,达到最大匹配的条件是找不到任何增广路径,那么其实更新 E l E_l El的目的就是引入一条新的未匹配边来构成增广路径,这样在新的子图中就一定能扩充1数量的匹配了,所以我们要找选择的 S S S和 T T T应该是当前 E l E_l El的一条以自由顶点开始的最长的交替路径所经过的点的集合,这样通过更新 l a b e l i n g labeling labeling,我们将给这条交替路径添上一条未匹配边构成增广路径,从而让匹配增多
回到我们的例子,在当前的 E l E_l El中,我们可以选择自由顶点 x 1 x_1 x1,它在 E l E_l El中没有任何连线,相当于一条空的交替路径,即 S = { x 1 } , T = ∅ S=\{x_1\},T=\emptyset S={x1},T=∅;用这个 S S S和 T T T来计算 α \alpha α:
α l = m i n x ∈ S , y ∉ T { w ( x , y ) − l ( x ) − l ( y ) } = { ( x 1 , y 1 ) : 210 − 0 − 80 = 130 ( x 1 , y 2 ) : 90 − 0 − 65 = 25 ( x 1 , y 3 ) : 180 − 0 − 105 = 75 ( x 1 , y 4 ) : 160 − 0 − 120 = 40 = 25 \alpha _l=min_{x\in S,y\notin T}\{w(x,y)-l(x)-l(y)\}= \begin{cases} (x_1,y_1):210-0-80=130 \\ (x_1,y_2):90-0-65=25\\ (x_1,y_3):180-0-105=75 \\ (x_1,y_4):160-0-120=40\\ \end{cases}\\ =25 αl=minx∈S,y∈/T{w(x,y)−l(x)−l(y)}=⎩⎪⎪⎪⎨⎪⎪⎪⎧(x1,y1):210−0−80=130(x1,y2):90−0−65=25(x1,y3):180−0−105=75(x1,y4):160−0−120=40=25
由此更新 l l l得到了新的 e q u a l i t y g r a p h equality\ graph equality graph,边 ( x 1 , y 2 ) (x_1,y_2) (x1,y2)是新加入的一条边
这样,我们就可以将匹配数扩充到2了:
总结一下完整的算法步骤:
该算法的复杂度同样是 n 3 n^3 n3;像该算法这样将一个优化问题转化成最大(最小)权重的图匹配问题,也是一种经典的组合优化求解手段
我用python写了段代码来具体实现上述的算法过程。定义了GraphHungarianAlgorithm类,它的输入只需要一个cost矩阵;calculate方法内实现算法的主过程;searchAugPath方法用于在 E l E_l El中搜索增广路径,如果搜索不到则返回一条交替路径;findRoute方法则是searchAugPath内的核心操作,其原理和二分图最大匹配中使用的搜索算法一致,不过除了返回代表增广路径的最短路径,也会返回不存在增广路径时的最长路径;invert方法则对增广路径取反来更新匹配
import numpy as np
class GraphHungarianAlgorithm:
def __init__(self, costEdges):
self.costEdges=costEdges
if costEdges.shape[0]!=costEdges.shape[1]:
raise Exception("Cost Edges must be n X n")
self.n=costEdges.shape[0]
def calculate(self):
# init labels and matches
self.labels_X=np.zeros((self.n,1))
self.labels_Y=np.zeros((self.n,1))
for i in range(self.n):
self.labels_Y[i]=(self.costEdges.min(0))[i]
equalityGraph=self.getEqualityGraph()
matches=[]
iter=1
while True:
print("################## "+str(iter)+" #################")
iter+=1
print("Searching augmenting path...")
(existAugPath, path, S, T)=self.searchAugPath(matches,equalityGraph)
if existAugPath: #If there's augment path, we can extend the matches
print("Find augmenting path, extending matches")
matches=self.invert(path,matches)
print("Current matches :")
print(matches)
if len(matches)==self.n:
break
else:
print("Can't find augmenting path, updating labels")
theta=self.getLabelTheta(S,T)
self.updateLabels(theta,S,T)
equalityGraph=self.getEqualityGraph()
return matches
def updateLabels(self, theta, S, T):
for i in range(self.n):
if i in S:
self.labels_X[i]=self.labels_X[i]+theta
for j in range(self.n):
if j in T:
self.labels_Y[j]=self.labels_Y[j]-theta
def getLabelTheta(self, S, T):
theta=float('inf')
for i in range(self.n):
for j in range(self.n):
if i in S and j not in T:
delta=self.costEdges[i,j]-self.labels_X[i]-self.labels_Y[j]
if theta>delta:
theta=delta
return theta
def getEqualityGraph(self):
# equalityGraph just descrip whether x and y exist edge
equalityGraph=np.zeros((self.n,self.n))
for i in range(self.n):
for j in range(self.n):
if self.labels_X[i]+self.labels_Y[j]==self.costEdges[i,j]:
equalityGraph[i,j]=1
else:
equalityGraph[i,j]=0
return equalityGraph
def searchAugPath(self,matches,equalityGraph):
#Create direct graph
#directMatrix[i,j]=1 : x_i -> y_j
totalVertexN=self.n+self.n+2 # node_0 is start node , node_totalVertexN-1 is end node
directMatrix=np.zeros((totalVertexN,totalVertexN))
for i in range(0,totalVertexN-1):
for j in range(0,totalVertexN):
if i==0 and j>=1 and j<=self.n:
directMatrix[i,j]=1
continue
if j==totalVertexN-1 and i>self.n:
directMatrix[i,j]=1
continue
if i==j or i==0 or j==0 or j==totalVertexN-1:
continue
iIsX=False
jIsX=False
if i>0 and i<=self.n:
iIsX=True
elif i>self.n and i<=totalVertexN-2:
iIsX=False
if j>0 and j<=self.n:
jIsX=True
elif j>self.n and j<=totalVertexN-2:
jIsX=False
if iIsX and jIsX==False:
directMatrix[i,j]=equalityGraph[i-1,j-1-self.n]
for (x,y) in matches:
directMatrix[x+1,y+1+self.n]=0
directMatrix[y+1+self.n,x+1]=1
for j in range(1, self.n+1):
isMatched=False
for k in range(self.n, totalVertexN-1):
if directMatrix[k][j]==1:
isMatched=True
break
if isMatched:
directMatrix[0][j]=0
for i in range(self.n, totalVertexN-1):
isMatched=False
for k in range(1, self.n+1):
if directMatrix[i][k]==1:
isMatched=True
break
if isMatched:
directMatrix[i][totalVertexN-1]=0
(shortestRoute, longestRoute)=self.findRoute(directMatrix,[0],[],[])
# shortestRoute represents augmenting path
# If shortestRoute doesn't exist, longestRoute represents the max alternating path
if len(shortestRoute)>0:
augmentPath=[]
for i in range(1,len(shortestRoute)-2):
vertexIndex_A=shortestRoute[i]
actualIndex_A=0
vertexIsX_A=False
if vertexIndex_A>=1 and vertexIndex_A<=self.n:
actualIndex_A=vertexIndex_A-1
vertexIsX_A=True
else:
actualIndex_A=vertexIndex_A-1-self.n
vertexIndex_B=shortestRoute[i+1]
actualIndex_B=0
vertexIsX_B=False
if vertexIndex_B>=1 and vertexIndex_B<=self.n:
actualIndex_B=vertexIndex_B-1
vertexIsX_B=True
else:
actualIndex_B=vertexIndex_B-1-self.n
augmentPath.append((actualIndex_A,actualIndex_B,vertexIsX_A))
return (True, augmentPath, None, None)
else:
S=[]
T=[]
alternatingPath=[]
for i in range(1,len(longestRoute)):
vertexIndex=longestRoute[i]
actualIndex=0
vertexIsX=False
if vertexIndex>=1 and vertexIndex<=self.n:
actualIndex=vertexIndex-1
vertexIsX=True
else:
actualIndex=vertexIndex-1-self.n
if vertexIsX:
S.append(actualIndex)
else:
T.append(actualIndex)
return (False, None, S, T)
def findRoute(self, directMatrix, currentRoute, currentShortestRoute, currentLongestRoute):
if len(currentLongestRoute)==0 or len(currentLongestRoute)<len(currentRoute):
currentLongestRoute=currentRoute.copy()
if currentRoute[-1]==directMatrix.shape[0]-1:
if len(currentShortestRoute)==0 or len(currentShortestRoute)>len(currentRoute):
currentShortestRoute=currentRoute.copy()
return (currentShortestRoute, currentLongestRoute)
for i in range(0,directMatrix.shape[0]):
if directMatrix[currentRoute[-1],i]==1:
nextRoute=currentRoute.copy()
nextRoute.append(i)
(route1, route2)=self.findRoute(directMatrix, nextRoute,currentShortestRoute, currentLongestRoute)
if len(currentShortestRoute)==0 or len(currentShortestRoute)>len(route1):
currentShortestRoute=route1.copy()
if len(currentLongestRoute)==0 or len(currentLongestRoute)<len(route2):
currentLongestRoute=route2.copy()
return (currentShortestRoute, currentLongestRoute)
def invert(self, augPath, matches):
for i in range(len(augPath)):
if i%2==0:
if augPath[i][2]:
matches.append((augPath[i][0],augPath[i][1]))
else:
matches.append((augPath[i][1],augPath[i][0]))
else:
removeIndex=-1
for index in range(len(matches)):
if (matches[index][0]==augPath[i][0] and matches[index][1]==augPath[i][1]) or (matches[index][0]==augPath[i][1] and matches[index][1]==augPath[i][0]):
removeIndex=index
break
matches.pop(removeIndex)
return matches
测试代码:
import numpy as np
from GraphHungarianAlgorithm import GraphHungarianAlgorithm
costEdges=np.array([
[210, 90, 180, 160],
[100, 70, 130, 200],
[175, 105,140, 170],
[80, 65, 105, 120]
])
algorithm=GraphHungarianAlgorithm(costEdges)
print(algorithm.calculate())