二分圖——最大匹配數(最小覆蓋數)、最大獨立數、最小路徑覆蓋、帶權最優匹配

文本內容框架:

§1圖論點、邊集和二分圖的相關概念和性質

§2二分圖最大匹配求解

匈牙利算法、Hopcroft-Karp算法

§3二分圖最小覆蓋集和最大獨立集的構造

§4二分圖最小路徑覆蓋求解

§5二分圖帶權最優匹配求解

Kuhn-Munkers算法

§6小結

每章節都詳細地講解了問題介紹,算法原理和分析,算法流程,算法實現四部分內容,力求徹底解決問題。

 

§1圖論點、邊集和二分圖的相關概念和性質

 

點覆蓋、最小點覆蓋

點覆蓋集即一個點集,使得所有邊至少有一個端點在集合裏。或者說是“點” 覆蓋了所有“邊”。。極小點覆蓋(minimal vertex covering):本身爲點覆蓋,其真子集都不是。最小點覆蓋(minimum vertex covering):點最少的點覆蓋。點覆蓋數(vertex covering number):最小點覆蓋的點數。

 

邊覆蓋、極小邊覆蓋

     邊覆蓋集即一個邊集,使得所有點都與集合裏的邊鄰接。或者說是“邊” 覆蓋了所有“點”。極小邊覆蓋(minimal edge covering):本身是邊覆蓋,其真子集都不是。最小邊覆蓋(minimum edge covering):邊最少的邊覆蓋。邊覆蓋數(edge covering number):最小邊覆蓋的邊數。

 

獨立集、極大獨立集

獨立集即一個點集,集合中任兩個結點不相鄰,則稱V爲獨立集。或者說是導出的子圖是零圖(沒有邊)的點集。極大獨立集(maximal independent set):本身爲獨立集,再加入任何點都不是。最大獨立集(maximum independent set):點最多的獨立集。獨立數(independent number):最大獨立集的點。

 

團即一個點集,集合中任兩個結點相鄰。或者說是導出的子圖是完全圖的點集。極大團(maximal clique):本身爲團,再加入任何點都不是。最大團(maximum clique):點最多的團。團數(clique number):最大團的點數。

 

邊獨立集、極大邊獨立集

邊獨立集即一個邊集,滿足邊集中的任兩邊不鄰接。極大邊獨立集(maximal edge independent set):本身爲邊獨立集,再加入任何邊都不是。最大邊獨立集(maximum edge independent set):邊最多的邊獨立集。邊獨立數(edge independent number):最大邊獨立集的邊數。

 

邊獨立集又稱匹配(matching),相應的有極大匹配(maximal matching),最大匹配(maximum matching),匹配數(matching number)。

 

支配集、極小支配集

支配集即一個點集,使得所有其他點至少有一個相鄰點在集合裏。或者說是一部分的“點”支配了所有“點”。極小支配集(minimal dominating set):本身爲支配集,其真子集都不是。最小支配集(minimum dominating set):點最少的支配集。支配數(dominating number):最小支配集的點數。

 

邊支配集、極小邊支配集

邊支配集即一個邊集,使得所有邊至少有一條鄰接邊在集合裏。或者說是一部分的“邊”支配了所有“邊”。極小邊支配集(minimal edge dominating set):本身是邊支配集,其真子集都不是。最小邊支配集(minimum edge dominating set):邊最少的邊支配集。邊支配數(edge dominating number):最小邊支配集的邊數。

 

最小路徑覆蓋

最小路徑覆蓋(path covering):是“路徑” 覆蓋“點”,即用盡量少的不相交簡單路徑覆蓋有向無環圖G的所有頂點,即每個頂點嚴格屬於一條路徑。路徑的長度可能爲0(單個點)。

最小路徑覆蓋數=G的點數-最小路徑覆蓋中的邊數。應該使得最小路徑覆蓋中的邊數儘量多,但是又不能讓兩條邊在同一個頂點相交。拆點:將每一個頂點i拆成兩個頂點Xi和Yi。然後根據原圖中邊的信息,從X部往Y部引邊。所有邊的方向都是由X部到Y部。因此,所轉化出的二分圖的最大匹配數則是原圖G中最小路徑覆蓋上的邊數。因此由最小路徑覆蓋數=原圖G的頂點數-二分圖的最大匹配數便可以得解。

 

匹配

匹配(matching)是一個邊集,滿足邊集中的邊兩兩不鄰接。匹配又稱邊獨立集(edge independent set)。

在匹配中的點稱爲匹配點(matched vertex)或飽和點;反之,稱爲未匹配點(unmatched vertex)或未飽和點。

交錯軌(alternating path)是圖的一條簡單路徑,滿足任意相鄰的兩條邊,一條在匹配內,一條不在匹配內

增廣軌(augmenting path):是一個始點與終點都爲未匹配點的交錯軌。

最大匹配(maximum matching)是具有最多邊的匹配。

匹配數(matching number)是最大匹配的大小。

