5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

モンティ・ホール問題をJavaで実験してみる

Last updated at Posted at 2020-11-23

最近統計学に関する本を読んでいます。
その中にモンティ・ホール問題という問題を紹介しています。

ちょっと直感に乖離しているの問題なので、プログラミングで実験してみよと思いました。
そのまま書くのもつまらないので、
FunctionalInterfaceStrategy Patternを使って遊びました。

1 問題について軽く

そのままウィキからコピーします。

  • (1) 3つのドア (A, B, C) に(景品、ヤギ、ヤギ)がランダムに入っている。

  • (2) プレーヤーはドアを1つ選ぶ。

  • (3) モンティは残りのドアのうち1つを必ず開ける。

  • (4) モンティの開けるドアは、必ずヤギの入っているドアである。

  • (5) モンティはプレーヤーにドアを選びなおしてよいと必ず言う。

2 コードを書く

2.1 Main

まず最終的に実行するMainクラスの部分を書きます。

public class Main {

    public static void main(String[] args) {
        // 最後に書く
    }

    private static boolean play(ChooseStrategy strategy) {
        Player player = new Player(strategy);
        Host host = new Host();
        Stage stage = new Stage();

        // Player make his first choice
        stage.doOperation(player::chooseDoor);
        // System.out.println(stage);

        // Host open a door
        stage.doOperation(host::openDoor);
        // System.out.println(stage);

        // Player make his second choice
        stage.doOperation(player::chooseAgain);
        // System.out.println(stage);

        return stage.isCorrect();
    }
}

まずplayメソッドを見ていただくと、いくつかのことを行いました。

  • プレーヤーを生成します。(ストラテジーを指定します、後で紹介)
  • モンティを生成します。(ホスト)
  • 三つドアを持っているステージを生成します。

次にゲームのルールに従ってそのまま実行手順を書いてみました。

  • プレーヤーはステージに対してドアを選択しました。
  • ホストはステージに対して一つのドアを開けました。
  • プレーヤーは二回目(選びなおし)の選択を行いました。
  • ステージを確認してプレーヤーの選択は当たってるかを返しました。

ストラクチャーは大体できたので、各部分を実装します。

2.2 ChooseStrategy

一回目のチョイスはランダムなのでdefaultでロジックを書きます。
二回目はストラテジーにより異なるので継承先に実装を任せます。

/**
* 一回目と二回目の選択ストラテジー
*/
@FunctionalInterface
public interface ChooseStrategy {

    private Random random = new Random();

    default void chooseFirstTime(List<Door> doors) {
        doors.get(random.nextInt(doors.size())).setChosen(true);
    }

    void chooseSecondTime(List<Door> doors);
}

2.2.0 Door

上記のストラテジーにはDoorクラスが出ていたのでその中身を紹介します。

各ドアの属性は以下の通りです。
ストラテジーはドアリストに対して操作します。

@AllArgsConstructor
@Data
public class Door {

    int index;
    // is or not has the price
    boolean isAnswer;
    // is or not opened by host
    boolean isOpened;
    // is or not chosen by player
    boolean isChosen;

}

2.2.1 NotChangeChoiceStrategy

ここはちょっと面白いところですが、
もしプレーヤーは一回目の選択を変えないと決めた場合に何もしなくていいので、
ここも何も実装しなくて大丈夫です。

public class NotChangeChoiceStrategy implements ChooseStrategy {

    @Override
    public void chooseSecondTime(List<Door> doors) {
        // do nothing
    }
}

2.2.2 ChangeChoiceStrategy

もし選択を変えると決めた場合に、
残りの二つのドアの状態を変えます。

まず三つのドアの中に一つのドアが開けられたので除外して、
次に残りの二つのドアの選択状態を切り替えます。

public class ChangeChoiceStrategy implements ChooseStrategy {

    @Override
    public void chooseSecondTime(List<Door> doors) {
        doors.stream().filter(d -> !d.isOpened()).forEach(d -> d.setChosen(!d.isChosen()));
    }
}

ここまででプレーヤーはどういう方法でドアを選択するかは定義できました。
次にプレーヤー自体を定義します。

2.3 Player

めちゃくちゃ簡単ですが、
一回目と二回目の動きは全部ストラテジーにより行います。

