1. 程式人生 > >匈牙利算法、KM算法

匈牙利算法、KM算法

eof 就是 ati 例子 jpg 模板 優化 typedef 過程

PS:其實不用理解透增廣路,交替路,網上有對代碼的形象解釋,看懂也能做題,下面我盡量把原理說清楚

  1. 基本概念 (部分來源、部分來源)

    二分圖: 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集(i in A,j in B

    ),則稱圖G為一個二分圖。技術分享圖片

    匹配: 一個匹配即一個包含若幹條邊的集合,且其中任意兩條邊沒有公共端點。下圖標紅的邊即為匹配

技術分享圖片

? 最大匹配: 匹配數最大,如下圖有4條匹配邊技術分享圖片

? 完全匹配: 即所有的頂點都是匹配點,上圖(Fig.4)即為完全匹配,不是所有的圖都存在完全匹配

? 交替路:

從未匹配點出發,依次經過未匹配的邊和已匹配的邊,即為交替路,如Fig.3:3 -> 5 -> 1 -> 7 -> 4 -> 8

? 增廣路(也稱增廣軌或交錯軌): 如果交替路經過除出發點外的另一個未匹配點,則這條交替路稱為增廣路,如交替路概念的例子,其途徑點8,即為增廣路

  1. 匈牙利算法

    由增廣路的定義推出下面三個結論(設P為一條增廣路):

    1). P的路徑長度一定為奇數,第一條邊和最後一條邊都是未匹配的邊(根據要途經已匹配的邊和要經過另一個未匹配點,這個結論可以理解成第一個點和最後一個點都是未匹配點,可以在Fig.3上的增廣路觀察到)

    2). P經過取反操作可以得到一個更大的匹配圖(取反操作即,未匹配的邊變成匹配的邊,匹配的邊變成未匹配的邊,這個結論根據結論1).和交替路概念可得該結論)

    3). 當且僅當不存在關於圖M的增廣路徑,則圖M為最大匹配

    算法思路:不停找增廣路,並增加匹配的個數

    代碼過程模擬(圖片來源)

    技術分享圖片

    技術分享圖片

    技術分享圖片

    技術分享圖片

這裏就解釋第三幅圖到第四幅圖的過程,註意這裏的過程就是找增廣路。圖三開始,男3可以匹配女1,(男3 -> 女1),發現女1已經跟男1匹配(男3 -> 女1 -> 男1),看到男1可以跟女2匹配(註意從開始到現在的是未匹配邊,匹配邊,未匹配邊)這樣走下去,路徑是男3 -> 女1 -> 男1 -> 女2 -> 男2 -> 女3,此時是以未匹配邊結束且找不到匹配邊了。

求最大匹配的模板

#include <iostream>
#include <algorithm>

using namespace std;

const int MAXN = 105;

int x, y, mp[MAXN][MAXN];
int mx[MAXN], my[MAXN];//x 跟哪個 y 匹配s
int vis[MAXN];//一條增廣路中哪些 y 點被訪問過

int dfs(int a)
{
    for(int i = 1;i <= n;++i)
    {
        //先找出與 a 點有聯系且沒被訪問過的 y 點
        if(!vis[i] && mp[a][i])
        {
            vis[i] = 1;
            //然後從就是 x -> i -> my[i] 構造這條增廣路,接著就是 dfs(my[i]),重復以上操作
            //根據增廣路的結論,如果存在增廣路,那麽最後 my[i] == 0,即為未匹配點
            //然後將其賦新值,根據 DFS 遞歸各層,該賦值即為增廣路中進行取反操作,未匹配邊變為匹配邊
            //至於匹配邊變為未匹配邊,my[i] 被賦新值
            //若不存在增廣路,那麽就沒進行賦值,即不取反
            if(!my[i] || dfs(my[i]))
            {
                my[i] = a;
                return 1;
            }
        }
    }
    return 0;
}

int solve()
{
    int ret = 0;
    fill(mx, mx + MAXN, 0);
    fill(my, my + MAXN, 0);
    //剛開始的 x 都是未匹配點
    //vis 每次清零,vis 就是增廣路中的 y 點
    for(int i = 1;i <= x;++i)
    {
        fill(vis, vis + MAXN, 0);
        ret += dfs(i);
    }
    return ret;
}

求最大匹配的模板題

#include <iostream>
#include <cstring>

using namespace std;

const int MAXN = 505;//這題數組要開大

int x, y, mp[MAXN][MAXN];
int mx[MAXN], my[MAXN];//x 跟哪個 y 匹配s
int vis[MAXN];//一條增廣路中哪些 y 點被訪問過

int dfs(int a)
{
    for(int i = 1;i <= y;++i)
    {
        if(!vis[i] && mp[a][i])
        {
            vis[i] = 1;
            if(!my[i] || dfs(my[i]))
            {
                my[i] = a;
                return 1;
            }
        }
    }
    return 0;
}

int solve()
{
    int ret = 0;
    memset(mx, 0, sizeof(mx));
    memset(my, 0, sizeof(my));
    for(int i = 1;i <= x;++i)
    {
        fill(vis, vis + MAXN, 0);
        ret += dfs(i);
    }
    return ret;
}

int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        cin >> x >> y;
        memset(mp, 0, sizeof(mp));
        int num, t;
        for(int i = 1;i <= x;++i)
        {
            cin >> num;
            while(num--)
            {
                cin >> t;
                mp[i][t] = 1;
            }
        }
        if(solve() == x)
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
    }
    return 0;
}

時間復雜度:鄰接矩陣最壞O(n ^ 3), 鄰接表O(nm)

空間復雜度:鄰接矩陣O(n ^ 2), 鄰接表O(n + m)