完美匹配(perfect matching)是匹配了所有點的匹配。

完備匹配(complete matching)是匹配了二分圖較小集合(二分圖X,Y中小的那個)的所有點的匹配。

增廣軌定理:一個匹配是最大匹配當且僅當沒有增廣軌。

所有匹配算法都是基於增廣軌定理:一個匹配是最大匹配當且僅當沒有增廣軌。這個定理適用於任意圖。

 

二分圖的性質

二分圖中,點覆蓋數是匹配數。
    (1) 二分圖的最大匹配數等於最小覆蓋數,即求最少的點使得每條邊都至少和其中的一個點相關聯,很顯然直接取最大匹配的一段節點即可。
    (2) 二分圖的獨立數等於頂點數減去最大匹配數,很顯然的把最大匹配兩端的點都從頂點集中去掉這個時候剩餘的點是獨立集,這是|V|-2*|M|,同時必然可以從每條匹配邊的兩端取一個點加入獨立集並且保持其獨立集性質。
    (3) DAG的最小路徑覆蓋,將每個點拆點後作最大匹配,結果爲n-m,求具體路徑的時候順着匹配邊走就可以,匹配邊i→j',j→k',k→l'....構成一條有向路徑。

     (4)最大匹配數=左邊匹配點+右邊未匹配點。因爲在最大匹配集中的任意一條邊,如果他的左邊沒標記,右邊被標記了,那麼我們就可找到一條新的增廣路,所以每一條邊都至少被一個點覆蓋。

     (5)最小邊覆蓋=圖中點的個數-最大匹配數=最大獨立集。

 

二分圖的判定

 

二分圖是這樣一個圖: 有兩頂點集且圖中每條邊的的兩個頂點分別位於兩個頂點集中,每個頂點集中沒有邊直接相連接!

 無向圖G爲二分圖的充分必要條件是,G至少有兩個頂點,且其所有迴路的長度均爲偶數。

 判斷二分圖的常見方法是染色法: 開始對任意一未染色的頂點染色,之後判斷其相鄰的頂點中,若未染色則將其染上和相鄰頂點不同的顏色, 若已經染色且顏色和相鄰頂點的顏色相同則說明不是二分圖,若顏色不同則繼續判斷,bfs和dfs可以搞定!

易知:任何無迴路的的圖均是二分圖。

 

§2二分圖最大匹配求解

 

§2.1問題簡介

設G=(V,E)是一個無向圖。如頂點集V可分割爲兩個互不相交的子集V1,V2之並,並且圖中每條邊依附的兩個頂點都分屬於這兩個不同的子集。則稱圖G爲二分圖。二分圖也可記爲G=(V1,V2,E)。

給定一個二分圖G,在G的一個子圖M中,M的邊集{E}中的任意兩條邊都不依附於同一個頂點,則稱M是一個匹配。 選擇這樣的子集中邊數最大的子集稱爲圖的最大匹配問題(maximal matching problem)

如果一個匹配中,圖中的每個頂點都和圖中某條邊相關聯,則稱此匹配爲完全匹配,也稱作完備,完美匹配。

 

§2.2匈牙利算法

 

匈牙利算法思想

       根據一個匹配是最大匹配當且僅當沒有增廣路,求最大匹配就是找增廣軌,直到找不到增廣軌,就找到了最大匹配。遍歷每個點,查找增廣路,若找到增廣路,則修改匹配集和匹配數,否則,終止算法,返回最大匹配數。

 

增廣路徑必須滿足的性質

1.有奇數條邊。

2.起點在二分圖的左半邊,終點在右半邊。

3.路徑上的點一定是一個在左半邊,一個在右半邊,交替出現。(其實二分圖的性質就決定了這一點,因爲二分圖同一邊的點之間沒有邊相連,不要忘記哦。)

4.整條路徑上沒有重複的點。

5.起點和終點都是目前還沒有配對的點,而其它所有點都是已經配好對的。

6.路徑上的所有第奇數條邊都不在原匹配中,所有第偶數條邊都出現在原匹配中。

7.最後,也是最重要的一條,把增廣路徑上的所有第奇數條邊加入到原匹配中去,並把增廣路徑中的所有第偶數條邊從原匹配中刪除(這個操作稱爲增廣路徑的取反),則新的匹配數就比原匹配數增加了1個(奇數=偶數+1)。

 

每次查找得到的增廣路徑的長度都是在上一次查找到的增廣路徑的基礎上延伸的,這樣每次更新匹配數都是增加1。

 

匈牙利算法步驟(令G = (X,*,Y)是一個二分圖,其中,X = {x1,x2,...xm}, Y = {y1,y2,...yn}。令M爲G中的任一個匹配)

 

 (1)置M爲空

 (2)從G中找出一個未匹配點v(增廣路性質5要求的),如果沒有則算法結束,否則,以v爲起點,查找增廣路(鄰接點是爲未匹配點,則返回尋找完成,若v的鄰接點u是匹配點,則從u開始查找,直至查找到有未匹配點終止)即滿足增廣路的性質,如果沒有找到增廣路,則算法終止

 

   (3)找出一條增廣路徑P,通過異或操作獲得更大的匹配M’代替M(方便要輸出增廣矩陣以及進一步查找),匹配數加1(性質7得到)

 (4)重複(2)(3)操作直到找不出增廣路徑爲止

 

