JAVA_2_몬티홀 문제_2(코드로 접근)
* 해당 포스트는 실습 과정 중 학습을 정리하는 글이기에 주관적인 내용이 포함되어 있을 수 있습니다.
잘못된 부분이 있다면 걸러들으시거나 댓글로 남겨주시면 감사하겠습니다.
이제 이 문제를 코드를 이용해 풀이하는 방식으로 접근해보자. 일단 이 문제를 해결하기 위해 파악해야하는 거대한 줄기는 다음과 같다.
1. 당첨될 확률을 참가자인 내가 사회자에게서 게임에 승리한 횟수의 전체 비율로 설정
2. output은 총 시행후 승리와 패배 횟수, 승리 비율
처음부터 설계를 해보자.
일단 이 문제를 코드로 푸는 방법도 여러 가지인데, 앞서 소개했던 내가 선택한 문을 A로 놓고 코드를 풀어갈 수도, 게임의 모든 과정과 유사하게 코드로 표현할 수도 있다. 하지만 선택한 문을 A로 놓고 푸는 것은 결국 내가 A,B,C 세 문 중 무슨 문을 선택하든 수학적인 확률은 같음을 이해해야하며 내 생각엔 뒤에 소개한 방식이 좀 더 이 문제를 일반적으로 접근했다고 판단하여 후자를 베이스로 코드를 진행하려 한다.
1. 게임에서 나올 수 있는 경우의 수는 (선택 변경,미변경) X (승리, 패배)으로 총 4가지이고 이를 변수로 선언한다.
int nswap_win_case=0; // 미변경 후 승리
int nswap_losw_case=0; // 미변경 후 패배
int swap_win_case=0; // 변경 후 승리
int swap_lose_case=0; // 변경 후 패배
2. 게임 시작 후 참가자는 3개의 문 중 하나를 고르는 시행을 한다. 이 때 사회자는 정답을 알기때문에 사회자가 고른 수와 참가자가 고른 수가 일치하면 승리로 인정한다. 이 게임의 횟수는 만 번으로 설정하겠다. Math.random을 사용하고 이 함수의 return 타입, 값은 각각 double, [0,1]사이의 랜덤한 수이기에 int로 형변환, 1~3 중 하나가 나오도록 코드를 짠다. 이 때, 수학적으로 0.3333333... = 1/3이라는 정의를 알기에 난수가 발생할 확률을 모두 동일함을 알지만 혹시 몰라 코드로 테스트 해보았다.
public class TEST {
public static void main(String[] args) {
int first, second, third, result;
first = 0; second = 0; third = 0;
for (int i = 0; i < 10000; i++) {
result = (int) (Math.random() * 3 + 1);
switch (result) {
case 1: first++;
break;
case 2: second++;
break;
case 3: third++;
break;
}
}
System.out.println(first+" "+second+" "+third);
}
}
이 때 시행횟수를 10000으로 하면 결과가 3400 3346 3254, 1000000으로 하면 333858 332958 333184가 나오고 100000000번 하면 33330561 33328980 33340459가 나오고 유효숫자는 시행횟수가 늘어남에 따라 증가하기에 신뢰성을 보장할 수 있다. 따라서 이전에 작성 한 코드는 유효하고 다음과 같다.
int reward = (int) (Math.random()*3+1); // 주최자가 정답을 고름
int choice = (int) (Math.random()*3+1); // 참가자가 정답을 고름
정답과 선택한 것을 고른 이후 사회자는 오답을 선택해 참가자에게 보여준다. 그 코드는 아래와 같다.
// 사회자가 오답을 하나 공개함
int other = (int) (Math.random()*3+1);
while(reward==other || choice == other){ // 변수 other은 정답도 참가자가 선택한 것도 아니어야 함
other = (int) (Math.random()*3+1);
}
다음에 참가자는 선택을 하는데, 변경하지 않는 경우의 코드는 간단하다.
// 이후 선택을 변경하지 않는 경우
if(reward == answer) nswap_win_case++;
else nswap_lose_case++;
하지만 변경하는 경우 조금 생각해 봐야하는데, 이전 코드의 경우 새로운 변수의 선언이 없지만 이후 코드는 다르다. 처음에 얘기한 경우 중 내가 문 A를 선택하는 경우를 코드로 바꾼다면 이전에 선언한 변수를 통해 작성하겠지만 지금 이어나가는 코드는 reward, choice 같은 변수들에 바인딩 된 값이 내가 모르는 난수이기 때문에 참가자가 선택을 바꾼 뒤의 문이 뭔지 모른다. 사실 알 수는 있지만 그 값에 대한 조건문을 추가해야하므로 코드가 너무 길어질게 뻔하기 때문에 쉽게 새로운 변수 swap을 도입해 변경한 뒤의 문 데이터를 할당해준다.
// 변경하는 경우
int swap = (int)(Math.random()*3+1);
while(other==swap || choice==swap){ // swap이랑 다른 변수가 중복되는 경우 다시 난수 부여
swap = (int)(Math.random()*3+1);
}
if(reward==swap) swap_win_case++;
else swap_lose_case++;
이로써 문제를 코드로 바꾸는게 끝났다. 아래는 출력을 위한 코드이다.
System.out.println("미변경 후 승리: "+nswap_win_case);
System.out.println("미변경 후 패배: "+nswap_lose_case);
System.out.println("미변경시 승리 확률: "+nswap_win_case*100/(nswap_lose_case+nswap_win_case)+"%\n");
System.out.println("변경 후 승리: "+swap_win_case);
System.out.println("변경 후 패배: "+swap_lose_case);
System.out.println("변경시 승리 확률: "+swap_win_case*100/(swap_lose_case+swap_win_case)+"%");
아래는 전체 코드이다. 이때 정답은
미변경 후 승리: 33460
미변경 후 패배: 66540
미변경시 승리 확률: 33%
변경 후 승리: 66540
변경 후 패배: 33460
변경시 승리 확률: 66% 로 출력되고 아래는 전체 코드이다.
public class game {
public static void main(String[] args) {
int nswap_win_case = 0; // 미변경 후 승리
int nswap_lose_case = 0; // 미변경 후 패배
int swap_win_case = 0; // 변경 후 승리
int swap_lose_case = 0; // 변경 후 패배
for (int i = 0; i < 100000; i++) { // 총 10만 번 반복한다.
int reward = (int) (Math.random()*3+1); // 주최자가 정답을 고름
int choice = (int) (Math.random()*3+1); // 참가자가 정답을 고름
// 사회자가 오답을 하나 공개함
int other = (int) (Math.random()*3+1);
while(reward==other || choice == other){ // 변수 other은 정답도 참가자가 선택한 것도 아니어야 함
other = (int) (Math.random()*3+1);
}
// 이후 선택을 변경하지 않는 경우
if(reward == choice) nswap_win_case++;
else nswap_lose_case++;
// 변경하는 경우
int swap = (int)(Math.random()*3+1);
while(other==swap || choice==swap){ // swap이랑 다른 변수가 중복되는 경우 다시 난수 부여
swap = (int)(Math.random()*3+1);
}
if(reward==swap) swap_win_case++;
else swap_lose_case++;
}
System.out.println("미변경 후 승리: "+nswap_win_case);
System.out.println("미변경 후 패배: "+nswap_lose_case);
System.out.println("미변경시 승리 확률: "+nswap_win_case*100/(nswap_lose_case+nswap_win_case)+"%\n");
System.out.println("변경 후 승리: "+swap_win_case);
System.out.println("변경 후 패배: "+swap_lose_case);
System.out.println("변경시 승리 확률: "+swap_win_case*100/(swap_lose_case+swap_win_case)+"%");
}
}
이렇게 문제를 다 풀었는데 아직 찜찜한 것이 있다. 이 예제에선 난수 생성을 위해 Math.random()을 사용했지만 값이 실수라는 점에서 Random.nextin()이라는 다른 좋은 선택지도 있었다. 하지만 이 포스트에선 nextInt를 사용하면 인스턴스를 생성해줘야 하기에 코드가 약간 더 길어지고 Math.random()에 수식을 통해 만든 값이 시행횟수가 증가함에 따라 어떻게 변화하는지, 잘 실행되는지를 학습하기 위해 사용한 것이다. 실제로 nextint로 바꿔 코드를 작성해도 전혀 문제가 없다.