匈牙利算法


  首先來了解下一些概念性的東西。

二分圖:  

  二分圖又稱作二部圖,是圖論中的一種特殊模型。 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集(i in A,j in B),則稱圖G為一個二分圖。圖一就是一個二分圖。

匈牙利算法:

  匈牙利算法是由匈牙利數學家Edmonds於1965年提出,因而得名。匈牙利算法是基於Hall定理中充分性證明的思想,它是一種用增廣路徑求二分圖最大匹配的算法。

Hall定理:

  二部圖G中的兩部分頂點組成的集合分別為X, Y; X={X1, X2, X3,X4, .........,Xm}, Y={y1, y2, y3, y4 , .........,yn}, G中有一組無公共點的邊,一端恰好為組成X的點的充分必要條件是:X中的任意k個點至少與Y中的k個點相鄰。(1≤k≤m)

匹配:

  給定一個二分圖G,在G的一個子圖M中,M的邊集中的任意兩條邊都不依附於同一個頂點,則稱M是一個匹配。圖一中紅線為就是一組匹配。

未蓋點:

  設Vi是圖G的一個頂點,如果Vi 不與任意一條屬於匹配M的邊相關聯,就稱Vi 是一個未蓋點。如圖一中的a3、b1。

交錯路:

  設P是圖G的一條路,如果P的任意兩條相鄰的邊一定是一條屬於M而另一條不屬於M,就稱P是一條交錯路。如圖一中a2->b2->a1->b4。

可增廣路:

  兩個端點都是未蓋點的交錯路叫做可增廣路。如圖一中的b1->a2->b2->a1->b4->a3。

頂點的數目:

  圖中頂點的總數。

最大獨立數:

  從V個頂點中選出k個頂,使得這k個頂互不相鄰。 那么最大的k就是這個圖的最大獨立數。

最小頂點覆蓋數:

  用最少的頂點數k來覆蓋圖的所有的邊,k就是這個圖的最小頂點覆蓋數。

最大匹配數:

  所有匹配中包含的邊數最多的數目稱為最大匹配數。

頂點的數目=最大獨立數+最小頂點覆蓋數(對於所有無向圖都有效)

最大匹配數=最小頂點覆蓋數(只對二分圖有效)

 

  這么多概念羅列出來了,下面放個實際中可能遇到的問題。其實就是算法競賽中的一道題目,簡化闡述下。

問題:

  幼兒園有G個女孩、B個男孩。女孩之間都相互認識,男孩之間都相互認識,部分男女之間相互認識。要求從中選出一部分小孩,他們之間都要相互認識,求能從中選出的最多人數。

分析:

  可以按男女畫出二分圖。因為要求選出的小孩都要相互認識,則可以在相互不認識的小孩之間連線,這樣只要小孩之間沒有線直接相連,那么他們就肯定就是相互認識的了。也就因此轉化為了求這個二分圖的最大獨立數。

  為了便於理解,首先我們來做個游戲。假設現在有5個男孩(b1,b2,b3,b4,b5)、五個女孩(g1,g2,g3,g4,g5)。b1不認識g1,g2;b2不認識g2,g3;b3不認識g2,g5;b4不認識g3;b5不認識g3,g4,g5。男孩女孩站兩排,相互不認識的他們之間用一根線相連。得到的圖如下:

  因為相互不認識的小孩之間會有一根線,所以如果我們想得到相互都認識的小孩,那么最終留下的小孩他們的手里都不能握有線了,很簡單如果他們手中還有線,那就說明留下的人還有他不認識的。這樣,之前的問題也就轉變為了如何去除最少的小孩,使留下的小孩手中沒有線。再換一種說法,也就是怎么在這么多小孩中找出最少的人,他們握有所有的線。這下就轉換為了尋找最小頂點覆蓋數。

  這樣如果找到了最小頂點覆蓋數,我們又知道頂點數(所有的小孩的數目),就可以求出最大獨立數(現場留下的小孩)。

  又因為對於二分圖,最大匹配數=最小頂點覆蓋數。這個問題進而也就變成了求解最大匹配數。而匈牙利算法正是用來求最大匹配數的一個很好的方法。

  

  下面我們就來看看匈牙利算法的具體流程。

  上面的流程有些抽象,具體要怎么來找增廣路呢,下面給出具體操作的流程圖。

  是不是感覺有些亂,讓我們來一步一步的分析。為了簡化步驟我們用下圖來分析

第一個最外層的循環從x1開始:

  按照流程圖,第一次肯定有xi了,清空標記后,yi肯定也是沒有標記的了。然后對yi進行標記,第一次M自然也是空的了,M中加入(x1,y1)得到新的M{(x1,y1)},於是得到下圖。

最外層循環到了x2:

  好,接下來最大匹配數加1,循環繼續。下面就該輪到x2了。首先清空Y的標記。

  這時x2再找到y1時它已經沒有標記了,這時再來標記它。但y1已經在M中了,它的對應頂點為x1。所以接下來x1要更新關聯點。

  由x1開始查找時,在Y中y1已經被標記了,只能找下一個頂點,於是便找到了y2。y2未被標記,標記它。x1的關聯點更新為y2,x2的關聯點更新為y1。因此得新的M為{(x2,y1),(x1,y2)}

   好,最大匹配數加1,循環繼續。清空Y中標記。

