【數據結構與算法】二分圖算法

首先什麼是二分圖

顧名思義就是能分成兩個部分的圖。

要注意的是,‘分’的是點並且這兩個集合(這裏我們稱作X集合和Y集合)內部所有的點之間沒有邊相連,也就是說X集合中任何兩點之間都不會有邊相連, Y亦然。

無向圖G爲二分圖的一個充要條件是(定理):

  1. G中至少包含兩個頂點
  2. G中所有的迴路長度都必須是偶數

匹配:設G=<V, E>爲二分圖,如果 M⊆E,並且 M 中兩點沒有任何兩點有公共端點(被其他匹配的邊共用),則成M爲G的一個匹配。

最大匹配:邊數最多的匹配。

完備匹配與完全匹配:若 X 中所有的頂點都是匹配 M 中的端點。則稱 M 爲X的完備匹配。 若M既是 X-完備匹配又是 Y-完備匹配,則稱M 爲 G 的完全匹配。

最小點覆蓋:用儘可能少的點去覆蓋所有的邊。

最大點獨立:跟網絡流中的最大點權獨立集有點類似,這裏指的是最大獨立的個數。

接下來是二分圖的一些性質:

設無向圖G有n個頂點,並且沒有孤立頂點,那麼,

  1. 點覆蓋數 + 點獨立數 = n

  2. 最小點覆蓋數 = 二分圖的最大匹配

  3. 最大點獨立數 = n - 最小點覆蓋數 = n - 最大匹配

二分圖的判定:

判斷一個圖是不是二分圖有兩條1、n>= 2 2、不存在奇圈

我們可以用黑白染色的方法進行判斷:

交叉染色法

下面着重介紹下交叉染色法的定義與原理

首先任意取出一個頂點進行染色,和該節點相鄰的點有三種情況:

1.未染色 那麼繼續染色此節點(染色爲另一種顏色)

2.已染色但和當前節點顏色不同 跳過該點

3.已染色並且和當前節點顏色相同 返回失敗(該圖不是二分圖)

下面在拓展兩個概念:

(1) 如果一個雙連通分量內的某些頂點在一個奇圈中(即雙連通分量含有奇圈),那麼這個雙連通分量的其他頂點也在某個奇圈中;

第一個條件的證明:我們假設有一個奇圈,因爲是點雙,沒有割點,必然有緊挨着的圈,假設這個是偶數圈,則,這個偶數圈必然能和原來的奇圈組成新的奇圈(因爲:新的圈=(奇數圈-k)+(偶數圈-k)=奇數+偶數-偶數=奇數,k是共同邊上的點數

(2) 如果一個雙連通分量含有奇圈,則他必定不是一個二分圖。反過來也成立,這是一個充要條件。

二分圖的最大匹配、完美匹配和匈牙利算法

二分圖:簡單來說,如果圖中點可以被分爲兩組,並且使得所有邊都跨越組的邊界,則這就是一個二分圖。準確地說:把一個圖的頂點劃分爲兩個不相交集 UU 和VV ,使得每一條邊都分別連接UU、VV中的頂點。如果存在這樣的劃分,則此圖爲一個二分圖。二分圖的一個等價定義是:不含有「含奇數條邊的環」的圖。

匹配:在圖論中,一個「匹配」(matching)是一個邊的集合,其中任意兩條邊都沒有公共頂點。例如,圖 3、圖 4 中紅色的邊就是圖 2 的匹配。

交替路:從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊…形成的路徑叫交替路。

增廣路:從一個未匹配點出發,走交替路,如果途徑另一個未匹配點(出發的點不算),則這條交替路稱爲增廣路(agumenting path)。例如,圖 5 中的一條增廣路如圖所示(圖中的匹配點均用紅色標出):

在這裏插入圖片描述在這裏插入圖片描述

增廣路有一個重要特點:非匹配邊比匹配邊多一條。因此,研究增廣路的意義是改進匹配。只要把增廣路中的匹配邊和非匹配邊的身份交換即可。由於中間的匹配節點不存在其他相連的匹配邊,所以這樣做不會破壞匹配的性質。交換後,圖中的匹配邊數目比原來多了 1 條。

我們可以通過不停地找增廣路來增加匹配中的匹配邊和匹配點。找不到增廣路時,達到最大匹配(這是增廣路定理)。匈牙利算法正是這麼做的。

// 頂點、邊的編號均從 0 開始
// 鄰接表儲存
 
struct Edge
{
    int from;
    int to;
    int weight;
 
    Edge(int f, int t, int w):from(f), to(t), weight(w) {}
};
 
vector<int> G[__maxNodes]; /* G[i] 存儲頂點 i 出發的邊的編號 */
vector<Edge> edges;
typedef vector<int>::iterator iterator_t;
int num_nodes;
int num_left;
int num_right;
int num_edges;

int matching[__maxNodes]; /* 存儲求解結果 */
int check[__maxNodes];
 
bool dfs(int u)
{
    for (iterator_t i = G[u].begin(); i != G[u].end(); ++i) { // 對 u 的每個鄰接點
        int v = edges[*i].to;
        if (!check[v]) {     // 要求不在交替路中
            check[v] = true; // 放入交替路
            if (matching[v] == -1 || dfs(matching[v])) {
                // 如果是未蓋點,說明交替路爲增廣路,則交換路徑,並返回成功
                matching[v] = u;
                matching[u] = v;
                return true;
            }
        }
    }
    return false; // 不存在增廣路,返回失敗
}
 
int hungarian()
{
    int ans = 0;
    memset(matching, -1, sizeof(matching));
    for (int u=0; u < num_left; ++u) {
        if (matching[u] == -1) {
            memset(check, 0, sizeof(check));
            if (dfs(u))
                ++ans;
        }
    }
    return ans;
}

匈牙利算法的要點如下

從左邊第 1 個頂點開始,挑選未匹配點進行搜索,尋找增廣路。

  1. 如果經過一個未匹配點,說明尋找成功。更新路徑信息,匹配邊數 +1,停止搜索。
  2. 如果一直沒有找到增廣路,則不再從這個點開始搜索。事實上,此時搜索後會形成一棵匈牙利樹。我們可以永久性地把它從圖中刪去,而不影響結果。

由於找到增廣路之後需要沿着路徑更新匹配,所以我們需要一個結構來記錄路徑上的點。DFS 版本通過函數調用隱式地使用一個棧,而 BFS 版本使用 prev 數組。

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