1. 程式人生 > >穩定匹配問題與GS演算法(單身狗脫單祕籍)

穩定匹配問題與GS演算法(單身狗脫單祕籍)

穩定匹配問題

穩定匹配問題(stable matching)是一個常見的問題,GS演算法是解決穩定匹配問題的一個優秀的演算法。下面,我將以男女配對的例子來介紹穩定匹配問題並闡述GS演算法的具體步驟。GS演算法,全稱Gale-Shapley演算法。學習完穩定匹配問題和整個演算法流程之後,我覺得它還可以起另外一個別名,Get-rid-of-Single演算法,單身狗脫單演算法。

問題描述

有n只男性單身狗的集合M = {m1, m2, …, mn}和n只女性單身狗的集合W = {w1, w2, …, wn}。假設每隻男性單身狗對n只女性單身狗的喜好程度都不同,每隻女性單身狗對n只男性單身狗亦如是。男單身狗mi(1 <= i <= n)有一張屬於自己的關於對面n只女單身狗的排序表,mi把他最愛慕的女性放在第一位,第二愛慕的女性放在第二位,以此類推,排名越靠前女性表示mi越愛慕的女性。同樣地,女單身狗wi(1 <= i <= n)也有一張屬於自己的關於對面n只男單身狗的排序表,排名越靠前的男性越受wi的喜愛。每隻單身狗都希望與自己最喜愛的物件結為俠侶,浪跡江湖。現在,有一個名喚月老的NPC在為這2*n只單身狗牽紅線,月老收集了這些單身狗各自的排序表,並根據他們的排序表來牽紅線讓這些單身狗結成n對俠侶,使得這n對俠侶達成一個穩定匹配,和諧地浪跡江湖。

【穩定匹配】假設有兩對伴侶(m1, w1)、(m2, w2),在m1的排序表中w2的排名比w1高,也就是說,m1喜歡w2比喜歡他現任w1要多一點。此時,若w2正好喜歡m1比喜歡她現任m2要多一點,那麼m1和w2就很有可能背叛他們目前各自的俠侶關係重新與更喜歡的物件結為俠侶,剩下被甩的w1和m2繼續淪落為單身狗。上面這種情況我們稱為不穩定因素,要是一個匹配之中沒有任何不穩定因素,那麼這個匹配稱為穩定匹配。再舉個例子,同樣是(m1, w1)、(m2, w2),m1更喜歡w2,但是w2不喜歡m1。此時,這個匹配是穩定的。因為m1和w2並非相互之間都更喜歡對方,因此他們不會”私奔”,不會打破現有的匹配關係。這樣一個匹配,雖然m1無法得到自己最喜歡的w2,但這個匹配的關係是和諧穩定的。因此,我們定義穩定匹配的概念如下:給定的一組匹配結果裡面,n對俠侶之間任何兩對俠侶都不會存在有人想”私奔”的不穩定因素。

輸入輸出

輸入: 每個男單身狗對n個女單身狗的排序表,每個女單身狗對n各男單身狗的排序表

輸出: n對滿足穩定匹配的伴侶

演算法基本思想

初始,每個人都是單身狗,分別根據自己對異性的排序表開始找物件。假設一隻男性單身狗mi選擇了他的排序表上排名最高的女性w,並且向她示愛。這個時候就可能存在下面3種情況:

  • 【1】w也是單身狗,於是他們兩個結為俠侶,成功脫單
  • 【2】w已經和某男性mj脫單,但是w更喜歡mi,於是w把mj甩了,重新和mi結為俠侶。mi成功挖到牆腳,換mj變成單身狗
  • 【3】w已經和某男性mj脫單,而且w不喜歡mi,於是mi挖牆腳失敗,為了脫單隻能繼續尋找排名表上下一個喜歡的女性示愛

對於每個單身狗都重複上述的過程,不斷地去”騷擾”排名表上的女性,找到還沒脫單的就一起脫單,找到脫單的就挖牆腳,挖不動就找下一個喜歡的物件繼續重複上述過程。這就是”偉大”的單身狗脫單(GS)演算法。

演算法的虛擬碼如下所示:

初始化所有M和W集合中元素為單身狗
初始化俠侶集合S為空集
While 存在男單身狗mi
    令w是mi的排名表中mi還未示愛的女性中排名最高的女性
    If w也是單身狗 then
        (mi, w)組成俠侶,加入俠侶集合S
    Else w已經和mj脫單
        If w更偏愛mj而不愛mi then
            mi挖牆腳沒戲,保持單身
        Else w更偏愛mi而不愛mj then
            (mj, w)解除俠侶關係,從俠侶集合S中移除
            mj淪落為單身狗
            mi挖牆腳成功,(mi, w)組成俠侶,加入俠侶集合S
        Endif
    Endif
Endwhile
輸出集合S中的所有俠侶配對情況

演算法分析

仔細分析一下這個單身狗脫單演算法我們可以發現它具備下面幾個特性:

某隻女單身狗w從第一次跟別人組成俠侶之後,如果某個男單身狗m繼續向她示愛,而且m剛好在w的排序表上的排名比w的現任更高,那麼w會甩了現任然後與m”私奔”。如果m在w的排序表上的排名比w的現任低,那麼w不理睬m,繼續和現任保持關係。這個規律可以看出,w自從第一個跟別人組成俠侶之後,她如果後面還有與其他人組成俠侶,那麼跟她組成俠侶的人只會”越來越好”,即越來越符合她的排序表,也就是說,她得到的異性質量會越來越好。