最外層循環到了x3:

  下面就輪到x3了。x3找到y1,y1未標記,標記之。

  y1的關聯是x2。又輪到x2找了。x2找到y1,但y1已被標記,於是便找到y2。y2未被標記,標記之。

  y2的關聯為x1,x1開始找。x1找到y1,y1已被標記,找到y2,y2已被標記。找到y3,y3未被標記,標記之。同時y3沒有關聯。更改x1的關聯為y3。

  原(x1,y2)的關聯也因為x1的改變,變為了(x2,y2)

  同時新增關聯(x3,y1)更新M為{(x3,y1),(x2,y2),(x1,y3)}。最大匹配數加1。這時已經沒有更多的X中頂點可選了。最大匹配數就這樣被找出來了。

  細心的人可能已經看出來了,有M'{(x2,y1),(x1,y2)},最后找出的路徑x3->y1->x2->y2->x1->y3是一條增廣路徑。

  下面說一些增廣路的特性,匈牙利法的正確性驗證自己有興趣可以證明下。

  (1)有奇數條邊。
  (2)起點在二分圖的左半邊,終點在右半邊。
  (3)路徑上的點一定是一個在左半邊,一個在右半邊,交替出現。
  (4)整條路徑上沒有重復的點。
  (5)路徑上的所有第奇數條邊都不在原匹配中,所有第偶數條邊都出現在原匹配中。
  (6)把增廣路徑上的所有第奇數條邊加入到原匹配中去,並把增廣路徑中的所有第偶數條邊從原匹配中刪除(這個操作稱為增廣路徑的取反),則新的匹配數就比原匹配數增加了1個。

  接下來就要用計算機來實現這個算法的過程了。相信有了上面的基礎,已經不難完成了。

  我是用c++編譯的,程序很多地方肯定還有待優化,主要是為了展示算法流程。到這里至少應該對匈牙利算法有所了解了,算法講解到此結束。

#include <stdio.h>
#include <string.h>

#define MAXNUM 1000
//遞歸
//xi 二分圖左部中的頂點
//ytotal 二分圖右部頂點總數
//relation xy之間的關聯關系
//link xy之間的匹配
//y的標記
bool recursion(const int xi, const int ytotal, const bool relation[][MAXNUM], int link[], bool* sign)
{
    for(int i = 0; i < ytotal; i++)
    {
        if(relation[xi][i] && !sign[i])//有關聯並且沒被標記
        {
            sign[i] = true;//標記
            if(link[i] == -1 || recursion(link[i], ytotal, relation, link, sign))//y沒有有匹配則更新y的匹配;y有匹配則用它的匹配繼續查找
            {
                link[i] = xi;//更新y的匹配 
                return true;
            }    
        }
    }
    return false;
}

//匈牙利算法
//xtotal 二分圖的左部包含頂點總數
//ytotal 二分圖的右部包含頂點總數
//xy之間的關聯
int Hungary(const int xtotal, const int ytotal,const bool relation[][MAXNUM])
{
    int link[MAXNUM][2];//與y匹配的x
    memset(link, -1, sizeof(link));
    int cnt = 0;//最大匹配數
    for(int i = 0; i < xtotal; i++)
    {
        bool sign[MAXNUM] = {false};//清空標記
        if(recursion(i, ytotal, relation, link[i], sign))//尋找增廣路徑
        {
            cnt++;
        }
    }
    return cnt;
}

int main(int argc, char** argv)
{
    while(true)
    {
        int boytotal = 0;
        int girltotal = 0;
        bool relation[MAXNUM][MAXNUM] = {false};
        //獲取男孩總數
        do 
        {
            fflush(stdin);
            printf("請輸入男孩總數(不大於%d):\n", MAXNUM);
            scanf("%d",&boytotal);
        }while(0 == boytotal || boytotal > MAXNUM);
        printf("男孩總數輸入成功。\n");
        printf("------------------------\n");

        //獲取女孩總數
        do 
        {
            fflush(stdin);
            printf("請輸入女孩總數(不大於%d):\n", MAXNUM);
            scanf("%d",&girltotal);
        }while(0 == girltotal || girltotal > MAXNUM);
        printf("女孩總數輸入成功。\n");
        printf("------------------------\n");

        //獲取相互不認識的男女
        do
        {
            int boyno = 0;
            int girlno = 0;
            fflush(stdin);
            printf("請輸入相互不認識的異性小孩,如第一個男孩不認識第二個女孩則輸入1,2:\n");
            scanf("%d,%d",&boyno, &girlno);
            if(boyno>0&&boyno<=boytotal&&girlno>0&&girlno<=girltotal)
            {
                relation[boyno-1][girlno-1] = true;
                char ret = '\0';
                fflush(stdin);
                printf("一對互不認識的男女輸入成功,是否結束輸入?(Y/N)\n");
                scanf("%c",&ret);
                if('Y' == ret || 'y' == ret)
                {
                    break;
                }
            }
        }while(true);
        printf("------------------------\n");

        
        printf("男孩個數:%d;女孩個數:%d;小孩總數:%d\n",boytotal,girltotal,boytotal+girltotal);
        //尋找最大匹配
        int tmp = Hungary(boytotal, girltotal, relation);
        printf("最大匹配數:%d\n", tmp);
        printf("最多可以留下人數:%d\n", boytotal+girltotal-tmp);
        printf("------------------------\n");

        char ret;
        fflush(stdin);
        printf("是否退出?(Y/N)\n");
        scanf("%c",&ret);
        if('Y' == ret || 'y' == ret)
        {
            break;
        }
        printf("------------------------\n");
    }
    
    return 1;
}

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM