StartseiteMatherätselDas ZiegenproblemLösung

Lösung: Das Ziegenproblem

Rätsel einblenden

Lösung

Zuerst entscheidet sich der Kandidat für eine Tür. Die Wahrscheinlichkeit, dass das Auto dahinter steht liegt bei 1/3. Die Wahrscheinlichkeit, dass es hinter einer der beiden anderen Türen steht liegt also bei 2/3. Daran ändert auch die Tatsache, dass eine der beiden anderen Türen geöffnet wurde, nichts. Folglich wäre es für den Kandidaten besser, die Tür zu wechseln, denn damit erhöht er seine Gewinnchance von 1/3 auf 2/3.

Ausblenden

Simulationsprogramm

Wer das so nicht glauben kann, der möge sich mal das folgende Simulationsprogramm ansehen, kompilieren (dazu werden die QT-Bibliotheken benötigt) und ausführen. Das Programm basiert auf Threads, kann also die Rechenleistung von Multicore-Prozessoren voll ausnutzen. Es besteht im Wesentlich aus zwei Klassen: einer, in der die eigentliche Simulation implementiert ist und einer, die die Simulation steuert.

Zuerst ein paar Worte zur Simulations-Klasse: In dieser Klasse wurde in der run()-Methode die eigentliche Simulation implementiert. Diese benötigt an verschiedenen Stellen Zufallszahlen. Diese werden durch einen linearen Kongruenzgenerator "errechnet", da der in der cstdlib-Bibliothek implementierte Zufallszahlengenerator nicht so effizient ist. Im Prinzip sind hier sogar drei Generatoren implementiert: einer für das Auto, einer für den Kandidaten und einer für den Moderator. Würde alles über einen Generator laufen, so wären die Zufallszahlen innerhalb einer Spielrunde voneinander abhängig, was das Ergebnis etwas verfälschen würde.

  1. #include <iostream>
  2. #include <cstdlib>
  3. #include <QThread>

  4. class Simulation : public QThread {

  5.   private:
  6.   // Konstanten fuer den Zufallszahlengenerator
  7.   static const unsigned long MASK = (1L << 48) - 1;
  8.   static const unsigned long ADD = 0xBL;
  9.   static const unsigned long MULTIPLIER = 0x5DEECE66DL;
  10.   // Hier werden die letzten Zufallszahln gespeichert
  11.   unsigned long m_random[3];
  12.   // Durchlaufzaehler
  13.   int m_counter;
  14.   // mit oder ohne Wechseln
  15.   bool m_shift;
  16.   // Zeiger auf den Gewinnzaehler
  17.   int *m_wins;

  18.   public:
  19.   Simulation(int counter, bool shift, int *wins) {
  20.   m_counter = counter;
  21.   m_shift = shift;
  22.   m_wins = wins;
  23.   // Drei unabhaengige Zufallszahlengeneratoren initialisieren
  24.   for (int i=0;i<3;i++) {
  25.   m_random[i] = rand();
  26.   }
  27.   }

  28.   protected:
  29.   void run() {
  30.   // So oft hat der Kandidat in diesem Thread schon gewonnen
  31.   int wins = 0;
  32.   while (m_counter > 0) {
  33.   // Das Auto wird versteckt
  34.   int car = getRandom(3,0);
  35.   // Der Kandidat entscheidet sich fuer eine Tuer
  36.   int kandidat = getRandom(3,1);
  37.   // Diese Tueren darf der Moderator nicht oeffnen
  38.   bool blocked[3];
  39.   for (int i=0;i<3;i++) {
  40.   blocked[i] = false;
  41.   }
  42.   // Naemlich die mit dem Auto
  43.   blocked[car] = true;
  44.   // und die mit dem Kandidaten
  45.   blocked[kandidat] = true;
  46.   // Jetzt sucht er sich zufaellig Tueren raus, bis
  47.   // eine nicht blockiert ist, und oeffnet diese
  48.   int opened;
  49.   do {
  50.   opened = getRandom(3,2);
  51.   } while (blocked[opened]);
  52.   // Je nachdem, was simuliert wird: Wechseln oder nicht.
  53.   if (m_shift) {
  54.   // Das ist genau die andere verschlossene Tuer
  55.   kandidat = 3 - (kandidat + opened);
  56.   }
  57.   // Gewinnt der Kandidat das Auto?
  58.   if (car == kandidat) {
  59.   wins++;
  60.   }
  61.   // Durchlauf vorbei: Zaehler eins runterzaehlen
  62.   m_counter--;
  63.   }
  64.   // Zum Gesamtergebnis dazuaddieren
  65.   (*m_wins) += wins;
  66.   }

  67.   private:
  68.   int getRandom(long n, int i) {
  69.   // linearer Kongruenzgenerator
  70.   m_random[i] = (MULTIPLIER*m_random[i]+ADD)%MASK;
  71.   return static_cast<int>(m_random[i]%n);
  72.   }

  73. };