徹底理解增廣路查找方法

1.總是從X集的未匹配點出發,尋找匹配點或者未匹配點,如查找到未匹配點則該增廣路終止,否則以該點的增廣路不存在。

2.每次查找增廣路都是在之前形成的匹配(上面步驟3中異或後的匹配)的基礎上進行延伸的,也就是查找匹配點總是在匹配M中,其實就是用起點和終點兩個未匹配點將得到匹配的邊儘可能的連接起來的增廣路,這樣增廣路長度就延長了,當然也可以是直接就是以兩個個未匹配點的邊(就直接添加進匹配中)。總而言之,每次擴充匹配不是通過延伸增廣路徑就是新增增廣路徑(當然長度爲1)。

時間空間複雜度

時間複雜度 鄰接矩陣:最壞爲O(n^3) 鄰接表:O(mn)   空間複雜度 鄰接矩陣:O(n^2) 鄰接表:O(m+n)

╝②

 

匈牙利算法實現

匈牙利算法只需要以每個節點爲起點找一次增廣路即可求得最大匹配,尋找增廣路的複雜度爲O(E),總的複雜度爲O(VE)。

下面的實現是查找從X到Y的匹配,X中每一個點最多隻能被遍歷一次用於查找增廣路(當已經是匹配點是就不遍歷了),對DFS進行了詳細的註釋,要是還是不懂,可以免費提供解惑。


DFS

#define maxn 10//表示x集合和y集合中頂點的最大個數!
  int nx,ny;//x集合和y集合中頂點的個數
  int edge[maxn][maxn];//edge[i][j]爲1表示ij可以匹配
  int cx[maxn],cy[maxn];//用來記錄x集合中匹配的y元素是哪個!
  int visited[maxn];//用來記錄該頂點是否被訪問過!
  int path(int u)
  {
      int v;
      for(v=0;v<ny;v++)
      {
          if(edge[u][v]&&!visited[v])
          {
              visited[v]=1;
             if(cy[v]==-1||path(cy[v]))//如果y集合中的v元素沒有匹配或者是v已經匹配,但是從cy[v]中能夠找到一條增廣路
              {
                  cx[u]=v; //找到增廣路,修改匹配M
                  cy[v]=u;
                  return 1;
              }
          }
      }
      return 0;
  }
  int maxmatch()
  {
      int res=0;
      memset(cx,0xff,sizeof(cx));//初始值爲-1表示兩個集合中都沒有匹配的元素!
      memset(cy,0xff,sizeof(cy));
      for(int i=0;i<=nx;i++)
      {
          if(cx[i]==-1)   //還沒被匹配,執行內部代碼
          {
              memset(visited,0,sizeof(visitited));  //重置標記爲爲訪問
              res+=path(i);   //以 i 爲起點開始查找增廣路,返回true ,匹配數+1
          }
      }
      return res;
  }

 BFS

#define maxn 10;
 int pred[maxn];
 int cx,cy;
 int nx,ny;
 int visited[maxn];
 int edge[maxn][maxn];
 int queue[maxn];// 用來模擬隊列
 int maxmatch()
 {
     int i,j,y;
     int cur,tail;
     int res=0;
     memset(cx,0xff,sizeof(cx));
     memset(cy,0xff,sizeof(cx));
     for(i=0;i<nx;i++)
     {
         if(cx[i]!=-1)
         continue;
         //對於x集合中的每個沒有匹配的點i進行一次bfs找交錯軌
         for(j=0;j<ny;j++)
         {
             if(edge[i][j])
             {
                 pred[j]=-1;//-1表示遍歷到了,
                 queue[tail++]=j;
             }
         }
         while(cur<tail)
         {
             y=queue[cur];
             if(cy[y]==-1)
             break;
             for(j=0;j<ny;j++)
             {
                 if(pred[j]==-2&&edge[cy[y]][j])
                 {
                     pred[j]=y;
                     queue[tail++]=j;
                 }
             }
         }
         if(cur==tail) continue;
         while(pred[y]>-1)
         {
             cx[cy[pred[y]]]=y;
             cy[y]=cy[pred[y]];
             y=pred[y];
         }
         cy[y]=i;
         cx[i]=y;
         res++;
     }
     return res;
 }

§2.3Hopcroft-Karp算法

在匈牙利算法中,我們每次尋找一條增廣路來增加匹配集合M.可以證明,每次找增廣路的複雜度是O(E),一共需要增廣O(V)次,因此總時間複雜度爲O(VE)。爲了降低時間複雜度,在Hopcroft-Karp算法中,我們在增加匹配集合M時,每次DFS尋找多條增廣路(不相交).可以證明,這樣迭代次數最多爲2*V^0.5,所以,時間複雜度就降到了O(V^0.5*E)。

 

