LoginSignup
3
1

More than 3 years have passed since last update.

安定結婚問題を解くアルゴリズムをJavaで作ってみた。

Posted at

安定結婚問題って知ってます…?

男性と女性が3人ずついたとします。

  • 男A
  • 男B
  • 男C
  • 女P
  • 女Q
  • 女R

すると3組の夫婦が作れますよね。
でも異性の好みは人それぞれですし、なるべくみんなが満足するような結果にしたい。

そこで個人個人がランキングを持っているとします。

1位 2位 3位
男Aの好み 女P 女Q 女R
男Bの好み 女Q 女R 女P
男Cの好み 女P 女Q 女R
女Pの好み 男C 男B 男A
女Qの好み 男B 男A 男C
女Rの好み 男C 男A 男B

さて、例えば下記のようなマッチングを考えたとしましょう。

  • 男A ⇔ 女Q
  • 男B ⇔ 女R
  • 男C ⇔ 女P

男Cと女Pはお互いに1位の相手と結婚しているので幸せです。
しかし、男Aと男Bはどうでしょう。妻をとりかえっこしたほうがもっと幸せですよね。
そこでマッチングを変更し、下記のようにしたとしましょう。

  • 男A ⇔ 女R
  • 男B ⇔ 女Q
  • 男C ⇔ 女P

これ以上とりかえっこすると、むしろ悪化しそうですね。
このように、これ以上とりかえっこが必要ないマッチング結果のことを「安定マッチング」といいます。

「安定結婚問題」とは、ランキングをもとにして、安定マッチングを見つける問題のことです。
数学の業界では、組合せ最適化問題と呼ばれる問題のひとつとして知られています。

Gale-ShapleyのアルゴリズムをJavaで試作

安定結婚問題はWikipediaにも載っているほど有名な問題です。
安定結婚問題 - Wikipedia

男女3人ずつなら簡単ですが、人数が増えるほど考えられる組合せは爆発的に増え、難しくなります。

Wikipediaを見てみるとGale-Shapleyのアルゴリズムというのが書いてありますね。
安定結婚問題の考案者である Gale と Shapley が提案したアルゴリズムで、
安定マッチングを確実に求められるらしい…。

ということでJavaを使ってさくっと実装してみました。

ただし、このソースコードは下記の理由によりほとんど参考にならないため注意!

  • とりあえず動くものをスピード重視で作っただけで、可読性や処理速度を一切考えていない
  • 入力(ランキングデータ)がハードコーディング
  • 出力(マッチング結果)が標準出力されるだけ

ソースコード

package hoge;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class randomMatching {

    public static HashMap<String, ArrayList<String>> ranking;
    public static HashMap<String, String> matching;

    public static void main(String args[]){

        // 変数初期化
        matching = new HashMap<String, String>();
        matching.put("男A", "");
        matching.put("男B", "");
        matching.put("男C", "");
        ranking = new HashMap<String, ArrayList<String>>();
        ranking.put("男A", new ArrayList<>());
        ranking.put("男B", new ArrayList<>());
        ranking.put("男C", new ArrayList<>());
        ranking.put("女P", new ArrayList<>());
        ranking.put("女Q", new ArrayList<>());
        ranking.put("女R", new ArrayList<>());
        Collections.addAll(ranking.get("男A"), "女P", "女Q", "女R");
        Collections.addAll(ranking.get("男B"), "女Q", "女R", "女P");
        Collections.addAll(ranking.get("男C"), "女P", "女Q", "女R");
        Collections.addAll(ranking.get("女P"), "男C", "男B", "男A");
        Collections.addAll(ranking.get("女Q"), "男B", "男A", "男C");
        Collections.addAll(ranking.get("女R"), "男C", "男A", "男B");

        while(true)
        {
            // 現在の状況を表示
            dump();

            // 独身男性を探す
            String singleManName = "";
            for (String manName : matching.keySet()) {
                if(matching.get(manName) == "")
                {
                    singleManName = manName;
                    break;
                }
            }

            // 独身男性がもういない場合
            if(singleManName == "")
            {
                System.out.println("フリーの人がいなくなったため、プログラムを終了します。");
                return;
            }

            // 独身男性の好みの女性を調べてプロポーズする
            String bestWomanName = ranking.get(singleManName).get(0);
            System.out.println(singleManName + "が、一番好きな" + bestWomanName + "にアタックしました。");

            // この女性が独身か調べる
            String husbandName = "";
            for (String manName : matching.keySet()) {
                if(matching.get(manName) == bestWomanName)
                {
                    husbandName = manName;
                    System.out.println("ところが" + bestWomanName + "は" + husbandName + "と付き合っています。");
                    break;
                }
            }

            // この女性が独身だった場合は婚約
            if(husbandName == "")
            {
                System.out.println(bestWomanName + "は現在フリーのため" + singleManName + "と付き合うことにしました。");
                matching.put(singleManName, bestWomanName);
                continue;
            }
            else
            {
                // 婚約中の hasbandName と、プロポーズしてきた singleManName 、どちらが好みか調べる
                for(String str : ranking.get(bestWomanName))
                {
                    // husbandName のほうが好みの場合
                    if(str == husbandName)
                    {
                        // singleManはこの女性に今後二度とプロポーズしない
                        System.out.println(bestWomanName + "は" + singleManName + "より" + husbandName + "が好きです。" + singleManName + "は" + bestWomanName + "を諦めました。");
                        ranking.get(singleManName).remove(bestWomanName);
                        break;
                    }

                    // husbandName のほうが好みの場合
                    if(str == singleManName)
                    {
                        // 婚約
                        System.out.println(bestWomanName + "は" + husbandName + "より" + singleManName + "が好きなので、乗り換えることにしました。");
                        matching.put(singleManName, bestWomanName);
                        matching.put(husbandName, "");
                        break;
                    }
                }
            }
        }

    }

    // 現在の状態を表示する関数
    public static void dump()
    {
        System.out.println("");

        System.out.println("■現在のランキング");
        for (String key : ranking.keySet()) {
            System.out.println("・" + key + " " + ranking.get(key));
        }

        System.out.println("◆現在のマッチング");
        for (String manName : matching.keySet()) {
            System.out.println("・" + manName + " ⇔ " + matching.get(manName));
        }

        System.out.println("");
    }
}