Die andere Klasse steuert den Ablauf der Simulation. Im Wesentlichen besteht der Sinn dieser Klasse darin, mehrere Threads zu starten, zu warten, bis diese mit der Simulation fertig sind und zum Schluss das Ergebnis auszugeben. Etwas verwirrend ist vielleicht, dass theThreads.at(i)->start() die run()-Methode der Simulationsklasse aufruft, aber das ist bei der Threadprogrammierung halt so.

  1. #include <vector>
  2. #include <iostream>

  3. #include "Simulation.hpp"

  4. class LetsMakeADeal {

  5.   private:
  6.   // So viele Threads sollen gleichzeitig laufen
  7.   int m_threads;

  8.   public:
  9.   LetsMakeADeal(int threads) {
  10.   m_threads = threads;
  11.   // Titel ausgeben
  12.   std::cout << std::endl;
  13.   std::cout << "Simulation der TV-Show Let's make a deal: "
  14.   << "Das Ziegenproblem" << std::endl;
  15.   std::cout << std::endl;
  16.   }

  17.   void tvShow(int runs, bool shift) {
  18.   // Gewinne zaehlen
  19.   int wins = 0;
  20.   // Hier werden die verschiedenen Threads gespeichert
  21.   std::vector<Simulation*> theThreads;
  22.   // Der erste Thread faengt den Fehler der Integerdivision ab
  23.   int counter = runs / m_threads + runs % m_threads;
  24.   theThreads.push_back(new Simulation(counter,shift,&wins));
  25.   // Der Rest wird auf die anderen Threads aufgeteilt
  26.   counter = runs / m_threads;
  27.   for (int i=1;i<m_threads;i++) {
  28.   theThreads.push_back(new Simulation(counter,shift,&wins));
  29.   }
  30.   // Threads starten
  31.   for (int i=0;i<m_threads;i++) {
  32.   theThreads.at(i)->start();
  33.   }
  34.   // Bis alle Threads fertig sind warten
  35.   for (int i=0;i<m_threads;i++) {
  36.   theThreads.at(i)->wait();
  37.   }
  38.   // Das Ergebnis der Simulation ausgeben
  39.   std::cout << "Simulation " << (shift ? "mit" : "ohne")
  40.   << " Wechsel der Tuer." << std::endl;
  41.   std::cout << wins << " Gewinne in " << runs
  42.   << " Runden." << std::endl;
  43.   std::cout << "Anteil der Gewinne: "
  44.   << ((double)wins)/((double)runs) << std::endl;
  45.   std::cout << std::endl;
  46.   }

  47. };

Zur main()-Methode gibt es nicht viel zu sagen. Hier werden die für die Simulation relevanten Konstanten festgelegt und die Simulation einmal mit einem "Wechsel-Kandidaten" und einmal mit einem "nicht-Wechsel-Kandidaten" gestartet.

  1. #include "LetsMakeADeal.hpp"

  2. int main() {
  3.   // So oft soll simuliert werden
  4.   const int RUNS = 2147483647;
  5.   // So viele Threads sollen benutzt werden
  6.   const int THREADS = 2;
  7.   // Zufallszahlengenerator initialisieren
  8.   srand((unsigned)time(NULL));
  9.   // Simuliere mit Threads
  10.   LetsMakeADeal* theShow = new LetsMakeADeal(THREADS);
  11.   // Zuerst mit Wechseln
  12.   theShow->tvShow(RUNS,true);
  13.   // dann ohne Wechseln
  14.   theShow->tvShow(RUNS,false);
  15.   return 0;
  16. }

Ich habe das Programm mit diesen Konstanten auf meinem Rechner (CPU: AMD Turion™ 64 X2 TL-50 @ 1,6 GHz (Dual-Core)) durchlaufen lassen und habe folgendes Ergebnis bekommen:

Simulation der TV-Show Let's make a deal: Das Ziegenproblem Simulation mit Wechsel der Tuer. 1431362079 Gewinne in 2147483647 Runden. Anteil der Gewinne: 0.66653 Simulation ohne Wechsel der Tuer. 715745454 Gewinne in 2147483647 Runden. Anteil der Gewinne: 0.333295

Das ganze hat 2:38 Minuten bei voller Prozessorauslastung gedauert. Danach habe ich das Programm nochmal mit nur einem Thread, also auch nur auf einem Prozessor, gestartet. Nach 5:12 Minuten hatte ich ein ähnliches Ergebnis. Die Programmierung mit Threads hat also in diesem Fall die Laufzeit auf die Hälfte verkürzt.

Zum Abschluss habe ich das Programm dann noch mit acht Threads auf einem Server (CPU: Intel® Xeon® E5420 @ 2,5 GHz (Dual-Quad-Core)) laufen lassen. Dieser war schon nach 30 Sekunden mit der Simulation fertig.