Hopcroft-Karp算法原理

Hopcroft-Karp算法先使用BFS查找多條增廣路,然後使用DFS遍歷增廣路(累加匹配數,修改匹配點集),循環執行,直到沒有增廣路爲止。

        Hopcroft-Karp算法的BFS遍歷只對點進行分層(不標記是匹配點和未匹配點),然後用DFS遍歷看上面的層次哪些是增廣路徑(最後一個點是未匹配的)。

BFS過程可以看做是圖像樹結構一樣逐層向下遍歷,還要防止出現相交的增廣路徑。

 

Hopcroft-Karp算法步驟

設U和V是圖G的二分圖,M是從U到V的匹配

 

   (1)使用BFS遍歷對圖的點進行分層,從X中找出一個未匹配點v,(所有v)組成第一層,接下的層是這樣形成的——都是查找匹配點(增廣路性質),直到在V中找到未匹配點才終止查找,對X其他未匹配點同樣進行查找增廣路徑(BFS只分層不標記是否匹配點)

   (2)使用DFS遍歷查找(1)形成的增廣路,找到就匹配數就累加1

 (3)重複(1)(2)操作直到找不出增廣路徑爲止

 

Hopcroft-Karp算法實現

下面的實現有詳細的註釋,該算法還是不完美,每次調用searchP()值保留了一個最小的dis值(爲什麼是最小,因爲其是BFS遍歷,當同一層次有一個v滿足My[v]==-1時,dis就附上相應的層次值),也就是在長度大於dis的層在本次調用時再遍歷下去,只能是下次調用searchP()查找,花了好幾個小時去理解。

通過上面的分析,易知searchP()是沒有遍歷層次大於dis的層,也就是說沒有把長度大於dis增廣路徑是沒有找到的。當然這樣做的好處——防止出現相交的增廣路徑。

還有個要知道的是dis在下面這個算法中的值只可能是從1逐漸增加偶數變大的,所以這樣做是不可能在一次searchP()調用之後DFS出現相交的增廣路徑的(一定只會是長度小的那個增廣路徑)。

Hopcroft-Carp 算法:

//poj_1469
/*==================================================*\
| 二分圖匹配(Hopcroft-Carp 的算法)
| INIT: g[][]鄰接矩陣;
| CALL: res = MaxMatch(); Nx, Ny要初始化!!!
| Mx,My爲match
| 時間複雜度爲O(V^0.5 E)
\*==================================================*/
/***********************Hopcroft-Carp 算法****************************************/
#include <cstdio>
#include <memory.h>
#include <queue>
using namespace std;

const int MAXN = 310;
const int INF = 1 << 28;
bool flag;
int p,n;
int  Mx[MAXN], My[MAXN], Nx, Ny;
int dx[MAXN], dy[MAXN], dis;
bool vst[MAXN],g[110][310];
bool searchP(void)    //BFS 
{
    queue <int> Q;
    dis = INF;
    memset(dx, -1, sizeof(dx));
    memset(dy, -1, sizeof(dy));
    for (int i = 1; i <= Nx; i++)
    if (Mx[i] == -1){
       Q.push(i); dx[i] = 0;
    }
    while (!Q.empty()) {
        int u = Q.front(); Q.pop();
        if (dx[u] > dis) break;        //說明該增廣路徑長度大於dis還沒有結束,等待下一次BFS在擴充
           for (int v = 1; v <= Ny; v++)
               if (g[u][v] && dy[v] == -1) {        //v是未匹配點
                  dy[v] = dx[u]+1;
                if (My[v] == -1) dis = dy[v];    //得到本次BFS的最大遍歷層次
                else{
                     dx[My[v]] = dy[v]+1;         //v是匹配點,繼續延伸
                     Q.push(My[v]);
                     }
                }
    }
    return dis != INF;
}

bool DFS(int u){
    for (int v = 1; v <= Ny; v++)
    if (!vst[v] && g[u][v] && dy[v] == dx[u]+1) {
       vst[v] = 1;
       if (My[v] != -1 && dy[v] == dis) continue;   //層次(也就是增廣路徑的長度)大於本次查找的dis,是searchP被break的情況,也就是還不確定是否是增廣路徑,只有等再次調用searchP()在判斷。
       if (My[v] == -1 || DFS(My[v])) {     //是增廣路徑,更新匹配集
       My[v] = u; Mx[u] = v;
       return 1;
       }
    }
 return 0;
}

int MaxMatch(void){
    int res = 0;
    memset(Mx, -1, sizeof(Mx));
    memset(My, -1, sizeof(My));
    while (searchP()) {
          memset(vst, 0, sizeof(vst));
          for (int i = 1; i <= Nx; i++)
              if (Mx[i] == -1 && DFS(i)) res++;   //查找到一個增廣路徑,匹配數res++
    }
    return res;
}

