1. 程式人生 > >啊哈演算法(8)——更多精彩的演算法

啊哈演算法(8)——更多精彩的演算法

1、圖的最小生成樹(Kruskal演算法)
對於一個給定的圖,找出其最小生成樹,用最少的邊讓n個頂點的圖連通,很顯然若要讓n個頂點的圖連通,最少要n-1條邊,最小生成樹還需要滿足這n-1條邊的權重和最小。例如對於如下輸入例項:

6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2

第一行n和m,n表示有n個頂點,m表示有m條路徑,接下來的m行形如a b c表示頂點a到頂點b的權重為c。
Kruskal演算法的核心為:首先對邊按照權重進行排序,每一次從剩餘的邊中選擇權值較小的並且不會產生迴路的邊,加入到生成樹中,直到加入了n-1條邊為止。實現如下:

/*Kruskal演算法,最小生成樹*/

#include<iostream>

using namespace std;

struct edge {
    int u;
    int v;
    int w;
};//為了方便按照邊來排序使用一個結構體來存放邊的資訊

struct edge e[10];// 結構體陣列存放邊的資訊

int f[7];    //最多輸入1000個節點
int n, m;//節點個數和線索數目

//對邊進行排序
void quicksort(int left, int right)
{
    //選擇基準
    struct edge t = e[left];
    if
(left > right) return; int i = left; int j = right; while (i!=j) { while (e[j].w >=t.w&&i < j) j--; while (e[i].w <=t.w&&i < j) i++; //交換 if (i < j) { struct edge temp = e[i]; e[i] = e[j]; e[j] = temp; } } //基準歸位
e[left] = e[i]; e[i] = t; quicksort(left, i - 1); //遞迴處理左邊 quicksort(i+1,right); //遞迴處理右邊 } //使用並查集來判斷兩個邊是不是在一個集合中 ,比用DFS快 void init() //剛開始每個節點都是孤立的 { for (int i = 1; i <= n; i++) { f[i] = i; } } //尋找祖宗的過程 int getf(int v) { if (f[v] == v) return v; else { //向上繼續尋找其祖宗,並且在遞迴函式返回的時候,把中間的父節點都改為最終找到的祖宗的編號 //這其實就是路徑壓縮,可以提高以後找到最高祖宗的速度 f[v] = getf(f[v]); return f[v]; } } //合併兩個子集合 int merge(int v, int u) { int t1 = getf(v); int t2 = getf(u); if (t1 != t2) { f[t2] = t1;// 遵循靠左合併的原則 return 1;//不在一個集合中 就返回1 } return 0; } int main() { int sum = 0; int count = 0; cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> e[i].u >> e[i].v >> e[i].w; } quicksort(1, m); //對邊進行排序 //並查集初始化 最開始每個節點都是一個獨立的節點 init(); //Kruskal演算法 for (int i = 1; i <= m; ++i) { //判斷兩個頂點是不是在同一個集合中 if (merge(e[i].u, e[i].v)) { //不在一個樹中就將這條邊加入生成樹中 cout << e[i].u << " " << e[i].v << " " << e[i].w << endl; count++; sum += e[i].w; } if (count == n - 1) //當選用了n-1條邊之後 就退出 break; } cout << sum; system("pause"); }

Kruskal演算法的時間複雜度為:對邊進行快排是O(MlogM),在m條邊中找出n-1條邊是O(MlogN),所以Kruskal演算法的時間複雜度為O(MlogM+MlogN)。

2、最小生成樹(prim演算法)
prim演算法的核心步驟如下:

  1. 從任意一個定點開始構造生成樹,假設從一號頂點開始。首先將一號頂點加入生成樹中,用一個數組book來標記哪些頂點已經加入到生成樹中了。
  2. 用dis陣列記錄生成樹到各個頂點的距離。最初生成樹中只有一號頂點,有直連邊時,陣列dis中儲存的就是1號頂點到該頂點的邊的權值,沒有直連邊的時候就是無窮大,即初始化dis陣列。
  3. 從陣列中選出離生成樹最近的頂點(假設頂點為j)加入到生成樹中(在dis陣列中找到最小值)。在以j為中間點,更新生成樹到每一個非樹頂點的距離(鬆弛),即如果dis[k]>e[j][k]則更新dis[k]=e[j][k]。
  4. 重複第三步
    例如對於如下示例:
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2

第一行n和m,n表示有n個頂點,m表示有m條路徑,接下來的m行形如a b c表示頂點a到頂點b的權重為c。實現程式碼如下:

/*prim演算法,使用鄰接矩陣存圖*/

#include<iostream>

using namespace std;

#define inf 99999

int e[7][7], dis[7], book[7];
int n, m;   //頂點數和邊數
int main()
{
    cin >> n >> m;

    //鄰接矩陣初始化
    for(int i=1;i<=n;++i)
        for (int j = 1; j <= n; ++j)
        {
            if (i == j)
                e[i][j] = 0;
            else
                e[i][j] = inf;
        }

    int a, b, c;
    for (int i = 1; i <= m; ++i)     //讀入邊,由於為無向圖所以兩邊都要存一下
    {
        cin >> a >> b >> c;
        e[a][b] = c;
        e[b][a] = c;               
    }

    //初始化dis陣列,假定從1號頂點開始
    for (int i = 1; i <= n; ++i)
        dis[i] = e[1][i];

    int count = 0;
    //1號頂點加入到生成樹中
    book[1] = 1;
    count++;

    //prim演算法核心語句
    int sum = 0; //最小生成樹路徑
    while (count<n)                  //總共n-1條邊就可以使得圖連通
    {
        //從非樹節點集合中選擇 離生成樹中最近的點加入到生成樹中
        int min = inf;
        int pos = 0;
        for (int i = 1; i <= n; ++i)
        {
            if (book[i]==0&&dis[i] < min)   
            {
                min = dis[i];
                pos = i;
            }
        }
        book[pos] = 1;
        sum += min;
        count++;
        //以pos點為中心,更新各個非樹節點到生成樹的距離
        for (int i = 1; i <= n; ++i)
        {
            if (book[i] == 0 && dis[i] > e[pos][i])
                dis[i] = e[pos][i];
        }
    }

    cout << sum << endl;
    system("pause");
}

上面這種演算法的時間複雜度為O(N^2),如果使用堆,每一次選邊的時間複雜度是O(logM),然後使用鄰接表來儲存整個圖時間複雜度會降低到O(MlogN)。實現程式碼如下:
使用三個陣列,dis陣列用來記錄生成樹到各個頂點的距離。陣列h是一個最小堆,堆裡面儲存的是頂點編號。這裡並不是按照頂點的編號來建立最小堆,而是按照頂點在dis陣列中的數值來建立最小堆的,此外還需要一個數組pos來記錄每個頂點在最小堆中的位置。

/*prim演算法實現:使用鄰接表來儲存圖並且使用堆優化*/

#include<iostream>
#define inf 999999;

using namespace std;

int dis[7], book[7];// 記錄各個頂點到生成樹的最小距離,判斷頂點是否在生成樹中
int h[7], pos[7]; //h用來儲存堆,pos用來儲存堆中每一個頂點的位置   
int h_size; //size用來表示堆的大小

void swap(int x,int y)
{
    int t = h[x];
    h[x] = h[y];
    h[y] = t;

    //同步更新pos
    t = pos[h[x]];
    pos[h[x]] = pos[h[y]];
    pos[h[y]] = t;
}

void siftdown(int i)  //對堆中編號為i的節點實時向下調整
{
    int t, flag = 0;  //flag用來標記是否需要繼續向下調整
    while (i*2<= h_size&&flag==0)  //左孩子存在 
    {
        if (dis[h[i]] > dis[h[2 * i]])
            t = i * 2;
        else
            t = i;

        //如果有右兒子急需判斷

        if (i * 2 + 1 <= h_size)
        {
            if (dis[h[t]] > dis[i * 2 + 1])
                t = i * 2 + 1;
        }

        if (t != i)
        {
            swap(t, i);
            i = t;       //便於接下來繼續向下調整
        }
        else
            flag = 1;//不在需要向下調整了
    }
}

void siftup(int i)   //對編號i進行向上調整
{
    int flag = 0;
    if (i == 1)
        return;//在堆頂直接返回

    //不在堆頂並
    while (i != 1&&flag==0)
    {
        //與父節點進行比較
        if (dis[h[i / 2]] > dis[h[i]])
            swap(i, i / 2);
        else
            flag = 1;
        i = i / 2;   //更新節點方便下一次使用
    }
}
void create() //建立一個堆
{
    //從最後一個非葉節點開始實行向下調整
    for (int i = h_size / 2; i >= 1; --i)
        siftdown(i);
}

//從堆頂中取出一個元素
int pop()
{
    int t = h[1];
    pos[t] = 0;
    h[1] = h[h_size];
    pos[h[1]] = 1;       //更新頂點h[1]在堆中的位置
    h_size--;
    siftdown(1);        //向下調整
    return t;
}
int main()
{
    int n, m;//頂點個數和邊的個數

    int u[19], v[19], w[19]; //採用鄰接矩陣來儲存圖 表示頂點u[i]到頂點v[i]的權重為w[i]  由於為無向圖實際的大小為2*m+1
    int first[7];           //儲存的是節點i的第一條邊的編號為first[i],大小為n+1;
    int next[19];           //儲存的是編號為i的邊的下一條邊的編號next[i]。

    cin >> n >> m;
    for (int i = 1; i <= m; ++i)
    {
        cin >> u[i] >> v[i] >> w[i];
    }

    //由於為無向圖所以還需要儲存一遍
    for (int i = m + 1; i <= 2 * m; ++i)
    {
        u[i] = v[i - m];
        v[i] = u[i - m];
        w[i] = w[i - m];
    }

    //採用鄰接表來儲存圖,首先對first陣列初始化,最開始沒有讀入邊 所以記錄為-1;
    for (int i = 1; i <= n; ++i)
        first[i] = -1;

    for (int i = 1; i <= 2 * m; ++i)
    {
        next[i] = first[u[i]];
        first[u[i]] = i;
    }

    //prim演算法核心
    int count = 0;
    int sum = 0;
    //1號頂點加入到生成樹中
    book[1] = 1;
    count++;

    //初始化dis陣列
    dis[1] = 0;
    for (int i = 2; i <= n; ++i)
        dis[i] = inf;
    int k = first[1];  //1號節點的第一條邊的編號
    while (k!=-1)
    {
        dis[v[k]] = w[k];
        k = next[k];
    }

    //初始化堆
    h_size = n;
    for (int i = 1; i <= h_size; ++i)
    {
        h[i] = i;
        pos[i] = i;
    }
    create();
    pop();//先彈出堆頂元素 此時堆頂元素是一號頂點

    while (count<n)
    {
        //堆頂元素加入到生成樹當中
        int j = pop();
        book[j] = 1;
        count++;
        sum += dis[j];

        //以j為中心對邊進行鬆弛
        int k = first[j];
        while (k!=-1)
        {
            if (book[v[k]] == 0 && dis[v[k]] > w[k])
            {
                dis[v[k]] = w[k]; //更新距離
                siftup(pos[v[k]]);  //對該頂點在堆中的位置進行鬆弛,pos[i]中存放的是節點i在堆中的位置
            }
            k = next[k];
        }
    }
    cout << sum << endl;
    system("pause");
}

3、圖的割點
對於一個給定的圖,求出圖中的割點,採用深度優先搜尋時訪問到了k點,此時圖就會被k點分割成兩個部分,一部分是已經被訪問過的點,另一部分是沒有訪問過的點。如果k點是割點,那麼剩下的沒有被訪問過的點中至少會有一個點在不經過k點的情況下,是無論如何再也回不到已訪問過的點。演算法的關鍵在於:當深度優先遍歷訪問到頂點u時,其孩子頂點v還是沒有訪問過的,如何判斷頂點v在不經過其父節點u的情況下可以回到祖先。為此使用兩個陣列:1、num記錄dfs訪問到每個節點時的時間戳,2、low記錄每個頂點在不經過其父節點時,能夠回到的最小時間戳。對於某個頂點u,對於其孩子v,使得low[v]>=num[u],即不能回到祖先,那麼u點就為割點。對於如下輸入:

6 7
1 4
1 3
4 2
3 2
2 5
2 6
5 6

第一行為節點個數和邊的個數,實現程式碼如下:

#include<iostream>

using namespace std;

int n, m, e[7][7], root;

//num記錄dfs訪問到每個節點時的時間戳,low記錄每個頂點在不經過其父節點時,能夠回到的最小時間戳。
//flag標記某個點是否為割點,index為時間戳
int num[7], low[7], flag[7], index;  


int min(int a, int b)
{
    return a < b ? a : b;
}

//割點演算法核心
void dfs(int cur, int father)   //傳入兩個引數,當前頂點的編號和父節點的編號
{
    int child = 0, i, j;      //child記錄生成樹中當前頂點cur的兒子個數

    index++;                 //時間戳加1
    num[cur] =index;         //當前頂點的時間戳
    low[cur] = index;        //當前頂點能夠訪問到的時間戳,最開始就是自己
    for (int i = 1; i <= n; ++i)  //列舉當前頂點相連的邊
    {
        if (e[cur][i] == 1)
        {
            if (num[i] == 0)      //如果當前頂點的時間戳為0,說明頂點i還沒有被訪問到
            {
                child++;
                dfs(i, cur);    //對此孩子進行升入遍歷

                //更新當前頂點能夠訪問到最早頂點的時間戳 不能通過父節點就只能通過孩子節點
                low[cur] = min(low[cur], low[i]);

                //如果當前頂點不是根節點並且滿足low[i]>=num[cur],則當前頂點為割點
                if (cur != root && low[i] >= num[cur])
                    flag[cur] = 1;

                //如果當前頂點是根節點,在生成樹中根節點必須要有兩個兒子,那麼這個根節點才是割點
                if (cur == root &&child == 2)
                    flag[cur] = 1;
            }
            //否則如果當前頂點被訪問過,並且這個頂點不是當前頂點cur的父親,則要更新當前節點最早可以訪問到的頂點的時間戳
            else if (i != father)  
                low[cur] = min(low[cur], num[i]);
        }
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            e[i][j] = 0;

    int x, y;
    for (int i = 1; i <= m; ++i)
    {
        cin >> x >> y;
        e[x][y] = 1;
        e[y][x] = 1;
    }

    root = 1;
    dfs(1, root);

    for (int i = 1; i <= n; ++i)
    {
        if (flag[i] = 1)
            cout << i << " ";
    }
    system("pause");
}

上述採用鄰接矩陣來實現,這樣時間複雜度都會在O(N^2),這與使用蠻力,刪除一個點然後判斷剩餘的點是否連通是一樣的,因此要採用鄰接表來儲存圖,時間複雜度為O(N+M),下面採用鄰接表存圖的割點演算法實現程式碼為:

/*割點演算法,採用鄰接表實現*/

#include<iostream>

using namespace std;

int u[19], v[19], w[19];   //採用鄰接表來儲存圖,由於為無向圖,所以大小為2*m+1
int first[7];
int nex[19];
int n, m;
int num[7], low[7], index;
int child, root; //child記錄一個節點的孩子節點 root根節點 
int flag[7];    //用來標記哪些節點為割點
int min(int a, int b)
{
    return a < b ? a : b;
}
void dfs(int cur, int father) //要傳入兩個節點,當前節點和當前節點的父節點
{
    index++;
    num[cur] = index;        //訪問到當前節點的時間戳
    low[cur] = index;        //最開始不經過父節點所能訪問到的節點的時間戳就是本身

    int k = first[cur];        //當前節點的第一條邊
    while (k!=-1)
    {
        if (num[v[k]] == 0)      //要訪問的節點時間戳為0,則說明還沒有訪問
        {
            child++;
            dfs(v[k], u[k]);    // 繼續深入訪問孩子節點,此時u[k]為v[k]的父節點 dfs遍歷就是得到一顆生成樹
            //就更新當前節點不經過父節點所能訪問到的節點的時間戳即low
            low[u[k]] = min(low[u[k]], low[v[k]]);  //由於一個節點要訪問其他節點並且不過父節點只能經過孩子節點,所以與low[v[k]]進行比較

            if (u[k] != root && low[v[k]] >=num[u[k]])  //不為根節點並且v[k]不過父節點u[k]最早能訪問到的節點的時間戳小於父節點u[k]的時間戳 那麼u[k]就為割點
                flag[u[k]] = 1;
            if(u[k]==root && child==2)   //為根節點並且有兩個孩子就為割點
                flag[u[k]] = 1;

        }
        //如果當前節點的所有邊都已經訪問,並且它所能到達的頂點不為其父節點,就更新其不經過父節點所能訪問到的節點的時間戳
        else if (v[k] != father)
            low[u[k]] = min(low[u[k]], num[v[k]]);
        k = nex[k];   //編號為k的邊的下一條邊   
    }
}
int main()
{
    cin >> n >> m;

    //使用鄰接表儲存影象
    for (int i = 1; i <= m; ++i)
    {
        cin >> u[i] >> v[i];
        w[i] = 1;
    }

    //由於為雙向圖
    for (int i = m + 1; i <= 2 * m; ++i)
    {
        u[i] = v[i - m];
        v[i] = u[i - m];
        w[i] = 1;
    }

    //初始化first陣列,由於開始沒有讀入邊的資訊,所以節點第一條邊的編號為-1
    for (int i = 1; i <= n; ++i)
        first[i] = -1;

    //讀入邊
    for (int i = 1; i <= 2 * m; ++i)
    {
        nex[i] = first[u[i]];
        first[u[i]] = i;   
    }

    root = 1;
    dfs(1, root);

    for (int i = 1; i <= n; ++i)
    {
        if (flag[i] == 1)
            cout << i << " ";
    }
    system("pause");
}

4、圖的割邊
割邊也稱為橋,在一個無向連通圖中,如果刪除某條邊之後圖不載連通,這與求圖的割點類似,在求割點的時候(u為父節點,v為子節點)判斷low[v]>=num[u]即在不經過父節點的情況下子節點最早能到的節點最早為父節點,在求割邊的時候將判斷條件改為low[v]>num[u]說明子節點v連父節點都到達不了,那麼就說明u->v這條邊就為割邊,因為v回不到祖先,並且也沒有另外一條路回到父節點,所以該邊為割邊,實現程式碼為:
對於如下輸入:

6 6
1 4
1 3
4 2
3 2
2 5
5 6

使用鄰接表實現,複雜度為O(M+N),實現的程式碼為:

/*割點演算法,採用鄰接表實現*/

#include<iostream>

using namespace std;

int u[19], v[19], w[19];   //採用鄰接表來儲存圖,由於為無向圖,所以大小為2*m+1
int first[7];
int nex[19];
int n, m;
int num[7], low[7], index;
int child, root; //child記錄一個節點的孩子節點 root根節點 
int flag[7];    //用來標記哪些節點為割點
int min(int a, int b)
{
    return a < b ? a : b;
}
void dfs(int cur, int father) //要傳入兩個節點,當前節點和當前節點的父節點
{
    index++;
    num[cur] = index;        //訪問到當前節點的時間戳
    low[cur] = index;        //最開始不經過父節點所能訪問到的節點的時間戳就是本身

    int k = first[cur];        //當前節點的第一條邊
    while (k!=-1)
    {
        if (num[v[k]] == 0)      //要訪問的節點時間戳為0,則說明還沒有訪問
        {
            child++;
            dfs(v[k], u[k]);    // 繼續深入訪問孩子節點,此時u[k]為v[k]的父節點 dfs遍歷就是得到一顆生成樹
            //就更新當前節點不經過父節點所能訪問到的節點的時間戳即low
            low[u[k]] = min(low[u[k]], low[v[k]]);  //由於一個節點要訪問其他節點並且不過父節點只能經過孩子節點,所以與low[v[k]]進行比較

            if (u[k] != root && low[v[k]] > num[u[k]])  //不為根節點並且v[k]不過父節點u[k]最早能訪問到的節點的時間戳小於父節點u[k]的時間戳 那麼u[k]-v[k]就為割邊
                cout << u[k] << "-" << v[k] << endl;
        }
        //如果當前節點的所有邊都已經訪問,並且它所能到達的頂點不為其父節點,就更新其不經過父節點所能訪問到的節點的時間戳
        else if (v[k] != father)
            low[u[k]] = min(low[u[k]], num[v[k]]);
        k = nex[k];   //編號為k的邊的下一條邊   
    }
}
int main()
{
    cin >> n >> m;

    //使用鄰接表儲存影象
    for (int i = 1; i <= m; ++i)
    {
        cin >> u[i] >> v[i];
        w[i] = 1;
    }

    //由於為雙向圖
    for (int i = m + 1; i <= 2 * m; ++i)
    {
        u[i] = v[i - m];
        v[i] = u[i - m];
        w[i] = 1;
    }

    //初始化first陣列,由於開始沒有讀入邊的資訊,所以節點第一條邊的編號為-1
    for (int i = 1; i <= n; ++i)
        first[i] = -1;

    //讀入邊
    for (int i = 1; i <= 2 * m; ++i)
    {
        nex[i] = first[u[i]];
        first[u[i]] = i;   
    }

    root = 1;
    dfs(1, root);

    system("pause");
}

當然也可以使用鄰接矩陣來存圖,但是時間複雜度至少為O(N^2),這樣完全沒有意義,因為完全可以依次刪除一條邊來進行DFS或者BFS來判斷整個圖是不是連通的,這裡還是給出實現程式碼:

#include<iostream>

using namespace std;

int n, m, e[7][7], root;

//num記錄dfs訪問到每個節點時的時間戳,low記錄每個頂點在不經過其父節點時,能夠回到的最小時間戳。
//flag標記某個點是否為割點,index為時間戳
int num[7], low[7], flag[7], index;


int min(int a, int b)
{
    return a < b ? a : b;
}

//割點演算法核心
void dfs(int cur, int father)   //傳入兩個引數,當前頂點的編號和父節點的編號
{
    int child = 0, i, j;      //child記錄生成樹中當前頂點cur的兒子個數

    index++;                 //時間戳加1
    num[cur] = index;         //當前頂點的時間戳
    low[cur] = index;        //當前頂點能夠訪問到的時間戳,最開始就是自己
    for (int i = 1; i <= n; ++i)  //列舉當前頂點相連的邊
    {
        if (e[cur][i] == 1)
        {
            if (num[i] == 0)      //如果當前頂點的時間戳為0,說明頂點i還沒有被訪問到
            {
                child++;
                dfs(i, cur);    //對此孩子進行升入遍歷

                                //更新當前頂點能夠訪問到最早頂點的時間戳 不能通過父節點就只能通過孩子節點
                low[cur] = min(low[cur], low[i]);

                //如果當前頂點不是根節點並且滿足low[i]>num[cur],則cur-i為割邊
                if (cur != root && low[i] > num[cur])
                    cout << cur << "-" << i << endl;

            }
            //否則如果當前頂點被訪問過,並且這個頂點不是當前頂點cur的父親,則要更新當前節點最早可以訪問到的頂點的時間戳
            else if (i != father)
                low[cur] = min(low[cur], num[i]);
        }
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            e[i][j] = 0;

    int x, y;
    for (int i = 1; i <= m; ++i)
    {
        cin >> x >> y;
        e[x][y] = 1;
        e[y][x] = 1;
    }

    root = 1;
    dfs(1, root);

    system("pause");
}

5、二分圖最大匹配
二分圖:如果一個圖的所有頂點可以被分為X和Y兩個集合,並且所有邊的兩個頂點恰好一個屬於集合X,另一個屬於集合Y,即每個集合內的頂點每有邊相連,那麼此圖就是二分圖。
二分圖的最大匹配就是,兩兩通過邊匹配(點不可以重複使用),求出最大的匹配樹,最直觀的解法:找出全部的匹配方案輸出配對數最多的一種方案。但是時間複雜度很高。
使用匈牙利演算法解決這個問題,匈牙利演算法過程如下:

  1. 首先從任意一個未配對的點u開始,從點u的邊中任意選擇一條邊(假設這條邊是u->v)開始配對。如果此時點v還沒有匹配,則配對成功,此時就找到了一條增廣路。如果此時點v已經配對了,就要嘗試進行連鎖反應(即看看v先在配對的點還有沒有其他的點可以配對),如果嘗試成功就找到了一條增廣路,此時需要更新原來的配對關係。這裡使用一個match陣列來記錄配對關係,比如v與u匹配成功就記作match[v]=u和match[u]=v。配對成功後將配對數加1。配對過程使用DFS來實現
  2. 如果剛才嘗試失敗,就從u的邊中重新選擇一條邊進行嘗試,直到點u匹配成功。
  3. 接下來對剩下沒有配對的點一一進行配對,直到所有的點嘗試完畢
    對於如下輸入:
6 5
1 4
1 5
2 5
2 6
3 4

其中1、2、3代表集合x中的元素,4、5、6代表集合Y中的元素,求出最大匹配對數,實現程式碼為:

/*匈牙利演算法實現*/
#include<iostream>

using namespace std;

int e[101][101];//使用鄰接矩陣儲存整個地圖
int match[101]; //用來記錄配對的關係 比如v與u匹配成功就記作match[v]=u和match[u]=v。
int book[101]; //用來標記某個頂點是否已經被嘗試了
int n, m;//頂點個數和邊的數目

int dfs(int u)
{

    for (int i = 1; i <= n; ++i)  //嘗試每一個頂點
    {
        if (book[i] == 0 && e[u][i] == 1)  //還沒有嘗試並且有邊相連
        {
            book[i] = 1; //標記已經嘗試

            //match[i]==0說明當前節點還沒有匹配
            /*dfs(match[i]) 說明當前節點i已經匹配了節點假設為j,則讓節點j與其他節點進行重新匹配看看是不是可以成功
            如果成功則當前節點就可以與節點j匹配*/
            if (match[i] == 0 || dfs(match[i]))
            {
                //更新匹配關係
                match[u] = i;
                match[i] = u;
                return 1;
            }
        }
    }
    return 0;
}
int main()
{
    int sum = 0;
    cin >> n >> m;
    int x, y;
    for (int i = 1; i <= m; ++i)
    {
        cin >> x >> y;
        e[x][y] = 1;
        e[y][x] = 1;        //為無向圖
    }

    //最開始沒有匹配關係 初始化match陣列
    for (int i = 1; i <= n; ++i)
        match[i] = 0;

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= n; ++j)
            book[j] = 0;      //清空上次搜尋的記錄
        if (dfs(i))
            sum++;            //尋找到一條
    }
    cout << sum << endl;
    system("pause");      
}

如果二分圖有n個點,那麼最多找到n/2條增廣路。如果圖中有m條邊,那麼沒找一條增廣路就要把所有邊遍歷一遍,所花時間時m。所以總的時間複雜度是O(NM)。對於判斷一個圖是否是二分圖,可以首先將任意一個頂點進行著紅色,然後將相鄰的點著藍色,如果按照這樣的著色可以將全部的頂點著色,並且相鄰的頂點著色不同,那麼該圖就是二分圖。