1. 程式人生 > >匈牙利演算法詳解(含時間複雜度)

匈牙利演算法詳解(含時間複雜度)

尋找二部圖最大匹配的匈牙利數學家埃德蒙德斯在1965年提出的一個簡化的最大流演算法。該演算法根據二部圖匹配這個問題的特點將最大流演算法進行了簡化,提高了效率。

普通的最大流演算法一般都是基於帶權網路模型的,二部圖匹配問題不需要區分圖中的源點和匯點,也不關心邊的方向,因此不需要複雜的網路圖模型,這就是匈牙利演算法簡化的原因。

正是因為這個原因,匈牙利演算法成為一種很簡單的二分匹配演算法,其基本流程是:

將圖 G 最大匹配初始化為空
while(從Xi點開始在圖G中找到新的增廣路徑)
{
    將增廣路徑假如到最大匹配中;
}
輸出圖 G 的最大匹配;

根據匈牙利演算法的流程看,尋找圖 G 中的增廣路徑是匈牙利演算法的關鍵。先來看看什麼是增廣路徑,二部圖中的增廣路徑具有以下性質:
  • 路徑中邊的條數是奇數;
  • 路徑的起點在二部圖的左半邊,終點在二部圖的右半邊;
  • 路徑上的點一個在左半邊,一個在右半邊,交替出現,整條路徑上沒有重複的點;
  • 只有路徑的起點和終點都是未覆蓋的點,路徑上其他的點都已經配對;
  • 對路徑上的邊按照順序編號,所有奇數編號的邊都不在已知的匹配中,所有偶數編號的邊都在已知的匹配中;
  • 對增廣路徑進行“取反”操作,新的匹配數就比已知匹配數增加一個,也就是說,可以得到一個更大的匹配;

所謂的增廣路徑取反操作,就是把增廣路徑上奇數編號的邊加入到已知匹配中,並把增廣路徑上偶數編號的邊從已知匹配中刪除。每做一次“取反”操作,得到的匹配就比原匹配多一個。

匈牙利演算法的思路就是不停地尋找增廣路徑,增加匹配的個數,當不能再找到增廣路徑時,演算法就結束了,得到的匹配就是最大匹配。

增廣路徑的起點總是在二部圖的左邊,因此尋找增廣路徑的演算法總是從一側的頂點開始,逐個頂點搜尋。從 Xi
頂點開始搜尋增廣路徑的流程如下:

while(從 Xi 的鄰接表中找到下一個關聯頂點 Yj)
{
    if(頂點 Yj 不在增廣路徑上)
    {
        將 Yj 加入增廣路徑;
        if(Yj 是未覆蓋點或者從與 Yj 相關連的頂點(Xk)能找到增廣路徑)
        {
            將 Yj 的關聯頂點修改為 Xi
            從頂點 Xi 開始有增廣路徑,返回 true;
        }
    }
    從頂點 Xi 開始沒有增廣路徑,返回 false;
}

在這個演算法流程中,“從與 Yj 相關連的頂點(Xk)能找到增廣路徑”這一步體現的是一個遞迴過程。因為如果之前的搜尋已經將 Yj
加入到增廣路徑中,說明 Yj 在 X 集合中一定有一個關聯點,我們假設 Yj 在 X 集合中的這個關聯點是 Xk,所以要從 Xk 開始繼續尋找增廣路徑。當從 Xk 開始的遞迴搜尋完成後,通過“將 Yj 的關聯頂點修改為 Xi”這一步操作,將其與 Xi 連在一起,形成一條更長的增廣路徑。

到現在為止,匈牙利演算法的流程已經很清楚了,現在我們來給出實現程式碼。

首先定義求最大匹配的資料結構,這個資料結構要能表示二部圖的邊的關係,還要能體現最終的增廣路徑結果,給出如下定義:
typedef struct tagMaxMatch
{
    int edge[UNIT_COUNT][UNIT_COUNT];
    bool on_path[UNIT_COUNT];
    int path[UNIT_COUNT];
    int max_match;
}GRAPH_MATCH;
edge 是頂點與邊的關係表,用來表示二部圖,on_path 用來表示頂點 Yj 是否已經在當前搜尋過程中形成的增廣路徑上了,path 是當前找到的增廣路徑,max_match 是當前增廣路徑中邊的條數,當演算法結束時,如果 max_match 不等於頂點個數,說明有頂點不在最大增廣路徑上,也就是說,找不到能覆蓋所有點的增廣路徑,此二部圖沒有最大匹配。

從 Xi 尋找增廣路徑的演算法實現如下:
bool FindAugmentPath(GRAPH_MATCH *match, int xi)
{
    for(int yj = 0; yj < UNIT_COUNT; yj++)
    {
        if((match->edge[xi][yj] == 1) && !match->on_path[yj])
        {
            match->on_path[yj] = true;
            if( (match->path[yj] == -1)
                || FindAugmentPath(match, match->path[yj]) )
            {
                match->path[yj] = xi;
                return true;
            }
        }
    }
    return false;
}
演算法實現基本上是按照之前的演算法流程實現的,不需要做特別說明,唯一需要注意的是 path 中存放增廣路徑的方式。讀者可能已經注意到了,存放的方式是以 Y 集合中的頂點為索引存放,其值是對應的關聯頂點在 X 集合中的索引。搜尋是按照 X 集合中的頂點索引進行的,增廣路徑以 Y 集合中的頂點為索引儲存,關係是反的。

輸出結果的時候,需要結合 Y 集合中的頂點索引輸出,如果需要以 X 集合的順序輸出結果,需要反向轉換,轉換的方法非常簡單:
int path[UNIT_COUNT] = { 0 };
for(int i = 0; i < match->max_match; i++)
{
    path[match->path[i]] = i;
}
轉換後 path 中就是以 X 集合的順序存放的結果。結合之前給出的匈牙利演算法基本流程,最後給出匈牙利演算法的入口函式實現:
bool Hungary_Match(GRAPH_MATCH *match)
{
    for(int xi = 0; xi < UNIT_COUNT; xi++)
    {
        if(FindAugmentPath(match, xi))
        {
            match->max_match++;
        }

        ClearOnPathSign(match);
    }
    return (match->max_match == UNIT_COUNT);
}
每完成一個頂點的搜尋,需要重置 Y 集合中相關頂點的 on_path 標誌,ClearOnPathSign() 函式就負責幹這個事情。

圖 1 二部圖
我們用圖 1 中的二部圖資料初始化 GRAPH_MATCH 中的頂點關係表 edge,然後呼叫 Hungary_Match() 函式得到一組匹配:

X1<--->Y3
X2<--->Y1
X3<--->Y4
X4<--->Y2
X5<--->Y5

結果與圖 2 一致,因為這個最大匹配沒有未覆蓋點,所以是完美匹配。

圖 2
匈牙利演算法的實現以頂點集合 V 為基礎,每次 X 集合中選一個頂點 Xi 做增廣路徑的起點搜尋增廣路徑。搜尋增廣路徑需要遍歷邊集 E 內的所有邊,遍歷方法可以採用深度優先遍歷(DFS),也可以採用廣度優先遍歷(BFS),無論什麼方法,其時間複雜度都是 O(E)

匈牙利演算法每個頂點 Vi 只能選擇一次,因此演算法的整體時間複雜度是 O(V*E),總的來說,是一個相當高效的演算法。