/**********************************************************************/
int main()
{
    int i,j,k,t,v,cnt;
    scanf("%d",&t);
    while (t--)
    {
          scanf("%d %d", &p, &n);
          for (i = 1; i <= p; i++)
              for (j = 1; j <= n; j++)
                  g[i][j] = false;
          flag = true;
          for (i = 1; i <= p; i++)
          {
              scanf("%d",&k);
              if (k == 0)
                 flag = false;
              while (k--)
              {
                    scanf("%d",&v);
                    g[i][v]  = true;
              }
          }
          Nx = p; Ny = n;
          if (flag)
          {
               cnt = MaxMatch();
               if (cnt == p)
                  printf("YES\n");
               else printf("NO\n");   
          } 
          else printf("NO\n"); 
    }
    
    return 0;
}

該算法實現的關鍵點:每次使用調用BFS查找到多條增廣路的路徑長度都是相等的,而且都以第一次得到的dis爲該次查找增廣路徑的最大長度。

 

下面給出另一個實現

代碼

//1550ms
#include <stdio.h>
#include <string.h>
#define CAP 50010

int n, m;
int mx[CAP], my[CAP], dis[CAP], que[CAP];
bool used[CAP];
struct Node {
    int id;
    struct Node *next;
}adj[CAP];

bool BFS()
{
    int front, rear;
    int i, j;
    front = rear = 0;
    for (i=1; i<=n; i++) {
        if (mx[i] < 0) {
            dis[i] = 0;
            que[rear++] = i;
            used[i] = true;
        }else {
            used[i] = false;
        }
    }
    bool suc = false;
    while (front < rear) {
        int u = que[front++];
        struct Node *p = &(adj[u]);
        while (p->next) {
            int v = p->next->id;
            if (my[v] < 0) suc = true;
            else if (!used[my[v]]) {
                dis[my[v]] = dis[u]+1;
                used[my[v]] = true;
                que[rear++] = my[v];
            }
            p = p->next;
        }
    }
    return suc;
}

bool DFS(int u)
{
    struct Node *p = &(adj[u]);
    while (p->next) {
        int v = p->next->id;
        if (my[v] < 0
            || dis[my[v]] == dis[u]+1 && DFS(my[v])) {
            my[v] = u;
            mx[u] = v;
            dis[u] = -1;
            return true;
        }
        p = p->next;
    }
    return false;
}

int main()
{
    int i, j, P;
    int a, b;
    struct Node *p;
    while (scanf("%d%d%d", &n, &m, &P) != EOF) {
        for (i=1; i<=n; i++) 
            adj[i].next = NULL;
        for (i=0; i<P; i++) {
            scanf("%d%d", &a, &b);
            p = new Node;
            p->id = b;
            p->next = adj[a].next;
            adj[a].next = p;
        }
        memset(mx, -1, sizeof(mx));
        memset(my, -1, sizeof(my));
        int match = 0;
        while (BFS()) {
            for (i=1; i<=n; i++) {
                if (mx[i] < 0 && DFS(i))
                    match++;
            }
        }
        printf("%d\n", match);
    }
    return 0;
}

匈牙利算法和Hopcroft-Karp算法細節的對比

匈牙利算法每次都以一個點查找增廣路徑,Hopcroft-Karp算法是每次都查找多條增廣路徑;匈牙利算法每次查找的增廣路徑的長度是隨機的,Hopcroft-Karp算法每趟查找的增廣路徑的長度只會在原來查找到增廣路徑的長度增加偶數倍(除了第一趟,第一趟得到增廣路徑長度都是1)。

 

§3二分圖最小覆蓋集和最大獨立集的構造

二分圖有下面兩個性質:

 

定理1:最小覆蓋數 = 最大匹配數

定理2:最大獨立集S 與 最小覆蓋集T 互補

 

算法實現步驟:

1. 做最大匹配,沒有匹配的空閒點∈S

2. 如果u∈S那麼u的鄰點必然屬於T

3. 如果一對匹配的點中有一個屬於T那麼另外一個屬於S

4. 還不能確定的,把左子圖的放入S,右子圖放入T

算法結束

╝④

 

§4二分圖最小路徑覆蓋求解

 

最小覆蓋的相關性質可查看前面的第一部分。

 

有向無環圖最小不相交路徑覆蓋

       定義:用最少的不相交路徑覆蓋所有頂點。

       定理:把原圖中的每個點V拆成Vx和Vy,如果有一條有向邊A->B,那麼就加邊Ax-By。這樣就得到了一個二分圖,最小路徑覆蓋=原圖的節點數-新圖最大匹配。

       簡單證明:一開始每個點都獨立的爲一條路徑,總共有n條不相交路徑。我們每次在二分圖裏加一條邊就相當於把兩條路徑合成了一條路徑,因爲路徑之間不能有公共點,所以加的邊之間也不能有公共點,這就是匹配的定義。所以有:最小路徑覆蓋=原圖的節點數-新圖最大匹配。

 