public class Player {

    private ChooseStrategy strategy;

    public Player(ChooseStrategy strategy) {
        this.strategy = strategy;
    }

    public void chooseDoor(List<Door> doors) {
        strategy.chooseFirstTime(doors);
    }

    public void chooseAgain(List<Door> doors) {
        strategy.chooseSecondTime(doors);
    }
}

2.4 Host

ホストは下記コードのようにまず開けるドアを確認してその中にランダムで一つをピックアップして開けます。

public class Host {

    /**
     * Host will open the door which does not have the price.
     * @param doors doors in stage
     */
    public void openDoor(List<Door> doors) {
        doors = doors.stream().filter(this::canOpen).collect(Collectors.toList());
        Collections.shuffle(doors);
        doors.get(0).setOpened(true);
    }

    // if a door is not the answer nor the chosen one, then can be opened.
    private boolean canOpen(Door door) {
        return !door.isAnswer() && !door.isChosen();
    }
}

2.5 Stage

まずステージのインスタンスが作られた時に、ステージ内の三つのドアをランダムで生成します。

注目してもらいたいのはdoOperationです。
今回の考え方はステージはあくまでドアを状態を保存する場所で、
一切具体的なオペレーションが入ってないです。

なので、ステージはあくまで自分が持っているdoorsをステージに操作したい人(プレーヤー・ホスト)に渡します。
該当の人がステージの定義した操作基準によってステージのドアを操作します。

public class Stage {

    private List<Door> doors;
    private final Random random = new Random();

    public Stage() {
        this.doors = randomInitDoors();
    }

    private List<Door> randomInitDoors() {
        int trueIndex = random.nextInt(3);
        doors = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            doors.add(new Door(i, i == trueIndex, false, false));
        }
        return doors;
    }

    public void doOperation(StageOperation op) {
        op.doOperation(doors);
    }

    public boolean isCorrect() {
        return this.doors.stream().filter(Door::isAnswer).anyMatch(Door::isChosen);
    }

    @Override
    public String toString() {
        return "Stage{" +
                "doors=" + doors +
                '}';
    }
}

では、StageOperationについてみてみましょう。

2.5.1 StageOperation

先ほどのChooseStrategyと似ているけど、
ChooseStrategyに定義された操作は継承先用の基準です。

ここで定義された操作は任意のステージを操作したいクラス用のものです。
つまり、ステージを操作したければこのメソッドを実装してくださいの意味です。

@FunctionalInterface
public interface StageOperation {
    void doOperation(List<Door> doors);
}

たぶんここまで行くとわかってると思いますが、
もう一度最初のMainクラスを見ますと:

  • player::chooseDoor
  • host::openDoor
  • player::chooseAgain

は全部暗黙でStageOperation::doOperationを実現したメソッドです。

なぜdoOperationのパラムがStageOperationを求めているのに、全然違うメソッドを渡しても動作するの原因は、
メソッドのシグネチャが一緒からです。

2.6 Main again

TIPS:ここにNotChangeChoiceStrategyを使ってなくて、
ラムダ式を使ってました。

こういうふうにわざわざ各ストラテジーを書かずに、
使いたいときにラムダ式で直接定義するのも便利かもですね。

public static void main(String[] args) {
    int count = 3000;
    int correct = 0;
    for (int i = 0; i < count; i++) {
        // we can also use in-line lambda to replace the implemented NotChangeChoiceStrategy
        // if (play(new NotChangeChoiceStrategy)) {
        if (play(doors -> {})) {
            correct++;
        }
    }
    System.out.printf("not change choice: %d / %d = %.2f%%%n", correct, count, correct * 1.0 / count);

    correct = 0;
    for (int i = 0; i < count; i++) {
        if (play(new ChangeChoiceStrategy())) {
            correct++;
        }
    }
    System.out.printf("change choice: %d / %d = %.2f%%%n", correct, count, correct * 1.0 / count);
}

ちなみにChangeChoiceStrategyもラムダ式で書けるのでぜひやってみてください。

3 実行結果

not change choice: 998 / 3000 = 0.33%
change choice: 1948 / 3000 = 0.65%

やはり選択を変えたほうがいいですね。

5
2
1

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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?