Java

몬티홀의 역설 실험

콘스_ 2022. 7. 7. 22:43

몬티홀의 역설을 실험하게 된 계기

아직 블로그에는 포스팅 하지 않았지만, 현재 '자바의 정석'을 5챕터인 배열까지 공부했다. 예전에 몬티홀의 역설에 대해 알게 된후 정말 그렇게 되는지 실험해 봐야겠다고 생각을 했었던것이 생각나서 1~5챕터를 복습할 겸 몬티홀의 역설 프로젝트를 만들었다.

 

몬티홀의 역설 초기구상

자동차와 염소는 각각 O와 X로 표현한다.

switch문을 이용해서 프로그램 종료와 직접 문을 고르는 방식과 횟수를 지정하고 확률만 구하는 방식으로 나눈다.

프로그램 종료를 제외한 방식들은 한번 실행 후 다시 방식을 선택하는 곳으로 돌아온다.

직접하는 방식을 실행하면 프로그램 종료 전까지 실행 횟수, O, X가 나온 횟수, O, X가 나온 확률이 누적된다.

확률계산만 하는 방식의 경우, 실행 횟수, O, X가 나온 횟수, O, X가 나온 확률을 나타낸다.

 

몬티홀의 역설 코드 설명

while (flag) {
            ...
            System.out.println("\t<몬티홀의 역설>");
            System.out.println("1.직접하기 2.확률계산");
            System.out.print("원하는 방식(1~2)을 선택하세요. (종료:0)>");
            
            menu = scanner.nextInt();
            System.out.println();
            ...
            }

while문에 flag를 사용해서 case 0인 프로그램 종료로 가지 않으면 계속 반복하도록 함.

 

case 0: // 프로그램 종료