有向無環圖最小可相交路徑覆蓋

       定義:用最小的可相交路徑覆蓋所有頂點。

       算法:先用floyd求出原圖的傳遞閉包,即如果a到b有路,那麼就加邊a->b。然後就轉化成了最小不相交路徑覆蓋問題。

╝⑦

 

問題描述

   給定有向圖 G=(V,E)。設 P 是 G 的一個簡單路(頂點不相交)的集合。如果 V 中每個頂點恰好在 P 的一條路上,則稱 P是 G 的一個路徑覆蓋。P 中路徑可以從 V 的任何一個頂點開始,長度也是任意的,特別地,可以爲0。G 的最小路徑覆蓋是 G 的所含路徑條數最少的路徑覆蓋。

 

 

算法思想

把每個頂點理解成兩個頂點,一個是出發,一個是目標,建立二分圖。該二分圖的任何一個匹配方案,都對應了一個路徑覆蓋方案。如果匹配數爲0,那麼顯然路徑數=頂點數。每增加一條匹配邊,那麼路徑覆蓋數就減少一個,所以路徑數=頂點數 - 匹配數。要想使路徑數最少,則應最大化匹配數,所以要求二分圖的最大匹配。


 

有向無環圖最小覆蓋算法流程

1.構建二分圖,原圖中的每個點V拆成Vx和Vy,如果有一條有向邊A->B,那麼就加邊Ax-By。這樣就得到了一個無向的二分圖。

2.求最大匹配數

 

算法實現

#include<iostream>
#include<string.h>
#include<stdio.h>
#include<algorithm>
#include<vector>
#define maxn 10000
using namespace std;
vector<int>node[maxn];
int mm[maxn];
int visit[maxn];
int n,m;
void init()
{
    cin>>n>>m;//輸入點數,邊數
    for(int i=0;i<=n;i++)
    node[i].clear();
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        node[a].push_back(b);//建邊
    }
}
int dfs(int fa)
{
    for(int i=0;i<node[fa].size();i++)
    {
        int v=node[fa][i];
        if(!visit[v])
        {
            visit[v]=1;
            if(mm[v]==-1||dfs(mm[v]))
            {
                mm[v]=fa;
                return 1;
            }
        }
    }
    return 0;
}
void solve()
{
    int cnt=0;
    memset(mm,-1,sizeof(mm));
    for(int i=1;i<=n;i++)
    {
        memset(visit,0,sizeof(visit));
        if(dfs(i))cnt++;
    }
    cout<<n-cnt<<endl;//最小路徑覆蓋=點數-最大匹配數
}
int main()
{
    int test;
    cin>>test;
    while(test--)
    {
        init();
        solve();
    }
    return 0;
}

§5二分圖帶權最優匹配求解

 

問題描述

二分圖帶權最優匹配:對於二分圖的每條邊都有一個權(非負),要求一種完備匹配方案,使得所有匹配邊的權和最大,記做最優完備匹配。(特殊的,當所有邊的權爲1時,就是最大完備匹配問題)

 

看到這裏,估計已經有點暈乎了,那麼下面幫你理順下思路:

二分圖帶權最大匹配和二分圖最有匹配的區別

二分最大匹配:使得匹配中的邊數量不小於任何其他的匹配。 

二分完備匹配:使得圖G中所有點出現在該匹配中。 

 

二分圖完備匹配的判定

 

Hall定理:設二分圖中G=<V1,V2,E>中|V1|=m<=|V2|=n,G中存在從V1到V2的完備匹配當且僅當V1中任意k(k=1,2,...,m)個頂點至少與V2中k個頂點相鄰(相異性條件)。

  證明:

  (必要性)顯然成立。

  (充分性)反證法。設G中不存在完備匹配,取G的一個最大匹配M,則V1中至少有一個點不在M上,且該點必至少與一條不在M中的邊相連,該邊的另一個頂點若也爲M-非飽和點,則與M爲最大匹配矛盾,若另一個頂點爲M-飽和點,則考察在M中與該頂點相鄰的點,利用飽和點去考察在M中相鄰的飽和點(交錯地考察,即交錯地通過M中的邊和非M中的邊),直至考察完畢,由相異性條件知,最後必考察至非飽和點,此時出現一條增廣路,又與假設矛盾,故充分性成立。

  Hall定理的一個推論:設二部圖中G=<V1,V2,E>中|V1|=m<=|V2|=n,V1中每個頂點至少關聯正整數t條邊,V2中每個頂點至多關聯t條邊(t條件),則G存在從V1到V2的完備匹配。

 

╝⑤

 

二分圖完備匹配判斷實現可參考http://www.cnblogs.com/moonbay/archive/2012/08/16/2642042.html

 

二分圖的帶權匹配就是求出一個匹配集合,使得集合中邊的權值之和最大或最小。 

而二分圖的最優匹配則一定爲完備匹配,在此基礎上,纔要求匹配的邊權值之和最大或最小。二分圖的帶權匹配與最優匹配不等價,也不互相包含。 