某隻男單身狗向他排序表上的女性示愛,第一個示愛失敗之後只能找第二個,再失敗再找第三個,以此類推。於是這個那單身狗在他脫單之前,他能選擇的女性只會越來越不符合他的排序表,也就是說,他能選擇的異性質量會越來越差。

這個演算法在執行結束之後會返回一個穩定的匹配。為什麼呢?因為該脫單的都脫單了,能挖動的牆腳也都被挖了,最後組成的匹配結果中,任何兩對俠侶之間不會再存在任何能夠挖牆腳私奔的不穩定因素了。

乍一看GS演算法好像是偏愛女性的一種演算法。但實際上,GS演算法在某些情況下也存在偏愛男性的情況。如果男性的排名表完全協調(他們全都列出不同女性作為他們的第一選擇),那麼在GS演算法的所有執行中所有男人最終都與他們的第一選擇匹配,而與女人的排序表無關。怎麼理解呢?假設男單身狗m1最喜歡女單身狗w1,m2最喜歡w2,…,mn最喜歡wn。那麼所有男單身狗在選擇時都會進入前面說到的【1】這種情況,也就是直接和最喜歡的女性脫單了。這個時候女性就變成沒有選擇權了,如果這時候女單身狗的排序表剛好跟男單身狗完全衝突的話,也就是說,w1最不喜歡m1,w2最不喜歡m2,以此類推。那麼這種情境下的匹配結果雖然是穩定的,但卻也往往也是帶著一股不太好的氣息,因為男性都得到了最喜愛的女性,而女性卻都得到了最不喜愛的男性。

演算法實現

經過前面的討論我們基本清楚了穩定匹配問題和GS演算法是怎麼一回事了。下面,我用C++簡單實現了GS演算法的整個過程。為了在O(1)的時間內判斷出女性w是否更加偏愛mi或mj,我將女性對男性的排序表的儲存方式小小調整了一下,跟男性對女性排序表的儲存方式有所不同。另外,俠侶集合S的資料結構也採用陣列表示以便更簡單地在O(1)的時間內增加或刪除俠侶。

GS演算法的時間複雜度為O(n^2),但是不同程式碼實現可能會有不同的複雜度。如果判斷女性w是否更加偏愛mi或mj這個地方使用遍歷女性w的排序表的方法的話會造成更高的複雜度。集合的增刪元素操作這裡也會有相應的複雜度影響。有了上面兩個O(1)複雜度的改進之後,下面整個演算法實現的時間複雜度為O(n^2)。下面是實現程式碼:

#include <iostream>
using namespace std;

class GSModel {
public:
  GSModel(int cpNum): cpNum(cpNum) {
    free_m  = new bool[cpNum];
    free_w  = new bool[cpNum];
    result  = new int[cpNum];
    order_m = new int*[cpNum];
    order_w = new int*[cpNum];
    for (int i = 0; i < cpNum; ++i) {
      order_m[i] = new int[cpNum];
      order_w[i] = new int[cpNum];
    }
  }
  ~GSModel() {
    for (int i = 0; i < cpNum; ++i) {
      delete [] order_m[i];
      delete [] order_w[i];
    }
    delete [] free_m;
    delete [] free_w;
    delete [] result;
    delete [] order_m;
    delete [] order_w;
  }
  // 輸入男性和女性的排序表
  void init() {
    cout << "[man's order list of women]\n";
    for (int i = 0; i < cpNum; ++i) {
      cout << "[m-" << i << "]: ";
      for (int j = 0; j < cpNum; ++j) {
        cin >> order_m[i][j];
      }
      free_m[i] = true;
    }
    cout << "[woman's order list of man]\n";
    for (int i = 0; i < cpNum; ++i) {
      cout << "[w-" << i << "]: ";
      int man;
      for (int j = 0; j < cpNum; ++j) {
        cin >> man;
        order_w[i][man] = cpNum - j;
      }
      free_w[i] = true;
      result[i] = -1;
    }
  }
  // 判斷是否全部人都脫單
  bool isOk() const {
    for (int i = 0; i < cpNum; ++i)
      if (free_m[i]) return false;
    return true;
  }
  void solve() {
    while (true) {
      for (int i = 0; i < cpNum; ++i) {
        if (free_m[i]) {
          for (int j = 0; j < cpNum && free_m[i]; ++j) {
            int w = order_m[i][j];
            // 女方自由
            if (free_w[w]) {
              result[w] = i;
              free_m[i] = false;
              free_w[w] = false;
            }
            // 女方已有物件,但愛此男比愛現任多一點
            else {
              if (order_w[w][i] > order_w[w][result[w]]) {
                free_m[result[w]] = true;
                result[w] = i;
                free_m[i] = false;
              }
            }
          }
        }
      }
      if (isOk()) break;
    }
  }
  // 輸出匹配結果
  void print() const {
    for (int i = 0; i < cpNum; ++i) {
      cout << "(w-" << i << ", m-" << result[i] << ")\n";
    } 
  }
private:
  bool* free_m;
  bool* free_w;
  int** order_m;
  int** order_w;
  int*  result;
  int   cpNum;
};

int main() {
  int cpNum;
  cin >> cpNum;
  GSModel gsm(cpNum);
  gsm.init();
  gsm.solve();
  gsm.print();
  return 0;
}

輸入輸出示例:

3
[man's order list of women]
[m-0]: 0 1 2
[m-1]: 1 0 2
[m-2]: 0 2 1
[woman's order list of man]
[w-0]: 1 2 0
[w-1]: 0 1 2
[w-2]: 1 0 2
(w-0, m-1)
(w-1, m-0)
(w-2, m-2)