switch (menu) {
                case 0: // 프로그램 종료
                    System.out.println("프로그램을 종료합니다.");
                    flag = false;
                    break;

방식을 선택할때, 0을 입력하면 "프로그램을 종료합니다."라는 문구와 함께 flag을 false로 저장해 프로그램이 종료하도록 함.

 

case 1: // 직접하기

case 1: // 직접하기
                    for (int i = 0; i < door.length; i++) { // 문 안의 결과를 랜덤으로 섞음
                        j = (int) (Math.random() * door.length);
                        temp = door[i];
                        door[i] = door[j];
                        door[j] = temp;
                    }

'자바의 정석'에서 배열을 공부할 때 나온 배열을 섞는 코드를 참고했다. 문 안의 결과를 섞는다.

while (opFlag) { // 사용자가 문을 고름
                        System.out.println("[1] [2] [3]");
                        System.out.print("1~3번 문 중에서 하나를 고르세요.>");
                        num1 = (scanner.nextInt() - 1);
                        System.out.println();

                        while (true) {
                            op = (int) (Math.random() * door.length); // 사회자가 공개하는 문 번호 (op = 0~2)
                            if (num1 + 1 <= door.length && op != num1 && !door[op].equals("O")) { // 사용자의 선택과 "O"를 제외한 문 하나의 결과를 보여줌
                                System.out.println("당신은 " + (num1 + 1) + "번 문을 고르셨습니다.");
                                System.out.println("사회자가 X가 있는 문 중 하나를 공개합니다.");
                                System.out.printf("%d번 문은 %s입니다.%n", op + 1, door[op]);
                                opFlag = false;
                                break;
                            } else if (num1 + 1 <= door.length && (op == num1 || door[op].equals("O"))) {
                                continue;
                            } else { // while문 처음으로 돌아가 문을 다시 고름
                                System.out.println("잘못된 번호를 입력하셨습니다.");
                                break;
                            }
                        }
                        System.out.println();
                    }

사용자가 문을 고르는 while문이다. 사용자가 1,2,3번 문 중에서 고르고, 사회자는 사용자가 고른 문과 "O"가 있지 않은 문 중 하나를 랜덤으로 선택해서 그 안의 결과를 공개하도록 했다.

만약 사회자가 선택한 문이 사용자가 고른 문이거나 "O"가 들어 있는 문이라면, 사회자가 문을 다시 선택하도록 했다.

4이상의 번호를 사용자가 입력시, 다시 문을 고른다.

while (true) { // 사용자가 선택을 바꿀지 결정함
                        System.out.print("선택을 바꾸시겠습니까? (y/n)>");
                        str = scanner.next();

                        if (str.equals("y")) {
                            System.out.printf("아쉽네요. %d번 문에는 %s가 있었습니다.%n", num1 + 1, door[num1]);
                            break;
                        } else if (str.equals("n")) { // 사회자가 공개한 문과 사용자가 선택한 문을 제외한 문을 공개함
                            for (int i = 0; i < door.length; i++) {
                                if (i != num1 && i != op) {
                                    System.out.printf("축하합니다. %d번 문에는 %s가 있었습니다.%n", i + 1, door[i]);
                                }
                            }
                            break;
                        } else {
                            System.out.println("y와 n 중 하나를 입력해주세요.");
                        }
                    }
                    System.out.println();

                    break; // case 1(직접하기)의 break

사회자가 "X"가 있는 문을 공개한 후, 사용자가 처음 고른 문이 아닌 다른 문을 선택할 지 결정한다.

'y'를 입력하면 사용자가 처음에 선택한 문의 결과를 보여줌.

'n'을 입력하면 사용자가 처음에 선택한 문과 사회자가 공개한 문을 제외한 문의 결과를 보여줌.

 

case 2: // 확률계산

case 2: // 확률계산
                    int com = 0; // 컴퓨터가 임의로 선택한 값

                    System.out.println("바꿨을 경우, O와 X가 나온 횟수와 둘이 나온 확률을 구합니다.(소수점 7번째 자리 이하는 버려짐.)");
                    System.out.print("몇 번 실행하시겠습니까?>");
                    num2 = scanner.nextInt();
                    System.out.println();

                    for (int k = 0; k < num2; k++) { // 사용자가 입력한 실행 횟수만큼 반복함
                        for (int i = 0; i < door.length; i++) { // 문 안의 결과를 랜덤으로 섞음
                            j = (int) (Math.random() * door.length);
                            temp = door[i];
                            door[i] = door[j];
                            door[j] = temp;
                        }
                        
                        ...
                        }

사용자가 반복 횟수를 입력하면 그만큼 반복함.

반복할때 마다, 문 안의 결과는 섞임.

 com = (int) (Math.random() * door.length); // com = 0~2

                        while (true) {
                            op = (int) (Math.random() * door.length); // 사회자가 공개하는 문 번호 (op = 0~2)

                            if (op != com && !door[op].equals("O")) {
                                break;
                            } else if (op == com || door[op].equals("O")) {
                                continue;
                            }
                        }

com이 사용자 대신 랜덤으로 문을 선택함.

사회자는 com이 선택한 문과 "O"가 들어있지 않은 문 중에서 랜덤으로 선택해서 그 값을 저장함.

for (int i = 0; i < door.length; i++) { // 선택을 바꿈
                            if (i != com && i != op) {
                                if (door[i].equals("O")) {
                                    car += 1; // 바꾼 선택이 O일시 car에 +1함
                                } else {
                                    goat += 1; // 바꾼 선택이 X일시 goat에 +1함
                                }
                            }
                        }
                    } // 사용자가 입력한 반복횟수만큼 수행후 종료

선택을 바꿨을 경우, O가 나온 횟수와 X가 나온 횟수를 각각 car와 goat에 저장함.

import java.text.DecimalFormat;
...
                    exe = df.format(num2);
                    carExe = df.format(car);
                    goatExe = df.format(goat);

                    cPer = ((double) car / (double) num2) * 100;
                    gPer = ((double) goat / (double) num2) * 100;

                    System.out.println("실행 횟수: " + exe + "번");
                    System.out.printf("O가 나온 횟수: %s번\t X가 나온 횟수: %s번%n", carExe, goatExe);
                    System.out.printf("O가 나온 확률: %f%c  X가 나온 확률: %f%c%n", cPer, '%', gPer, '%');
                    System.out.println();

                    break; // case 2(확률계산)의 break

DecimalFormat 클래스를 사용해서 천단위로 콤마를 찍도록 함.

O, X가 나온 횟수와 확률을 출력함

 

default:

default:
                    System.out.println("잘못된 번호를 입력하셨습니다.");
                    System.out.println();

