Click here to Skip to main content
15,892,161 members
Articles / Desktop Programming / Windows Forms
Article

Monty Hall Paradox Illustrated

Rate me:
Please Sign up or sign in to vote.
4.59/5 (15 votes)
27 Oct 2008CPOL5 min read 35.5K   171   21   4
A Window Forms application illustrating the Monty Hall paradox.

If your experiment needs statistics, you ought to have done a better experiment.

- Ernest Rutherford

What is Monty Hall paradox?

In case you have watched the movie "21", you may remember a scene in the beginning of the film when a lecturer confronts his student with the following logical puzzle:

Suppose you're on a game show, and you're given the choice of three doors: behind one door is a car; behind the others, goats. You pick a door, and the host who knows what's behind the doors opens another door which has a goat. He then asks if you want to rethink and choose another door. Is it to your advantage to switch your choice?

The student agrees to swap his choice, and he is right. His winning chances will then increase from 1/3 to 2/3. The extract from the movie can be viewed here, and Wikipedia has a fairly comprehensive explanation of this paradox known as Monty Hall problem, named after a television show host.

I will not repeat the solution of the problem here. It’s well explained in various sources available on the net. Still, the paradox triggers hot discussions when sometimes theoretical argumentation is not enough - people may insist that such things just can’t happen.

This is why I decided to write a simple Windows Forms application that takes as input the number of doors (let’s show the statistics for any number of doors, not just three, and the host will have to open N-2 doors with goats behind them) and the number of iterations, and shows the chances of a player that chooses to swap his original choice. For clarity, we will assume that the player chooses to switch his initial choice, so all chances and probabilities will be computed with regards to the strategy based on choice switching.

So much depends on the host!

To make a program more illustrative, I decided to show not just Monty Hall’s case. Since so many people insist that swapping the door does not increase the player’s chances, I wanted to show when it is actually true: and it is true if the host opens doors randomly, with a risk to reveal the car. But, if the host knows where the car is and deliberately opens only doors with goats, then swapping the door is in the player’s interest – and the greater the number of the doors is, the higher the winning chances after swapping.

Well-informed host

Let’s start now with the classical Monty Hall case: when the host knows what’s behind the doors and only open doors with goats behind. Let’s make random selections for a lucky door and the player’s initial choice.

C#
int luckyDoor = random.Next(0, numberOfDoors);
int doorSelectedByPlayer = random.Next(0, numberOfDoors);

Now, we should make a selection for the host, shouldn’t we? Well, actually we don’t need to. He has not so much freedom to make a selection, and even when he has a choice, nothing really depends on it. If the player didn’t pick up a door with the car behind it, then the host will have to open all the remaining doors with goats, leaving unopened the door with the car. In this case, swapping our first choice will make the player a winner. But, if the player had luck in his first attempt, that was his bad luck, because as we mentioned earlier, we evaluate the strategy when the player switches his initial choice; so, no matter what door(s) the host selects to open, the player will choose another door and reveal the goat. Here’s how the algorithm will look for the informed host:

C#
if (luckyDoor == doorSelectedByPlayer)
{
    // Player guessed right on the first attempt,
    // so he will lose after the swap
    return GameResult.Goat;
}
else
{
    // Player missed on the first attempt,
    // so he will win the car after the swap
    return GameResult.Car;
}

Clueless host

The case with a clueless host is somewhat more complicated, since what can happen is that the host randomly picks a door with the car behind it. Obviously, such a situation breaks the game – we are evaluating the winning chances in a situation when the host reveals the goats. So, the rules of the game with a clueless host require the game to be replayed in case the car is revealed while the host opens N-2 doors that are not selected by the player. In this case, unlike the classical Monty Hall situation, the host’s selection must be taken into account:

C#
int luckyDoor = random.Next(0, numberOfDoors);
int doorSelectedByPlayer = random.Next(0, numberOfDoors);
int doorNotSelectedByHost = random.Next(0, numberOfDoors - 1);

if (luckyDoor == doorSelectedByPlayer)
{
    // Player guessed right on the first attempt,
    // so he will lose after the swap
    return GameResult.Goat;
}
else
{
    // Player missed on the first attempt, check if host opened the lucky door
    if (luckyDoor < doorSelectedByPlayer && luckyDoor != doorNotSelectedByHost ||
        luckyDoor > doorSelectedByPlayer && luckyDoor != doorNotSelectedByHost + 1)
    {
        return GameResult.Discarded;
    }
    else
    {
        // Player will win the car after the swap
        return GameResult.Car;
    }
}

Let’s look closer at the code above. In case the player initially selects a door with the car, he will again end up with a goat. The situation when he first selects a door with a goat needs further examination. Instead of selecting N-2 doors to be open by the host, we simply select one of the remaining (unselected) N-1 doors not to be open. It is equivalent. The host may not open a door selected by the player, so we need to implement a "hole" in his door set that corresponds to a player’s door. The game result is discarded (and the game is replayed) when the car is behind one of the doors open by the host (i.e., luckyDoor != doorNotSelectedByHost). If the game is not discarded, then the player will become a winner after swapping his initial choice.

