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
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 🙂 )
> 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).
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. 🙁
Nie dokończyłem zadania w stosownym czasie, ale chciałem dodać pewną uwagę.
W swoim rozwiązaniu nie przeprowadzałem symulacji a konkretne wyliczenia prawdopodobieństwa.
Liczba możliwych stanów wcale nie jest astronomicznie duża, wynosi zaledwie 2^19 czyli ledwie ponad pół miliona.
https://tio.run/##jVLBbqMwED3jrxglihTS1gVOvbBSD3vrriq1t4iDwUPqlNjIGCUhyVf1E/ph6RhIt7t76Ylh5s17fjNj23x/Pk9hu69U0cGrMxSurGASS8AdFq3DuRVahiyQKCSkcG@t2HON23kSXUMpqgapqOSOahELVuh@487RTyU2uRRwYEFfvUohHkMCgirBhz9SSKjrxAKigxuIQ@7UBpvDUR8JPYVOi7XsQBowhahQKwEKarNRmsrbF1Uh@IctiSzzqFGfE7gKUEtCfU31nKYu1kaLSivsWWGNTVd0SF@pcWX8M0vwtpc6g7T39V2xf9WG7FcfnchV0dvw0frbNv7UU3C2xWFqEec8iUKOong5HJUfmkXXWk38ZEIbNxJnHu@ZWOOE9Qt6pkFzbbas0O6/xUYhe7r/9fjw84lKyWJByZs4ZHXrGpg80LHkAmr7/pYD3cz0MGJPk0vXsMZ5CMMmLbFc7knTvZDk0mbDUZwY@8vG2DJIzZKWzmIWx/yunMAMlhquIL4GT0DLWUAcRTyCWxh1M6KrP61Ra@/2fP4A
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.