원하는 방식을 선택할 때, 0~2가 아닌 다른 숫자를 입력시, 다시 돌아가서 선택하도록 함.

 

전체 코드

import java.text.DecimalFormat;
import java.util.Scanner;

public class MontyHall {
    public static void main(String[] args) {
        boolean flag = true; // 최상위 while문 조건식
        int num1 = 0, num2 = 0; // 사용자의 선택을 저장할 변수
        int menu = 0; // 직접하기, 확률계산 선택하는 switch문 조건식을 저장할 변수
        int op = 0; // 사회자가 고개하는 문 번호를 저장할 변수
        int j = 0; // 임의의 값을 얻어서 저장할 변수(문 안 결과를 섞을 떼 시용)
        String temp = " "; // 두 값을 바꾸는 데 사용할 임시변수(문 안 결과를 섞을 떼 시용)
        String str = " "; // 선택을 바꿀지 고른 후 결과를 보여주는 switch문 조건식을 저장할 변수
        String exe = " ";
        String carExe = " ";
        String goatExe = " ";
        String[] door = {"O", "X", "X"}; // 문 안에 존재하는 결과

        Scanner scanner = new Scanner(System.in);
        DecimalFormat df = new DecimalFormat("###,###");

        System.out.printf("몬티홀의 역설에 따르면, 사회자가 염소가 있는 문을 공개한 후에 선택을 바꾸면%n" +
                "자동차가 나올 확률이 2/3, 염소가 나올 확률이 1/3이라고 한다.%n" +
                "자동차는 O, 염소는 X로 해서 몬티홀의 역설을 실험할 것이다.%n%n");

        while (flag) {
            int car = 0; // O
            int goat = 0; // X
            double cPer = 0.0; // O가 나온 확률
            double gPer = 0.0; // X가 나온 확률
            boolean opFlag = true; // 사용자가 처음 문을 고르는 while문 조건식

            System.out.println("\t<몬티홀의 역설>");
            System.out.println("1.직접하기 2.확률계산");
            System.out.print("원하는 방식(1~2)을 선택하세요. (종료:0)>");

            menu = scanner.nextInt();
            System.out.println();

            switch (menu) {
                case 0: // 프로그램 종료
                    System.out.println("프로그램을 종료합니다.");
                    flag = false;
                    break;
                case 1: // 직접하기
                    for (int i = 0; i < door.length; i++) { // 문 안의 결과를 랜덤으로 섞음
                        j = (int) (Math.random() * door.length);
                        temp = door[i];
                        door[i] = door[j];
                        door[j] = temp;
                    }

                    while (opFlag) { // 사용자가 문을 고름
                        System.out.println("[1] [2] [3]");
                        System.out.print("1~3번 문 중에서 하나를 고르세요.>");
                        num1 = (scanner.nextInt() - 1);
                        System.out.println();

                        while (true) {
                            op = (int) (Math.random() * door.length); // 사회자가 공개하는 문 번호 (op = 0~2)
                            if (num1 + 1 <= door.length && op != num1 && !door[op].equals("O")) { // 사용자의 선택과 "O"를 제외한 문 하나의 결과를 보여줌
                                System.out.println("당신은 " + (num1 + 1) + "번 문을 고르셨습니다.");
                                System.out.println("사회자가 X가 있는 문 중 하나를 공개합니다.");
                                System.out.printf("%d번 문은 %s입니다.%n", op + 1, door[op]);
                                opFlag = false;
                                break;
                            } else if (num1 + 1 <= door.length && (op == num1 || door[op].equals("O"))) { //TODO 숫자0 입력시 else로 가지 않는 버그 수정해야함
                                continue;
                            } else { // while문 처음으로 돌아가 문을 다시 고름
                                System.out.println("잘못된 번호를 입력하셨습니다.");
                                break;
                            }
                        }
                        System.out.println();
                    }


                    while (true) { // 사용자가 선택을 바꿀지 결정함
                        System.out.print("선택을 바꾸시겠습니까? (y/n)>");
                        str = scanner.next();

                        if (str.equals("y")) {
                            System.out.printf("아쉽네요. %d번 문에는 %s가 있었습니다.%n", num1 + 1, door[num1]); // TODO 아쉽네요, 축하합니다 수정
                            break;
                        } else if (str.equals("n")) { // 사회자가 공개한 문과 사용자가 선택한 문을 제외한 문을 공개함
                            for (int i = 0; i < door.length; i++) {
                                if (i != num1 && i != op) {
                                    System.out.printf("축하합니다. %d번 문에는 %s가 있었습니다.%n", i + 1, door[i]);
                                }
                            }
                            break;
                        } else {
                            System.out.println("y와 n 중 하나를 입력해주세요.");
                        }
                    }
                    System.out.println();

                    break; // case 1(직접하기)의 break
                case 2: // 확률계산
                    int com = 0; // 컴퓨터가 임의로 선택한 값

                    System.out.println("바꿨을 경우, O와 X가 나온 횟수와 둘이 나온 확률을 구합니다.(소수점 7번째 자리 이하는 버려짐.)");
                    System.out.print("몇 번 실행하시겠습니까?>");
                    num2 = scanner.nextInt();
                    System.out.println();

                    for (int k = 0; k < num2; k++) { // 사용자가 입력한 실행 횟수만큼 반복함
                        for (int i = 0; i < door.length; i++) { // 문 안의 결과를 랜덤으로 섞음
                            j = (int) (Math.random() * door.length);
                            temp = door[i];
                            door[i] = door[j];
                            door[j] = temp;
                        }

                        com = (int) (Math.random() * door.length); // com = 0~2

                        while (true) {
                            op = (int) (Math.random() * door.length); // 사회자가 공개하는 문 번호 (op = 0~2)

                            if (op != com && !door[op].equals("O")) {
                                break;
                            } else if (op == com || door[op].equals("O")) {
                                continue;
                            }
                        }

                        for (int i = 0; i < door.length; i++) { // 선택을 바꿈
                            if (i != com && i != op) {
                                if (door[i].equals("O")) {
                                    car += 1; // 바꾼 선택이 O일시 car에 +1함
                                } else {
                                    goat += 1; // 바꾼 선택이 X일시 goat에 +1함
                                }
                            }
                        }
                    } // 사용자가 입력한 반복횟수만큼 수행후 종료

                    exe = df.format(num2);
                    carExe = df.format(car);
                    goatExe = df.format(goat);

                    cPer = ((double) car / (double) num2) * 100;
                    gPer = ((double) goat / (double) num2) * 100;

                    System.out.println("실행 횟수: " + exe + "번");
                    System.out.printf("O가 나온 횟수: %s번\t X가 나온 횟수: %s번%n", carExe, goatExe);
                    System.out.printf("O가 나온 확률: %f%c  X가 나온 확률: %f%c%n", cPer, '%', gPer, '%');
                    System.out.println();

                    break; // case 2(확률계산)의 break
                default:
                    System.out.println("잘못된 번호를 입력하셨습니다.");
                    System.out.println();
            }
        }
    }
}

 