Notes on random number generation and threading

As much as I like to implement self-contained classes (especially in such a simple example), I had to create an instance of a Random object and use it in classes that implemented different host strategies (GameWithInformedHost and GameWithCluelessHost). Creating a random generator in each iteration was too slow, and resulted in less randomized data (because of repetitive use of the same seed). Sharing a single instance of the Random object between classes made the application run much faster.

Originally, I considered running simulated games in multiple threads with continuous progress report, but after testing how blazingly fast is to run such algorithms on modern machines, I concluded that multi-threading would only introduce additional code complexity without any gain in practice. Running a million games take only a few seconds, and is enough to produce results with three significant digits.

Million Monty Hall games at once

The Monty Hall application simulates any number of iterations with any number of doors. Here are the results produced with three doors and ten million iterations:

MontyHall

And, here are the results if the car is behind one of hundred doors:

MontyHall

As you can see, in the case of well-informed host, increasing the number of doors makes swapping the player’s initial choice a must: it’s all or nothing. And, if the host is clueless, the player’s chances are still 50/50.

References

  1. Monty Hall Problem
  2. 21, the movie

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Miles AS
Norway Norway
Vagif Abilov is a Russian/Norwegian software developer and architect working for Miles. He has more than twenty years of programming experience that includes various programming languages, currently using mostly C# and F#.

Vagif writes articles and speaks at user group sessions and conferences. He is a contributor and maintainer of several open source projects, including Simple.Data OData adapter, Simple.OData.Client and MongOData.

Comments and Discussions

 
GeneralMy vote of 2 Pin
mbue18-Aug-11 8:39
mbue18-Aug-11 8:39 
This article explains nothing about the goof in that film.
C++
#pragma once
#include <stdio.h>
#include <tchar.h>
#include <stdlib.h>
#include <windows.h>

template <const int N>
class Doors
{
public:
  typedef enum{ GOAT, CAR, } BEHIND;
  typedef enum{ CLOSED, SELECT, OPEN, } DOOR;
  typedef int  (*FNSHOW)(Doors& doors);

  Doors(){}

  int  PlayGame(FNSHOW fn)
  {
    fill_doors();
    return fn(*this);
  }

  int  IsCar(unsigned int i)
  {
    return i<N?CAR==_door[i]:0;
  }
  void  Select(unsigned int i)
  {
    if(i<N) _select[i] = SELECT;
  }
  void  UnSelect(unsigned int i)
  {
    if((i<N) && (SELECT==_select[i])) _select[i] = CLOSED;
  }
  void  OpenDoor(unsigned int i)
  {
    if(i<N) _select[i] = OPEN;
  }
  unsigned int  ClosedDoor(const unsigned int exclude=-1)
  {
    unsigned int  ix;
    unsigned int  na;
    unsigned int  aa[N];
    for(na=ix=0;ix<N;ix++)
    {
      if((CLOSED==_select[ix]) && (exclude!=ix))
      {
        aa[na++] = ix;
      }
    }
    return 0<na?aa[Random(na)]:-1;
  }
  unsigned int  GoatBehind()
  {
    unsigned int  ix;
    unsigned int  na;
    unsigned int  aa[N];
    for(na=ix=0;ix<N;ix++)
    {
      if((CLOSED==_select[ix]) && (GOAT==_door[ix]))
      {
        aa[na++] = ix;
      }
    }
    return 0<na?aa[Random(na)]:-1;
  }

  unsigned int Won(FNSHOW fn,const unsigned int tries)
  {
    unsigned int    itry;
    unsigned int    nwon;
    for(nwon=itry=0;itry<tries;itry++)
    {
      if(PlayGame(fn)) ++nwon;
    }
    return nwon;
  }

  static unsigned int  Random(const unsigned int n)
  {
    CLSID            clsid; CoCreateGuid(&clsid);
    unsigned short*  lpi = (unsigned short*)&clsid;
    unsigned int    ix,num;

    for(num=ix=0;ix<(sizeof(CLSID)/sizeof(short));ix++)
    {
      num += lpi[ix];
      num = (num & 0xFFFF) ^ (num >> 16);
    }
    return (num*n)>>16;
  }

protected:
  unsigned int  car_door()
  {
    return Random(N);
  }
  void          fill_doors()
  {
    unsigned int  ix;
    unsigned int  car = car_door();
    for(ix=0;ix<car;ix++) _door[ix] = GOAT;
    _door[car] = CAR;
    for(ix=1+car;ix<N;ix++) _door[ix] = GOAT;
    for(ix=0;ix<N;ix++) _select[ix]=CLOSED;
  }
  BEHIND        _door[N];
  DOOR          _select[N];
};

template <const int N>
unsigned int  PlayersDoor(const unsigned int n)
{
  return Doors<N>::Random(n);
}

template <const int N>
int  methodA(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host does nothing
  return doors.IsCar(ix);
}

template <const int N>
int  methodB1(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host must open a door with goat behind
  doors.OpenDoor(doors.GoatBehind());
  // player should select another door
  doors.UnSelect(ix);
  ix = doors.ClosedDoor(ix);
  return doors.IsCar(ix);
}

