Ś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.

Skomentuj xpil Anuluj pisanie odpowiedzi

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.