最近統計学に関する本を読んでいます。
その中にモンティ・ホール問題という問題を紹介しています。
ちょっと直感に乖離しているの問題なので、プログラミングで実験してみよと思いました。
そのまま書くのもつまらないので、
FunctionalInterfaceやStrategy 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%
やはり選択を変えたほうがいいですね。