求解分配问题(三) 二分图最小权重匹配

在上一篇文章中,介绍了二分图的最大匹配问题及其算法,但是分配问题和二分图最大匹配似乎关联不大,为什么要详细介绍二分图最大匹配呢? 其实如果给二分图中的边加上权重,那么这个二分图就是分配问题的图形式,如果分配问题的目标是最大值目标,那么对应的图论问题就是求二分图的最大权重匹配,具体来说:

  • 给二分图 G G G的边设置权重 w ( i , j ) w(i,j) w(i,j) i i i j j j分别表示两个点集中的点,对于一个匹配 M M M,其权重和 w ( M ) = ∑ e ∈ M w ( e ) w(M)=\sum_{e\in M}w(e) w(M)=eMw(e),最大权重匹配问题就是找到是权重和最大的匹配;如果设置权重为 c ( i , j ) = W − w ( i , j ) , W ≥ m a x ( w ( i , j ) ) c(i,j)=W-w(i,j),W\geq max(w(i,j)) c(i,j)=Ww(i,j),Wmax(w(i,j)),那么最大权重匹配就转化成了最小权重匹配

因为分配问题大多数情况是要让总的cost最小,所以本文还是以二分图最小完美匹配作为对象进行叙述(分配的结果应该是让所有的对象都有分配值,所以要强调是完美匹配)

二分图最大匹配其实是权重都是1的特殊情况,我们可以使用搜索增广路径的算法来求解二分图最大匹配问题;那么对于更为一般的权重二分图,该方法还可以使用吗? 其实算法的核心还是和增广路径相关,不过需要在原始的权重二分图上扩充一下

转化成最大匹配问题

需要介绍两个非常关键的算法术语:Feasible Labeling(不知道怎么用中文描述,有的资料里会称对偶变量)和Equality Graph

我们用一个例子来帮助理解:假设一个二分图的权重矩阵如下表所示:

y1 y2 y3
x1 0 1 0
x2 2 1 3
x3 0 1 2

用图表示:
求解分配问题(三) 二分图最小权重匹配_第1张图片
然后:

  • 定义对一个顶点的 l a b e l i n g labeling labeling表示对这个顶点附加一个值映值 l : V → R l:V \rightarrow R l:VR,意思就是给每个点附加一个变量值
  • 对每个顶点都做 l a b e l i n g labeling labeling,我们称这个 l a b e l i n g labeling labeling f e a s i b l e feasible feasible的当:
    l ( x ) + l ( y ) ≤ w ( x , y ) ,    ∀ x ∈ X , y ∈ Y \begin{aligned} l(x)+l(y)\leq w(x,y),\ \ \forall x \in X,y\in Y \end{aligned} l(x)+l(y)w(x,y),  xX,yY
    例如对于上面的这个二分图,下面的 l a b e l i n g labeling labeling(标红的数字)就是 f e a s i b l e feasible feasible的,因为任意两个顶点的 l a b e l i n g labeling labeling值的和都不大于它们之间的权重值
    求解分配问题(三) 二分图最小权重匹配_第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,用数学语言描述就是, G e = ( V , E l ) G_{e}=(V,E_l) Ge=(V,El),满足:
    E l = { ( x , y ) : l ( x ) + l ( y ) = w ( x , y ) } \begin{aligned} E_l=\{(x,y):l(x)+l(y)=w(x,y)\} \end{aligned} El={(x,y):l(x)+l(y)=w(x,y)}
    在上面的例子中,其 E q u a l i t y   G r a p h Equality\ Graph Equality Graph如下图所示:
    求解分配问题(三) 二分图最小权重匹配_第3张图片

对于一个权重二分图,我们如果设定了 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,我们有下面这个定理:

  • 如果一个 l l l f e a s i b l e feasible feasible的,并且 M M M E l E_l El上的一个完美匹配,那么 M M M就是该二分图的最小权重匹配

其证明过程如下:

  • 定义任意的边为 e = ( e x , e y ) ∈ E e=(e_x,e_y)\in E e=(ex,ey)E e x e_x ex e y e_y ey是它的两个端点,假设 M ′ M' M是图 G G G上的任意完美匹配(不限定在 G l G_l Gl上),因为是完美匹配,每个顶点 v ∈ V v\in V vV都正好被完美匹配上的一条边连接,就有
    w ( M ′ ) = ∑ e ∈ M ′ w ( e ) ≥ ∑ e ∈ M ′ ( l ( e x ) + l ( e y ) ) = ∑ v ∈ V l ( v ) \begin{aligned} w(M')=\sum_{e\in M'}w(e)\geq \sum_{e\in M'}(l(e_x)+l(e_y))=\sum_{v\in V}l(v) \end{aligned} w(M)=eMw(e)eM(l(ex)+l(ey))=vVl(v)
    所以 ∑ v ∈ V l ( v ) \sum_{v\in V}l(v) vVl(v)是所有完美匹配的权重和的下界值;现在假设 M M M E l E_l El上的完美匹配,那么:
    w ( M ) = ∑ e ∈ M w ( e ) = ∑ v ∈ V l ( v ) \begin{aligned} w(M)=\sum_{e\in M}w(e)=\sum_{v\in V}l(v) \end{aligned} w(M)=eMw(e)=vVl(v)
    就有 w ( M ′ ) ≥ w ( M ) w(M')\geq w(M) w(M)w(M) M M M就是最优值

通过设定 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在保留上一次的边的情况下再多出一些边,这样匹配数可以增加,继续判断是否达到完美匹配

  1. 初始化 f e a s i b l e   l a b e l i n g   l feasible\ labeling\ l feasible labeling l
  2. E l E_l El是当前的 e q u a l i t y   g r a p h equality\ graph equality graph,搜索 E l E_l El上的最大匹配 M M M,如果它是完美匹配,则退出算法;否则进行3
  3. 改进 l l l得到 l ′ l' l,使得 E l ⊆ E l ′ E_l\subseteq E_{l'} ElEl,回到2

我们还是用一个例子来辅助说明,假设下表是一个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} xX,l(x)=0yY,l(y)=minxX{w(x,y)}
对于我们的例子,初始化结果如下图所示