実行結果

夫婦がまったく成立していない状態から、
進行状況を段階的にSystem.out.println()するように作ってあります。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女P, 女Q, 女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 
・男A ⇔ 
・男B ⇔ 

男Cが、一番好きな女Pにアタックしました。
女Pは現在フリーのため男Cと付き合うことにしました。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女P, 女Q, 女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 女P
・男A ⇔ 
・男B ⇔ 

男Aが、一番好きな女Pにアタックしました。
ところが女Pは男Cと付き合っています。
女Pは男Aより男Cが好きです。男Aは女Pを諦めました。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女Q, 女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 女P
・男A ⇔ 
・男B ⇔ 

男Aが、一番好きな女Qにアタックしました。
女Qは現在フリーのため男Aと付き合うことにしました。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女Q, 女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 女P
・男A ⇔ 女Q
・男B ⇔ 

男Bが、一番好きな女Qにアタックしました。
ところが女Qは男Aと付き合っています。
女Qは男Aより男Bが好きなので、乗り換えることにしました。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女Q, 女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 女P
・男A ⇔ 
・男B ⇔ 女Q

男Aが、一番好きな女Qにアタックしました。
ところが女Qは男Bと付き合っています。
女Qは男Aより男Bが好きです。男Aは女Qを諦めました。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 女P
・男A ⇔ 
・男B ⇔ 女Q

男Aが、一番好きな女Rにアタックしました。
女Rは現在フリーのため男Aと付き合うことにしました。

■現在のランキング
・男C [女P, 女Q, 女R]
・男A [女R]
・女Q [男B, 男A, 男C]
・男B [女Q, 女R, 女P]
・女R [男C, 男A, 男B]
・女P [男C, 男B, 男A]
◆現在のマッチング
・男C ⇔ 女P
・男A ⇔ 女R
・男B ⇔ 女Q

フリーの人がいなくなったため、プログラムを終了します。

以上が実行結果となります。
安定マッチングが無事に求まりました。

このアルゴリズムのすごいところ

(1) 必ず安定マッチングが求まる

実行結果の途中で

女Pは男Aより男Cが好きです。男Aは女Pを諦めました。

と出力されているところがあります。
このように男のアタックを女が断ったとき、
男は「諦める」、つまりその男のランキングから該当の女を除外します。
この除外操作により、男が同じ女に繰り返しアタックすることは絶対に発生しません。
無限ループに陥ることなく、アルゴリズムが必ず終了するのはこのためです。

(2) 結婚だけでなく応用がきく

今回は「どの男とどの女を夫婦にする?」という題材で試してみましたが、
ほかにも例えば学校で「どのゼミにどの学生を入れる?」という題材でもできそうですよね。
ゼミの教授は好きな学生のランキングを作れるでしょうし、
学生も入りたいゼミのランキングを作れるでしょう。

お互いのランキングさえあれば、あらゆる題材にこのアルゴリズムは実行可能なのです。
そして求まるのは「安定マッチング」。
求まったマッチング結果に文句を言う人はいないはず!

まとめ

突貫工事ではありますが、JavaでGale-Shapleyのアルゴリズムを実装することができました。
実装してみたいアルゴリズム、解決してみたい組合せ最適化問題があれば、また何か挑戦してみたいと思います。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1