template <const int N>
int  methodB2(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host must open a door with goat behind
  doors.OpenDoor(doors.GoatBehind());
  // host offers to change the door by player
  if(doors.Random(2))
  {
    doors.UnSelect(ix);
    ix = doors.ClosedDoor(ix);
  }
  return doors.IsCar(ix);
}


template <const int N>
int  methodC1(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host can offer to open a door
  if(doors.Random(2))
  {
    doors.OpenDoor(doors.GoatBehind());
    // player must change the door
    doors.UnSelect(ix);
    ix = doors.ClosedDoor(ix);
  }
  return doors.IsCar(ix);
}

template <const int N>
int  methodC2(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host can offer to open a door
  if(doors.Random(2))
  {
    // player can decide to open a door by host
    if(doors.Random(2))
    {
      doors.OpenDoor(doors.GoatBehind());
      // player must change the door
      doors.UnSelect(ix);
      ix = doors.ClosedDoor(ix);
    }
  }
  return doors.IsCar(ix);
}

template <const int N>
int  methodC3(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host offers to open a door if player is on car
  if(doors.IsCar(ix))
  {
    // player can decide to open a door by host
    if(doors.Random(2))
    {
      doors.OpenDoor(doors.GoatBehind());
      // player must change the door
      doors.UnSelect(ix);
      ix = doors.ClosedDoor(ix);
    }
  }
  return doors.IsCar(ix);
}

template <const int N>
int  methodD1(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host can offers to open a random door
  if(doors.Random(2))
  {
    // player can decide to open a door by host
    if(doors.Random(2))
    {
      // host opens a random door
      doors.OpenDoor(doors.ClosedDoor());
      // in this case the player lost if the car door is opened
      // player must change the door
      doors.UnSelect(ix);
      ix = doors.ClosedDoor(ix);
    }
  }
  return doors.IsCar(ix);
}

template <const int N>
int  methodD2(Doors<N>& doors)
{
  unsigned int  ix = PlayersDoor<N>(N);
  // player should select one door
  doors.Select(ix);
  // host can offers to open a random door
  if(doors.Random(2))
  {
    // player can decide to open a door by host
    if(doors.Random(2))
    {
      unsigned int  open;
      // host opens a random door
      doors.OpenDoor(open = doors.ClosedDoor());
      // player wins if car door is opened
      if(doors.IsCar(open)) return 1;
      // player must change the door
      doors.UnSelect(ix);
      ix = doors.ClosedDoor(ix);
    }
  }
  return doors.IsCar(ix);
}

int _tmain(int argc, _TCHAR* argv[])
{
  enum{ DOORS=3, TRIES = 1000000, };
  Doors<DOORS>      doors;
  unsigned int      won;

  _tprintf(__T("%i doors\r\n"),(unsigned int)DOORS);

  // 
  won = doors.Won(&methodA<DOORS>,TRIES);
  _tprintf(__T("case  A, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  #if(1)
  // 
  won = doors.Won(&methodB1<DOORS>,TRIES);
  _tprintf(__T("case B1, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  // 
  won = doors.Won(&methodB2<DOORS>,TRIES);
  _tprintf(__T("case B2, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  // 
  won = doors.Won(&methodC1<DOORS>,TRIES);
  _tprintf(__T("case C1, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  // 
  won = doors.Won(&methodC2<DOORS>,TRIES);
  _tprintf(__T("case C2, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  // 
  won = doors.Won(&methodC3<DOORS>,TRIES);
  _tprintf(__T("case C3, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  // 
  won = doors.Won(&methodD1<DOORS>,TRIES);
  _tprintf(__T("case D1, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  // 
  won = doors.Won(&methodD2<DOORS>,TRIES);
  _tprintf(__T("case D2, Won: %i of %i (%.1lf%%)\r\n"),won,(unsigned int)TRIES,(double)(100.0*won)/TRIES);
  #endif

  _tprintf(__T("<key>")); _gettch();
  return 0;
}

The result:
<br />
3 doors<br />
case  A, Won: 333154 of 1000000 (33.3%)<br />
case B1, Won: 666359 of 1000000 (66.6%)<br />
case B2, Won: 499127 of 1000000 (49.9%)<br />
case C1, Won: 500294 of 1000000 (50.0%)<br />
case C2, Won: 417358 of 1000000 (41.7%)<br />
case C3, Won: 166598 of 1000000 (16.7%)<br />
case D1, Won: 332154 of 1000000 (33.2%)<br />
case D2, Won: 416791 of 1000000 (41.7%)<br />

Its on you to create your rule to increase the players chance. in my oppinion best matches rules C2 or C3.

Its still a game Wink | ;) .
Regards.

modified on Friday, August 19, 2011 9:20 AM

GeneralMy vote of 5 Pin
Filip D'haene26-May-11 4:31
Filip D'haene26-May-11 4:31 
GeneralCongrats Pin
EricFromMars4-Nov-08 0:54
EricFromMars4-Nov-08 0:54 
GeneralTo evaluate the winning strategy... [modified] Pin
supercat928-Oct-08 8:10
supercat928-Oct-08 8:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.