Śmiertelna zagadka – rozwiązanie

https://xpil.eu/azhs

Zagadka numer 1 jest faktycznie banalna i można ją rozwiązać w dwie minuty za pomocą kartki i ołówka, i w zasadzie bez żadnego pomyślunku. Przeżyje więzień z plakietką numer 9.

Dla pewności jednak - mój kod symulujący pierwszą zagadkę:

n = 20  # Liczba uczestników
k = 2  # Krotność (co drugi)
więźniowie = list(range(1, n+1)) # plakietki ponumerowane od 1, lista poindeksowana od 0
bieżący = 0  # Zaczynamy od pierwszego więźnia
while len(więźniowie) > 1: # Dopóki żyje więcej niż jeden więzień
    bieżący = (bieżący + k - 1) % len(więźniowie)
    del więźniowie[bieżący]
print(f"Przeżył więzień numer {więźniowie[0]}")

Wynik: Przeżył więzień numer 9

Oryginalnie problem znany jest jako permutacja Flawiusza. Jeżeli zajrzymy tam do sekcji "Przypadek ogólny", to z prezentowanego tam wzoru na J(n) można napisać całkiem interesującą funkcję znajdującą zwycięzcę rekurencyjnie:

def flawiusz(n, k):
    return 1 if n == 1 else (flawiusz(n - 1, k) + k - 1) % n + 1

print(f"Przeżył więzień numer {flawiusz(20, 2)}")

Nie będę kłamał, że rozumiem tę rekurencję, ale rozwiązanie podoba mi się ze względu na swoją uniwersalność i zwartość.

Zagadka numer 2 jest nieco bardziej skomplikowana, jeżeli chcielibyśmy rozwiązać ją w sposób formalny. Teoretycznie można próbować bawić się w modelowanie procesów Markowa, jednak w rzeczywistości liczba możliwych stanów przy dwudziestu uczestnikach jest astronomicznie duża. Dla czterech czy pięciu więźniów jeszcze miałoby to sens. Dla dwudziestu - nie za bardzo. Dlatego uciekamy się do symulacji.

Kod będzie bardzo podobny jak przy zagadce numer 1, tylko musimy za każdym razem losować wartość k a także przeprowadzić odpowiednio dużo eksperymentów, żeby wynik był jak najbardziej zbliżony do rzeczywistości.

import random
from collections import Counter

def symulacja(n):
    więźniowie = list(range(1, n+1))
    bieżący = 0  # Zaczynamy liczyć od gościa z plakietką 1
    while len(więźniowie) > 1:
        k = random.choice([2, 3])  # losujemy długość kroku - 2 lub 3
        bieżący = (bieżący + k - 1) % len(więźniowie)  # wyliczamy pozycję kolejnego więźnia do ubicia
        del więźniowie[bieżący]  # szczelamy
    return więźniowie[0]  # ostatni będą piersiami czy coś

liczba_więźniów = 20
liczba_symulacji = 100000

wyniki = Counter(symulacja(liczba_więźniów) for _ in range(liczba_symulacji))

print("Szanse i numery pierwszej trójki na przeżycie:", [f"{x[0]}:{round(x[1]/liczba_symulacji*100,1)}%" for x in wyniki.most_common(3)])

Po stu tysiącach symulacji wyszło mi:

Szanse i numery pierwszej trójki na przeżycie: ['18:6.8%', '19:6.7%', '17:6.7%']

Oczywiście the more, the merrier, proszę sobie zrobić milion albo i dziesięć milionów symulacji jak ktoś ma cierpliwość.

Wychodzi więc na to, że wybierając plakietkę 17, 18 lub 19 mamy około 6.7% szans na przeżycie. Szału nie ma.

Ponieważ formularz odpowiedzi dopuszcza wyłącznie liczby całkowite (moje niedopatrzenie), postanowiłem uznać za poprawne odpowiedzi 6%, 7% lub 8%.

A jak Wam poszło?

1Jeszcze tego samego dnia pierwsze zgłoszenie nadesłał Cichy Fragles i chociaż numery plakietek wyszły mu całkiem błędne (a więc znalazłszy się w tej konkretnej sytuacji, prawdopodobnie wybrałby złą plakietkę), to formalnie muszę mu tę odpowiedź uznać, bo podał prawdopodobieństwo 7%, a w treści zadania pytałem właśnie o tę liczbę.