求解分配问题(三) 二分图最小权重匹配_第4张图片
对于这个初始的 l l l e q u a l i t y   g r a p h equality\ graph equality graph为:
求解分配问题(三) 二分图最小权重匹配_第5张图片

显然这四条边中任选一条都是一个最大匹配,假设选择的是 ( 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 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 uV的邻点(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 SV N l ( S ) = ∪ u ∈ S N l ( u ) N_l(S)=\cup _{u\in S}N_l(u) Nl(S)=uSNl(u),意思就是多个点的邻点的集合;则有推论:

  • S ⊆ X S\subseteq X SX T = N l ( S ) ≠ Y T=N_l(S)\ne Y T=Nl(S)=Y,然后设
    α l = m i n x ∈ S , y ∉ T { w ( x , y ) − l ( x ) − l ( y ) } \begin{aligned} \alpha _l=min_{x\in S,y\notin T}\{w(x,y)-l(x)-l(y)\} \end{aligned} αl=minxS,y/T{w(x,y)l(x)l(y)}
    α \alpha α更新当前的 l a b e l i n g labeling labeling:
    l ′ ( v ) = { l ( v ) + α l , v ∈ S l ( v ) − α l , v ∈ T l ( v ) , o t h e r s l'(v)= \begin{cases} l(v)+\alpha _l,v\in S\\ l(v)-\alpha_l,v\in T\\ l(v), others \end{cases} l(v)=l(v)+αl,vSl(v)αl,vTl(v),others
    那么这个 l ′ l' l也是 f e a s i b l e feasible feasible的,并且 E l ⊆ E l ′ E_l\subseteq E_{l'} ElEl,具体来说:
  1. 如果 ( x , y ) ∈ E l , x ∈ S , y ∈ T (x,y)\in E_l,x\in S, y\in T (x,y)El,xS,yT,那么 ( x , y ) ∈ E l ′ (x,y)\in E_{l'} (x,y)El
  2. 如果 ( x , y ) ∈ E l , x ∉ S , y ∉ T (x,y)\in E_l,x\notin S, y\notin T (x,y)El,x/S,y/T,那么 ( x , y ) ∈ E l ′ (x,y)\in E_{l'} (x,y)El
  3. ∃ l ( x , y ) ∈ E l ′ \exist l(x,y)\in E_{l'} l(x,y)El,且 x ∈ S , y ∉ T x\in S,y\notin T xS,y/T

这个推论表明我们可以采用上面描述的 α \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=minxS,y/T{w(x,y)l(x)l(y)}=(x1,y1):210080=130(x1,y2):90065=25(x1,y3):1800105=75(x1,y4):1600120=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)是新加入的一条边求解分配问题(三) 二分图最小权重匹配_第6张图片
这样,我们就可以将匹配数扩充到2了:
求解分配问题(三) 二分图最小权重匹配_第7张图片

完整算法

总结一下完整的算法步骤:

  1. 初始化 f e a s i b l e   l a b e l l i n g feasible\ labelling feasible labelling 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} xX,l(x)=0yY,l(y)=minxX{w(x,y)}
  2. X X X中找一个未匹配的点 u u u,以 u u u为root结点,在 E l E_l El中搜索增广路径,同时记录当前最长的交替路径,如果能找到增广路径,执行4;如果找不到,把交替路径中属于 X X X的顶点归入 S S S,属于 Y Y Y的顶点归入 T T T,执行3
  3. 计算 α l \alpha _l αl
    α l = m i n x ∈ S , y ∉ T { w ( x , y ) − l ( x ) − l ( y ) } \begin{aligned} \alpha _l=min_{x\in S,y\notin T}\{w(x,y)-l(x)-l(y)\} \end{aligned} αl=minxS,y/T{w(x,y)l(x)l(y)}
    α l \alpha _l αl更新 l l l
    l ′ ( v ) = { l ( v ) + α l , v ∈ S l ( v ) − α l , v ∈ T l ( v ) , o t h e r s l'(v)= \begin{cases} l(v)+\alpha _l,v\in S\\ l(v)-\alpha_l,v\in T\\ l(v), others \end{cases} l(v)=l(v)+αl,vSl(v)αl,vTl(v),others
    回到2
  4. 对增广路径取反,得到新的匹配 M M M,如果 M M M是完美匹配,退出算法;否则回到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())

结果:
求解分配问题(三) 二分图最小权重匹配_第8张图片

你可能感兴趣的:(Intelligence,Solution,运筹优化)