使用Kuhn-Munkers算法實現求二分圖的最優匹配。Kuhn-Munkers算法可以實現爲O(N^3)的時間複雜度。 

 

Kuhn-Munkers算法

Kuhn-Munkers算法是通過給每個頂點一個標號(叫做頂標)來把求最大權匹配的問題轉化爲求完備匹配的問題的。設頂點Xi的頂標爲A[i],頂點Yi的頂標爲B[i],頂點Xi與Yj之間的邊權爲w[i,j]。在算法執行過程中的任一時刻,對於任一條邊(i,j), A[i]+B[j]>=w[i,j]始終成立。

 

Kuhn-Munkers算法的正確性基於以下定理:

  若由二分圖中所有滿足A[i]+B[j]=w[i,j]的邊(i,j)構成的子圖(稱做相等子圖)有完備匹配,那麼這個完備匹配就是二分圖的最大權匹配。

  這個定理是顯然的。因爲對於二分圖的任意一個匹配,如果它包含於相等子圖,那麼它的邊權和等於所有頂點的頂標和;如果它有的邊不包含於相等子圖,那麼它的邊權和小於所有頂點的頂標和。所以相等子圖的完備匹配一定是二分圖的最大權匹配。

 

Kuhn-Munkers算法的基本思路

  初始時爲了使A[i]+B[j]>=w[i,j]恆成立,令A[i]爲所有與頂點Xi關聯的邊的最大權,B[j]=0。如果當前的相等子圖沒有完備匹配,就按下面的方法修改頂標以使擴大相等子圖,直到相等子圖具有完備匹配爲止。

  我們求當前相等子圖的完備匹配失敗了,是因爲對於某個X頂點,我們找不到一條從它出發的交錯路。這時我們獲得了一棵交錯樹,它的葉子結點全部是X頂點。現在我們把交錯樹中X頂點的頂標全都減小某個值d,Y頂點的頂標全都增加同一個值d,那麼我們會發現:

兩端都在交錯樹中的邊(i,j),A[i]+B[j]的值沒有變化。也就是說,它原來屬於相等子圖,現在仍屬於相等子圖。

兩端都不在交錯樹中的邊(i,j),A[i]和B[j]都沒有變化。也就是說,它原來屬於(或不屬於)相等子圖,現在仍屬於(或不屬於)相等子圖。

X端不在交錯樹中,Y端在交錯樹中的邊(i,j),它的A[i]+B[j]的值有所增大。它原來不屬於相等子圖,現在仍不屬於相等子圖。

X端在交錯樹中,Y端不在交錯樹中的邊(i,j),它的A[i]+B[j]的值有所減小。也就說,它原來不屬於相等子圖,現在可能進入了相等子圖,因而使相等子圖得到了擴大。

  現在的問題就是求d值了。爲了使A[i]+B[j]>=w[i,j]始終成立,且至少有一條邊進入相等子圖,d應該等於min{A[i]+B[j]-w[i,j]|Xi在交錯樹中,Yi不在交錯樹中}。

 

  以上就是Kuhn-Munkers算法的基本思路。但是樸素的實現方法,時間複雜度爲O(n4)——需要找O(n)次增廣路,每次增廣最多需要修改O(n)次頂標,每次修改頂標時由於要枚舉邊來求d值,複雜度爲O(n²)。實際上Kuhn-Munkers算法的複雜度是可以做到O(n3)的。我們給每個Y頂點一個“鬆弛量”函數slack,每次開始找增廣路時初始化爲無窮大。在尋找增廣路的過程中,檢查邊(i,j)時,如果它不在相等子圖中,則讓slack[j]變成原值與A[i]+B[j]-w[i,j]的較小值。這樣,在修改頂標時,取所有不在交錯樹中的Y頂點的slack值中的最小值作爲d值即可。但還要注意一點:修改頂標後,要把所有的slack值都減去d。

 

Kuhn-Munkers算法流程

 

(1)初始化可行頂標的值

(2)用匈牙利算法尋找完備匹配

(3)若未找到完備匹配則修改可行頂標的值

(4)重複(2)(3)直到找到相等子圖的完備匹配爲止

 

 

Kuhn-Munkers算法實現

#include <cstdio>
#include <queue>
#include <algorithm>
using namespace std;
 
const int N = 128;
const int INF = 1 << 28;
 