擴展:匈牙利算法也可以看成DFS過程,參考

  1. KM算法

    該算法是求帶權二分圖的最大權匹配

    此篇易懂,新概念少,沒代碼,概念看不懂的,建議先看左邊易懂那篇再來,這個是代碼篇

    頂標:設頂點 Xi 的頂標為 A[i],頂點 Yi 的頂標為 B[j],頂點 Xi 與 Yj 之間的邊權為 w[i][j],初始化時,A[i] 的值為與該點關聯的最大邊權值,B[j] 的值為0

    相等子圖:選擇 A[i] + B[j] = w[i][j] 的邊 <i, j> 構成的子圖,就是相等子圖

    算法執行過程中,對任一條邊<i, j>,A[i] + B[j] >= w[i][j] 恒成立,這個下面圖示解釋清楚

    這裏的算法介紹是用slack[j]數組優化過,slack數組存的數是Y部的點相等子圖時,最小要增加的值

    沒有優化過的容易理解,代碼量也短,去原博客先看完沒有優化的會更容易理解優化過的代碼

    算法圖示:

    給個二分圖,只取邊權跟頂標相等的邊,邊權值跟頂標理解為工作效率

    技術分享圖片

    看到員工B,B與c以外的其他工作嘗試匹配後,頂標和,即上面說的A[i] + B[j] >= w[i][j],並且最小的差值為1,該差值存在slack數組裏,當B與c嘗試匹配,因為A -> c,就能找到一條增廣路,B -> c -> A -> a,此時A只有找到a匹配,但是A -> a 的頂標和 = 4 也是大於邊權值 3,與slack數組比較,存儲最小的差值1。

    因為能夠進行匹配的是相等子圖,即不存在此相等子圖的減少量,所以遍歷未匹配的Y部的slack數組,找出最小的減少量minz。我們要使總的工作效率盡可能地高,即減少地少。

    若X部的點還未匹配,就將增光路中X部減去minz,即下圖中A、B都減去1,Y部加上minz,再進行嘗試匹配,註意,匹配成功的話,A[i] + B[j] = w[i][j]

    技術分享圖片

    其實下面這圖不符合匈牙利算法的具體過程,由於這裏只要理解結果,就拿下面這圖了,這裏X部+Y部的頂標值 = 對應點的邊權值。正確的連接應該是(看上圖),B -> c, A -> a,因為根據匈牙利算法所形成的增廣路應該是 B -> c -> A -> a, 在這條增廣路中只有 A -> c是匹配的,然後取反,就B -> c, A -> a

    技術分享圖片

    由於圖到此就錯了,所以就不繼續貼流程圖,直接上結果圖,具體過程如上重復技術分享圖片

    下面是以上推薦博客的代碼,跑了一遍測試,我就把maxn改了

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<vector>
    #include<map>
    using namespace std;
    typedef long long ll;
    const int maxn = 300 + 10;
    const int INF = 0x3f3f3f3f;
    
    int wx[maxn], wy[maxn];//每個點的頂標值(需要根據二分圖處理出來)
    int cx[maxn], cy[maxn];//每個點所匹配的點
    int visx[maxn], visy[maxn];//每個點是否加入增廣路
    int cntx, cnty;//分別是X和Y的點數
    int Map[maxn][maxn];//二分圖邊的權值
    int slack[maxn];//邊權和頂標最小的差值
    
    bool dfs(int u)//進入DFS的都是X部的點
    {
        visx[u] = 1;//標記進入增廣路
        for(int v = 1; v <= cnty; v++)
        {
            if(!visy[v] && Map[u][v] != INF)//如果Y部的點還沒進入增廣路,並且存在路徑
            {
                int t = wx[u] + wy[v] - Map[u][v];
                if(t == 0)//t為0說明是相等子圖
                {
                    visy[v] = 1;//加入增廣路
    
                    //如果Y部的點還未進行匹配
                    //或者已經進行了匹配,可以從原來的匹配反向找到增廣路
                    //那就可以進行匹配
                    if(cy[v] == -1 || dfs(cy[v]))
                    {
                        cx[u] = v;
                        cy[v] = u;//進行匹配
                        return 1;
                    }
                }
                else if(t > 0)//此處t一定是大於0,因為頂標之和一定>=邊權
                {
                    slack[v] = min(slack[v], t);
                    //slack[v]存的是Y部的點需要變成相等子圖頂標值最小增加多少
                }
            }
        }
        return false;
    }
    
    int KM()
    {
        memset(cx, -1, sizeof(cx));
        memset(cy, -1, sizeof(cy));
        memset(wx, 0, sizeof(wx));//wx的頂標為該點連接的邊的最大權值
        memset(wy, 0, sizeof(wy));//wy的頂標為0
        for(int i = 1; i <= cntx; i++)//預處理出頂標值
        {
            for(int j = 1; j <= cnty; j++)
            {
                if(Map[i][j] == INF)continue;
                wx[i] = max(wx[i], Map[i][j]);
            }
        }
        for(int i = 1; i <= cntx; i++)//枚舉X部的點
        {
            memset(slack, INF, sizeof(slack));
            while(1)
            {
    
                memset(visx, 0, sizeof(visx));
                memset(visy, 0, sizeof(visy));
                if(dfs(i))break;//已經匹配正確
    
    
                int minz = INF;
                for(int j = 1; j <= cnty; j++)
                    if(!visy[j] && minz > slack[j])
                        //找出還沒經過的點中,需要變成相等子圖的最小額外增加的頂標值
                        minz = slack[j];
                //和全局變量不同的是,全局變量在每次while循環中都需要賦值成INF,每次求出的是所有點的最小值
                //而slack數組在每個while外面就初始化好,每次while循環slack數組的每個值都在用到
                //在一次增廣路中求出的slack值會更準確,循環次數比全局變量更少
    
    
                //還未匹配,將X部的頂標減去minz,Y部的頂標加上minz
                for(int j = 1; j <= cntx; j++)
                    if(visx[j])wx[j] -= minz;
                for(int j = 1; j <= cnty; j++)
                    //修改頂標後,要把所有不在交錯樹中的Y頂點的slack值都減去minz
                    if(visy[j])wy[j] += minz;
                    else slack[j] -= minz;
            }
        }
    
        int ans = 0;//二分圖最優匹配權值
        for(int i = 1; i <= cntx; i++)
            if(cx[i] != -1)ans += Map[i][cx[i]];
        return ans;
    }
    int n, k;
    int main()
    {
        while(scanf("%d", &n) != EOF)
        {
            for(int i = 1; i <= n; i++)
            {
                for(int j = 1; j <= n; j++)
                    scanf("%d", &Map[i][j]);
            }
            cntx = cnty = n;
            printf("%d\n", KM());
        }
        return 0;
    }
    

匈牙利算法、KM算法