2Nazajutrz prawidłową odpowiedź nadesłał Waldek. Jego odpowiedź na drugą zagadkę jest nieco zagadkowa, bo bez żadnych objaśnień a także z dwoma miejscami po przecinku (6.73), co zgadza się z symulacją. Pochwal się, Waldku, w wolnej chwili, jaką metodą dotarłeś do wyniku.

3W niedzielę nadeszło połowiczne rozwiązanie Buttera. Połowiczne, bo Butter pokusił się tylko o rozwiązanie pierwszej zagadki. Co do drugiej - wysłał mi tylko krótką wiadomość, że chwilowo nie ma wystarczająco zapału, może kiedyś się zbierze. Zaliczam połowicznie 🙂

4Rozie przysłał swoje rozwiązanie ciut później, też połowiczne. Nadgryzł pierwszą zagadkę algorytmem trochę dookoła, ale wynik mu wyszedł poprawny. Podobnie jak Butterowi, tu też zaliczam połowicznie.

prisoners = [ n for n in range(1, 21) ]
dead = []

skip = True
while len(dead) < 20:
    for p in prisoners:
        if p not in dead:
            if skip:
                skip = False
            else:
                dead.append(p)
                skip = True
print(dead)

5W okolicach wtorku wieczorem drugą odpowiedź nadesłał Waldek, przy okazji wyjaśniło się, że byle jak zorganizowałem nie tylko pole z odpowiedzią (niemożność wpisania ułamków), ale też miejsce do zamieszczania komentarzy. Na pohybel mi.

Wisienką na torcie waldkowego rozwiązania jest załączony kod, który - zamiast wszechobecnego ostatnimi czasy Pythona - został napisany w Visual Basic-u Delphi:

Function Flawiusz(n: integer): integer;
begin
  if n=1 then begin
    Result:=1;
    Exit;
  end;
  Result:=((Flawiusz(n-1)+Random(2)+1) mod n)+1;
end;

const zakres=10000000;

procedure TForm1.BStartClick(Sender: TObject);
var i, j: integer;
    s: string;
    tRes: array[1..20]of integer;
begin
  Randomize;
  for j:=1 to zakres do
    Inc(tRes[Flawiusz(20)]);

... Liczenie średniej trzech najlepszych wyników i drukowanie...
end;

Wyniki faktycznie dają sporą dokładność (10M prób - nie w kij dmuchał):

Wyniki:
------
1:  622837
2:  306165
3:  302503
4:  454501
5:  306385
6:  389789
7:  398538
8:  375279
9:  432581
10:  435332
11:  465354
12:  501813
13:  525919
14:  569315
15:  605702
16:  637130
17:  667813
18:  678269  <-- maks1
19:  672987  <-- maks2
20:  651788  <-- maks3

Średnia: 0,0667681333333333

https://xpil.eu/azhs

5 komentarzy

  1. 1. To nie VB, lecz Delphi – mój drugi język zaraz po polskim.
    2. Delphi umie kompilować, więc 10 milionów pętli * 20 rekurencji = 200 milionów wywołań funkcji, to tylko 0,9 sekundy.
    3. Funkcja rekurencyjna jest do ogarnięcia, jeśli zacznie się od 1 (a potem to już samo idzie 🙂 )

    1. > To nie VB, lecz Delphi

      Danke – skorygowałem w treści wpisu. Niestety używana przeze mnie wtyczka do formatowania kodu nie ma opcji Delphi (ani Pascal).

  2. Hmm, czemu dookoła? To chyba najbardziej łopatologiczna symulacja. Bo problemu oczywiście nie rozpoznałem, a nawet nie znałem. Wiec właściwego, optymalnego algorytmu nie użyłem.

    Tu ciekawostka, teraz sprawdziłem i ChatGPT co prawda problem rozpoznał, ale dwukrotnie podał błędną odpowiedź (17 i 1, odpowiednio).

    Część druga – brak czasu/zapomniałem. 🙁

    1. Liczba możliwych stanów – owszem, jednak aby policzyć zadanie metodą procesów Markowa potrzebujemy zbudować macierz przejść stanów, łącznie jakieś 274 mld komórek. Owszem, może to nie “astronomiczna” liczba, ale jednak już kłopotliwa do policzenia.

Leave a Comment

Komentarze mile widziane.

Jeżeli chcesz do komentarza wstawić kod, użyj składni:
[code]
tutaj wstaw swój kod
[/code]

Jeżeli zrobisz literówkę lub zmienisz zdanie, możesz edytować komentarz po jego zatwierdzeniu.