class Graph {
private:
   bool xckd[N], yckd[N];
   int n, edge[N][N], xmate[N], ymate[N];
   int lx[N], ly[N], slack[N], prev[N];
   queue<int> Q;
   bool bfs();
   void agument(int);
public:
   bool make();
   int KMMatch();
};
bool Graph::make() {
   int house[N], child[N], h, w, cn = 0;
   char line[N];
   scanf("%d %d", &h, &w);
   if(w == 0) return false;
   scanf("\n"); n = 0;
   for(int i = 0; i < h; i++) {
      gets(line);
      for(int j = 0; line[j] != 0; j++) {
         if(line[j] == 'H') house[n++] = i * N + j;
         if(line[j] == 'm') child[cn++] = i * N + j;
      }
   }
   for(int i = 0; i < n; i++) {
      int cr = child[i] / N, cc = child[i] % N;
      for(int j = 0; j < n; j++) {
         int hr = house[j] / N, hc = house[j] % N;
         edge[i][j] = -abs(cr-hr) - abs(cc-hc);
      }
   }
   return true;
}
bool Graph::bfs() {
   while(!Q.empty()) {
      int p = Q.front(), u = p>>1; Q.pop();
      if(p&1) {
         if(ymate[u] == -1) { agument(u); return true; }
         else { xckd[ymate[u]] = true; Q.push(ymate[u]<<1); }
      } else {
         for(int i = 0; i < n; i++)
            if(yckd[i]) continue;
            else if(lx[u]+ly[i] != edge[u][i]) {
               int ex = lx[u]+ly[i]-edge[u][i];
               if(slack[i] > ex) { slack[i] = ex; prev[i] = u; }
            } else {
               yckd[i] = true; prev[i] = u;
               Q.push((i<<1)|1);
            }
      }
   }
   return false;
}
void Graph::agument(int u) {
   while(u != -1) {
      int pv = xmate[prev[u]];
      ymate[u] = prev[u]; xmate[prev[u]] = u;
      u = pv;
   }
}
int Graph::KMMatch() {
   memset(ly, 0, sizeof(ly));
   for(int i = 0; i < n; i++) {
      lx[i] = -INF;
      for(int j = 0; j < n; j++) lx[i] >?= edge[i][j];
   }
   memset(xmate, -1, sizeof(xmate)); memset(ymate, -1, sizeof(ymate));
   bool agu = true;
   for(int mn = 0; mn < n; mn++) {
      if(agu) {
         memset(xckd, false, sizeof(xckd));
         memset(yckd, false, sizeof(yckd));
         for(int i = 0; i < n; i++) slack[i] = INF;
         while(!Q.empty()) Q.pop();
         xckd[mn] = true; Q.push(mn<<1);
      }
      if(bfs()) { agu = true; continue; }
      int ex = INF; mn--; agu = false;
      for(int i = 0; i < n; i++)
         if(!yckd[i]) ex <?= slack[i];
      for(int i = 0; i < n; i++) {
         if(xckd[i]) lx[i] -= ex;
         if(yckd[i]) ly[i] += ex;
         slack[i] -= ex;
      }
      for(int i = 0; i < n; i++)
         if(!yckd[i] && slack[i] == 0) { yckd[i] = true; Q.push((i<<1)|1); }
 
   }
   int cost = 0;
   for(int i = 0; i < n; i++) cost += edge[i][xmate[i]];
   return cost;
}
 
int main()
{
   Graph g;
 
   while(g.make()) printf("%d\n", -g.KMMatch());
 
   return 0;
}

 

Kuhn-Munkers算法的幾種變形應用

1.Kuhn-Munkers算法是求最大權完備匹配,如果要求最小權完備匹配怎麼辦?方法很簡單,只需將所有的邊權值取其相反數,求最大權完備匹配,匹配的值再取相反數即可。 

2.Kuhn-Munkers算法的運行要求是必須存在一個完備匹配,如果求一個最大權匹配(不一定完備)該如何辦?依然很簡單,把不存在的邊權值賦爲0。 

3.Kuhn-Munkers算法求得的最大權匹配是邊權值和最大,如果我想要邊權之積最大,又怎樣轉化?還是不難辦到,每條邊權取自然對數,然後求最大和權匹配,求得的結果a再算出e^a就是最大積匹配。

╝⑥

 

 

§6小結

本文詳細地講解了二分圖——最大匹配數(最小覆蓋數)、最大獨立數、最小路徑覆蓋、帶權最優匹配五大主題,每部分解釋都做到有過之而不及的詳盡,緊扣重點,簡明易懂。如果你有任何建議或者批評和補充,請留言指出,不勝感激,更多參考請移步互聯網。

 

 

參考:

波風七夜http://www.cnblogs.com/bofengqiye/archive/2012/05/02/2479809.html

acronixhttp://www.cppblog.com/acronix/archive/2010/08/21/124203.aspx

superbin:   http://www.cnblogs.com/superbin/archive/2010/06/09/1754996.html

〓D〓F〓 //ubuntu

http://hi.baidu.com/ewamihcjxublmrd/item/5729ae2346a7783395f62b2d

zxfx100http://www.cnblogs.com/zxfx100/

⑥NOCOW: http://www.nocow.cn/index.php/Kuhn-Munkres%E7%AE%97%E6%B3%95

Memento: http://endlesscount.blog.163.com/blog/static/821197872012622103810976/

一把刷子http://www.cnblogs.com/cs1003/archive/2012/08/28/2660845.html

http://dsqiu.iteye.com/blog/1689505


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章