보완할 점과 느낀 점

보완할 점

case 1에서

  • 사용자가 문을 선택할 때, 0을 입력시, 다시 입력하라고 반복하지 않고 그대로 실행되는 버그 수정해야함.
  • 최종 결과를 출력할때, 'y'를 입력시, 앞에 무조건 '아쉽네요.'를 출력하는 것과 'n'을 입력시, 무조건 '축하합니다.'를 출력하는 버그 수정해야함.
  • 문을 공개할 때마다 그 문의 상태를 쉽게 확인 활 수 있도록 추가해야함.
  • O, X가 나온 횟수와 확률를 프로그램 종료 전까지 누적해서 보여주도록 추가해야함.
  • 문을 [1] [2] [3]으로 표현했지만, 좀더 문처럼 표현하고, 'O', 'X'가 아닌 자동차나 염소로 표현이 가능하다면 그렇게 수정하고 싶다.

 

느낀 점

생각보다 만들때 고려해야 할 부분도 많았고, 자잘한 실수가 많았다. 최대한 코드가 깔끔해 보이게 하고 싶었는데 그나마 깔끔해 보일지는 잘 모르겠다. 과제나 예제가 아닌 처음으로 내가 만든 프로젝트였기에 시행착오가 많았지만, 재밌게 작업했다.

현재 찾은 보완할 점은 조금씩 수정해서 깃헙(https://github.com/kons2003/MontyHall)에 업로드 할 것이다.