711 wyrazów o optymalizacji – część 2

Tym, co nie czytali polecam lekturę części pierwszej, a tymczasem pojawił się wpis z rozwiązaniem i pojawiło się tam znacznie lepsze podejście do tematu. Korzysta ono z właściwości, że wszystkie składowe muszą być dzielnikami 7,11 i wielokrotnością 0,01. W wersji całkowitej – muszą być liczbami całkowitymi będącymi dzielnikami . Następnie generuje kombinacje tych liczb i sprawdza właściwe warunki. Lekko dostosowany kod to:

import itertools
number = 711
iterations = 0
divs = list()
for i in range(1, number + 1):
    if 711000000 % i == 0:
        divs.append(i)

for i in itertools.combinations_with_replacement(divs, 4):
    iterations += 1
    if sum(i) == 711 and i[0] * i[1] * i[2] * i[3] == 711000000:
        print(i, iterations)

print(len(divs))

Dodałem wyświetlanie ilości dzielników – jest ich raptem 62, więc przeszukiwana przestrzeń to 62^4 czyli… niecałe 15 mln. Rozwiązanie jest znajdowane po nieco ponad 600 tys. iteracji w czasie… pomijalnym, bowiem ok. 0,2 sekundy, niezależnie od interpretera. Przy pomiarze tak niskich czasów wykonania wypadałoby się pobawić już w uśrednianie, ale chodzi o wartości orientacyjne.

W zasadzie nie ma sensu optymalizować dalej, ale pobawić się można. Przede wszystkim, można wyeliminować samo 711 – jeśli którakolwiek wartość byłaby taka, to pozostałe musiałyby być zerami, co jest sprzeczne z warunkami zadania. Kolejny całkowity dzielnik to połowa 711. Po uwzględnieniu tego, skrypt przyjmie postać:

import itertools
number = 711
iterations = 0
divs = list()
for i in range(1, round(number/2) + 1):
    iterations += 1
    if 711000000 % i == 0:
        divs.append(i)

for i in itertools.combinations_with_replacement(divs, 4):
    iterations += 1
    if sum(i) == 711 and i[0] * i[1] * i[2] * i[3] == 711000000:
        print(i, iterations)

print(len(divs))

Ogranicza nam to liczbę sprawdzanych dzielników do 49, przestrzeń do niecałych 6 mln, a liczbę iteracji potrzebnych do znalezienia rozwiązania do 266 tys. Dla przypomnienia, w pierwszym rozwiązaniu, które było czystym brute force zaczynaliśmy od przestrzeni 225 miliardów, czyli 44 tys. razy większej.

A gdyby tak nadal korzystać z dzielników, ale zapomnieć o itertools i wrócić do starych, dobrych pętli, tym razem nie na wartościach, tylko na indeksach w liście divs? Wersja naiwna to:

number = 711
iterations = 0
divs = list()
for i in range(1, round(number/2) + 1):
    iterations += 1
    if 711000000 % i == 0:
        divs.append(i)

for a in range(0, len(divs)-1):
    for b in range(0, len(divs)-1):
        for c in range (0, len(divs)-1):
            for d in range (0, len(divs)-1):
                iterations += 1
                if divs[a] + divs[b] + divs[c] + divs[d] == 711:
                    if divs[a] * divs[b] * divs[c] * divs[d] == 711000000:
                        print("Solved: ", divs[a], divs[b], divs[c], divs[d], iterations)
            exit()

Wyraźny krok wstecz – 3,6 mln iteracji i prawie sekunda (PyPy nadal ~0,2s). Ale otwiera nam to drogę do znanych już optymalizacji:

number = 711
iterations = 0
divs = list()
for i in range(1, round(number/2) + 1):
    iterations += 1
    if 711000000 % i == 0:
        divs.append(i)

for a in range(0, len(divs)-1):
    for b in range(a, len(divs)-1):
        for c in range (b, len(divs)-1):
            for d in range (c, len(divs)-1):
                iterations += 1
                if divs[a] + divs[b] + divs[c] + divs[d] == 711:
                    if divs[a] * divs[b] * divs[c] * divs[d] == 711000000:
                        print("Solved: ", divs[a], divs[b], divs[c], divs[d], iterations)
            exit()

Jest tak dobrze, jak przy itertoolsach: ~0,2s oraz 246 tys. iteracji. Pamiętamy jednak, że najlepsze wyniki były dla sprawdzania od największych do najmniejszych, zatem:

number = 711
iterations = 0
divs = list()
for i in range(1, round(number/2) + 1):
    iterations += 1
    if 711000000 % i == 0:
        divs.append(i)

for a in range(len(divs)-1, 0, -1):
    for b in range(a, 0, -1):
        for c in range (b, 0, -1):
            for d in range (c, 0, -1):
                iterations += 1
                if divs[a] + divs[b] + divs[c] + divs[d] == 711:
                    if divs[a] * divs[b] * divs[c] * divs[d] == 711000000:
                        print("Solved: ", divs[a], divs[b], divs[c], divs[d], iterations)
            exit()

Wynik znajdowany jest już po 30 tys. iteracji, w czasie poniżej 0,1s na zwykłym interpreterze Pythona. Co ciekawe, w tym wariancie PyPy jest nieco wolniejsze, z czasem nieco ponad 0,1s, zapewne większy narzut na uruchomienie interpretera.

711 wyrazów o optymalizacji

Tytuł jest przekorny – raczej nie będzie to 711 wyrazów, ale liczba na początku tytułu dobrze wpływa na klikalność, a 711 jak najbardziej jest na miejscu, za to będzie o optymalizacji.

Mianowicie pojawił się pod koniec zeszłego roku Sekurak Book Simple CTF, gdzie było pewne zadanie. Jeśli chcesz pobawić się w zrobienie tego CTF samodzielnie, choć jest już zakończony[1], to dobry moment na przerwanie lektury wpisu. Zadanie jest na tyle proste i znane, że publikacja rozwiązania nie spowoduje krzywdy, a przy tym podczas dyskusji z kolegą z pracy pojawiły się ciekawe zagadnienia natury optymalizacyjnej, więc postanowiłem opisać.

Zmodyfikowana treść zadania, z zachowaniem pierwotnego sensu, pojawiła się – z moją niewielką pomocą – jako wpis zagadka o siódmej jedenaście na zaprzyjaźnionym blogu. Jeśli ktoś nie chce się bawić w całą CTFową otoczkę, a ma ochotę wytężyć mózg, zapraszam tamże. Tym bardziej, że jest więcej zagadek.

Całość daje się sprowadzić do układu dwóch równań z czterema niewiadomymi:
a + b + c + d = 7,11
a * b * c * d = 7,11
WolframAlpha – być może niewprawnie użyty – protestował Standard computation time exceeded, ale przecież to się da policzyć… W końcu chodzi o wartości nieciągłe, bo ceny muszą być wielokrotnością jednego grosza. Wiemy zatem, że każda z wartości jest większa od zera, mniejsza od 711 i jest wielokrotnością 0,01. Tu pauza – komputery znacznie lepiej radzą sobie z wartościami całkowitymi[2], więc przemnóżmy wartości przez 100 i przejdźmy w tym momencie na resztę czasu równoważną formę:
a + b + c + d = 711
a * b * c * d = 711000000

Przy naiwnym brute force, korzystając jedynie z faktu, że wszystkie zmienne muszą być liczbami całkowitymi, bez żadnych optymalizacji, mamy do sprawdzenia maksymalnie 711^4 kombinacji, czyli 225 miliardów. Można to zapisać w Pythonie w postaci:

limit = 712
iterations = 0
for a in range(1, limit):
    print(a)
    for b in range(1, limit):
        for c in range(1, limit):
            for d in range(1, limit):
                iterations += 1
                if a + b + c + d == 711:
                    if a * b * c * d == 711000000:
                        print("Solved: ", a, b, c, d, iterations)
                        exit()

Jak widać zliczam też ilość iteracji, i dzięki temu wiem, że komputer musi – w różnych pętlach – policzyć w sumie prawie do 43 miliardów, zanim znajdzie rozwiązanie. Czas znalezienia rozwiązania litościwe pominę – spokojnie można wybrać się na spacer czy zakupy.

Mamy jednak dwie dodatkowe własności: iloczyn czterech liczb oraz ich sumę. Jeśli ktoś bawił się w sprawdzanie, czy dana liczba jest pierwsza, to pamięta zapewne, jeśli liczba jest złożona, to mniejszy dzielnik będzie co najwyżej równy pierwiastkowi danej liczby. W naszym przypadku iloczyn dwóch niewiadomych będzie co najwyżej równy pierwiastkowi z 711000000, czyli 26665. Z kolei któraś z niewiadomych będzie mniejsza od pierwiastka z 26665, czyli nieco ponad 163. Można więc ograniczyć jedną ze zmiennych – oczywiście tę najczęściej używaną – do 163.
Dodatkowo, niewiele myśląc, można skorzystać z własności sumowania i ograniczyć pozostałe zmienne do 711-163=548, czyli zmniejszyć przeszukiwaną przestrzeń do niecałych 27 miliardów. Oblekając to w skrypt:

limit = 549
iterations = 0
for a in range(1, limit):
    print(a)
    for b in range(1, limit):
        for c in range(1, limit):
            for d in range(1, 164):
                iterations += 1
                if a + b + c + d == 711:
                    if a * b * c * d == 711000000:
                        print("Solved: ", a, b, c, d, iterations)
                        exit()

Czas potrzebny na rozwiązanie nadal pomijam, ale jest to raczej tyle, ile potrzeba na zrobienie kawy czy herbaty, niż zakupów. Ilość iteracji potrzebnych do znalezienia rozwiązania to niecałe 6 mld.

Jeśli nie pamiętamy o ciekawej własności związanej z dzielnikami danej liczby, to nadal możemy zoptymalizować pętle tak, żeby automatycznie uwzględniać w nich warunek związany z sumą – daną zmienną zwiększamy do wartości zależnej od wartości pozostałych zmiennych, dzięki czemu można zaobserwować, że w miarę wzrostu wartości zmiennej a sprawdzenia pozostałych są coraz szybsze.

limit = 712
iterations = 0
for a in range(1, limit):
    print(a)
    for b in range(1, limit - a):
        for c in range(1, limit - a - b):
            for d in range(1, limit - a - b - c):
                iterations += 1
                if a + b + c + d == 711:
                    if a * b * c * d == 711000000:
                        print("Solved: ", a, b, c, d, iterations)
                        exit()

Rozwiązanie jest znajdowane nawet nieco szybciej, niż w poprzednim przypadku – ok. 5,5 mld iteracji.

Tu pojawił się pomysł – a co gdyby zapisać to w taki sposób, by program działał na początku bardzo szybko i zwalniał? Czyli nie zaczynamy od niskich wartości zmiennych i zwiększamy, tylko zaczynamy od wysokich i zmniejszamy? Można to zapisać następująco:

limit = 711
iterations = 0
for a in range(limit, 0, -1):
    print(a)
    for b in range(limit - a, 0, -1):
        for c in range(limit - a - b, 0, -1):
            for d in range(limit - a - b - c, 0, -1):
                iterations += 1
                if a + b + c + d == 711:
                    if a * b * c * d == 711000000:
                        print("Solved: ", a, b, c, d, iterations)
                        exit()

Przyznaję, że efekty tego podejścia mnie zaskoczyły. Tylko 1 mld operacji potrzebnych do znalezienia rozwiązania. Na moim sprzęcie skrypt wykonał się poniżej 3 minut na Pythonie 3.8.3. Co ciekawe dla Pythona 2.7.18 był to czas poniżej 2 minut, więc znacznie szybciej. Oba z paczek z repo Debiana unstable. Ale to raczej tylko ciekawostka, Python 2 jest martwy.

Dochodzimy jednak do sedna. Widać, że interpreter Pythona ma znaczenie. Jeśli zależy nam na szybkości, to warto zainteresować się alternatywną implementacją Pythona, czyli projektem PyPy. PyPy w wersji 7.3.1 (kompatybilna z Pythonem 3.6.9) wykonuje powyższy program w… nieco ponad 3 sekundy. Czyli jakieś pięćdziesiąt razy szybciej. Jeśli wrócimy do rozwiązania drugiego, to okaże się, że z użyciem PyPy można je znaleźć w 17 sekund. Natomiast wersja naiwna to… raptem 2 minuty.

Jak widać, szybkie narzędzia mogą prowadzić do lenistwa umysłowego – skoro działa szybko, to po co optymalizować? Optymalizacja algorytmu w powyższych przykładach nie jest skończona, da się lepiej. Znacznie lepiej. Na razie zostawię pole do popisu czytelnikom w komentarzach, za jakiś tydzień zaktualizuję wpis.

[1] Jest to naprawdę prosty CTF, jeśli ktoś nie miał okazji się bawić – polecam przymierzyć.
[2] Jak mawiają: real programmers use integers.

Ergonomia maca – system i software

W poprzednim wpisie o podobnym tytule było o hardware maców, pora zająć się softem i systemem. Będzie z ponad półrocznej perspektywy, ale muszę przyznać, że niewiele się od pierwszego wrażenia zmieniło. A nie ukrywam, że jestem rozczarowany. Mocno rozczarowany.

Pierwsze na co się naciąłem korzystając z macowego filesystemu to fakt, że jest prawie case sensitive. Prawie. Otóż lubię robić symlink download -> Download, bo wygodniej się wpisuje bez shift w konsoli. I co? I nie da się. Nawet nie ma sensownego komunikatu o błędzie.

Spotkałem się z opiniami, że maci są wygodne, dopracowane, a system jest przyjazny. Tymczasem konfiguracja wyglądu kuleje i nie daje miejsca na dostosowanie do indywidualnych preferencji, nawet w podstawowym zakresie.

Pasek z zegarkiem, statusem Wi-Fi, stanem baterii itp. musi być u góry. Nie da się przesunąć go na dół i sprawdzić, by zawsze był widoczny. Nie i już – musi być u góry, a o tym, że ktoś chciałby cały czas mieć dostęp do zegarka itp. również jakby nie pomyślano – o tym niżej.

Na dole jest z kolei dock, który IMO jest totalnie nieprzydatny i – jak pisała Yzoja – zachowuje się dziwnie i nielogicznie. Na szczęście da się skonfigurować jego ukrywanie, więc kontakt z nim ogranicza się w zasadzie do „skaczących” powiadomień. Może są one ładne, ale na dłuższą metę irytujące. Na szczęście nie wyskakują często.

Przyciski do maksymalizacji, minimalizacji okien są po lewej stronie belki okna i nie da się tego zmienić. Dodatkowo brakuje przycisku do maksymalizacji okna takiej, aby pasek statusu był widoczny. Da się to osiągnąć klikając na belce okna, ale przycisku nie ma, a domyślna maksymalizacja ukrywa pasek z zegarkiem i nie da się tego zmienić konfiguracją. Jak dla mnie poświęcenie kikunastu pikseli ekranu to niewielka cena za stały dostęp do skrótu uruchamiania programów, statusu programu i kilku innych informacji.

Systemowy zegarek. Gdy zobaczyłem, że kliknięcie w zegarek nie pokazuje kalendarza, zupełnie zwątpiłem w zdrowy rozsądek twórców systemu. Funkcja bardzo przydatna, obecna i w Linuksie, i w Windows, nawet nie sądziłem, że tak często z niej korzystam. Nie znalazłem fajnego darmowego rozwiązania, które naprawia ten problem, ale przyznaję, że szukałem pobieżnie – i tak mam cały czas w pracy otwarty Google Calendar.

Przełączanie między programami – pisałem, że mi się podoba, bo przełącza nie kolejno, tylko na ostatnio używane, co jest nawet fajne. Niestety, jeśli mamy uruchomione dwie instancje tego samego programu, np. Firefox z dwoma różnymi profilami, to przełączanie cmd-tab nie działa między nimi i trzeba korzystać z innego skrótu: cmd-`. Czyli najpierw cmd-tab, by wybrać Firefoksa, potem cmd-`, by wybrać instancję. Korzystam na szczęście rzadko, bo jest to wyjątkowo niewygodne. Oczywiście możliwości zmiany zachowania brak. Jak cmd-c i cmd-v działające między wszystkimi aplikacjami są plusem, tak ww. jest analogicznym minusem. IMO bardziej upierdliwym.

Aplikacje i ich instalacja. Yzoja nazwała instalator aplikacji żartem dla kilkulatków. Faktycznie jest fatalnie. Oczywista wada w stosunku do Linuksa to brak centralnego zarządzania pakietami – nie da się jednym poleceniem/kliknięciem zaktualizować wszystkich aplikacji – wymagana jest instalacja każdego programu z osobna, a aktualizacje żyją własnym życiem. Miało być łatwo (przeciągnięcie w celu instalacji), wyszło koślawo, bo każda aplikacja realizuje to trochę inaczej (np. pozwalając na różne wersje tej samej aplikacji lub nie), są też aplikacje instalowane zupełnie inaczej (brew, macports). Czyli nie jest to jeden sposób instalacji. Dodatkowo kwestię instalacji nowych wersji komplikuje wymuszanie zamknięcia działających instancji. A czasem po prostu wygodniej jest zainstalować nowszą wersję, dokończyć pracę w starej i dopiero wtedy zrobić restart.

Stabilność aplikacji pozostawia wiele do życzenia. LibreOffice Calc z arkuszem kalkulacyjnym, nawet pustym, wiesza mi się po kilkunastu sekundach. Na szczęście nie potrzebuję arkusza, tj. mam online. Może reinstalacja pomoże, tylko zastanawiam się, co poszło nie tak, bo wersję mam najnowszą…

Virtualbox nie jest szybki (w porównaniu z natywnym linuksowym KVM), ale to jakby niekrytyczne i jestem w stanie wybaczyć. Za to po wybudzeniu z uśpienia zużywał 100% CPU i powodował radosne wycie wiatraków, a tego wybaczyć nie mogę. Drążyłem temat, dobrzy ludzie z supportu desktopów w firmie podpowiedzieli polecenia diagnostyczne (konsola, dużo konsoli), w logach padały często odniesienia do dźwięku i faktycznie – po wyłączeniu dźwięku w wirtualkach jest spokój. Ponieważ moje VMki to raczej serwery, to brak dźwięku w nich mnie nie boli, ale wada pozostaje wadą.

Skoro przy dźwięku jesteśmy to kolejna sprawa. Korzystam ze stacji dokującej (Belkin), do której podłączone mam słuchawki. Po odłączeniu od stacji dźwięk przełącza się na wbudowane głośniki, po podpięciu z powrotem do stacji wraca na słuchawki. Prawie zawsze, bo czasem nie wróci i trzeba klikać w ustawieniach systemowych. Urządzenie jest widoczne, więc nie wiem o co mu chodzi.

Ja rozumiem, że I don’t like the bugs, but the bugs like me zobowiązuje, ale jak dla mnie macOS łączy najgorsze cechy Windowsa i Linuksa i zdecydowanie daleko mu do wygodnego, bezproblemowego systemu, działającego OOTB, jak niektórzy próbują go przedstawać. YMMV

Taskwarrior

Istnieją różne sposoby doprowadzania do realizacji celów/zadań i narzędzia wspomagające to zadanie. Nie fetyszyzuję realizacji zadań[1], nie przepadam za poradnikami i metodykami, chyba w życiu nie przeczytałem poradnika w formie książki. Bardziej jestem zwolennikiem zapoznania się jak ktoś coś robi i wyciągnięcia własnych wniosków, ew. przyjęcia fragmentów rozwiązań. Czyli luźna inspiracja.

Zdarzają się jednak sytuacje, kiedy na realizacji czegoś zależy mi trochę bardziej. Rzeczy ważne, ale nie pilne, albo zwyczajnie takie, które wylatują z głowy. Albo takie, które chciałbym zrealizować, jeśli akurat będę miał czas. Niezależnie od typu zadań i motywacji (prywatnie czy zawodowo), zawsze wysoko na liście narzędzi wspomagających była u mnie lista. Zwykła lista na kartce papieru, gdzie zadania są wykreślane/odptaszkowywane[2]. Bez większej filozofii, czy lista ma być numerowana, czy zadania sortowane wg priorytetu, a papier czysty czy w kratkę. Po prostu lista, idealnie jeśli mieści się na pojedynczej kartce – od razu widać co jest do zrobienia.

Taskwarrior lista zadań w wersji z priorytetami i datami; theme dark-16. Źródło: https://taskwarrior.org/docs/themes.html

Okazało się, że istnieje narzędzie open source Taskwarrior, które dobrze wpisuje się w takie podejście w wersji komputerowej. Działa także w konsoli. Opcji tak naprawdę jest znacznie więcej, jeśli ktoś potrzebuje, ale w najprostszej wersji jest to właśnie elektroniczny odpowiednik wykreślanej listy, równie prosty w użyciu. Taskwarrior nie jest związany z żadną konkretną metodyką realizacji projektów i… jeśli ktoś chce, to pozwala na trochę więcej, niż zwykła lista.

Z taskwarriora zacząłem korzystać ponad rok temu, używam go głównie do rzeczy nie posiadających własnej listy TODO[3], które łatwo zrealizować, jeśli się o nich akurat pamięta i ma chwilę czasu, a zapisanie zajmuje znacząco mniej czasu, niż realizacja. Ostatni warunek to mały prztyczek w nos dla różnych metod, które mają narzut porównywalny z zadaniami, których realizację „wspomagają”. Do narzutu zaliczam zapoznanie się z metodą i jej naukę. 😉

Z taskwarriora można korzystać w wersji standalone[4], można jednak mieć wspólne dane za pośrednictwem taskserver AKA taskd. Również jest open source, daje niezależność od zewnętrznych dostawców, więc chroni naszą prywatność – żadna duża firma nie ma wglądu, czym się zajmujemy i nie będzie nas profilować. Możemy uruchomić go na dowolnym serwerze w sieci, albo nawet z domu. Konfiguracja serwera jest nieco długa/skomplikowana, ale także tu pomaga dobra dokumentacja projektu. Istnieje appka dla Androida, ogłoszona w 2016.

Użycie taskwarriora w najprostszej postaci można sprowadzić do kilku oczywistych poleceń:

  • task add – dodanie zadania
  • task done – oznaczenie jako wykonane
  • task list – wyświetlenie listy zadań
  • task delete – usunięcie zadania z listy (bez realizacji)

Więcej ciekawych poleceń, zwł. dotyczących zestawień wykonanych zadań można obejrzeć na stronie projektu w dziale motywów kolorystycznych. Ładny dodatek, ale bez wpływu na podstawową funkcjonalność.

Jeśli ktoś nie zna, a lubi wspomagać się listą zadań, to polecam wypróbowanie tego oprogramowania. Bardzo niska bariera wejścia, open source. Jeśli ktoś potrzebuje więcej możliwości typu kolejność zadań, grupowanie w projekty, tagowanie, uwzględnienie dat czy zestawienia, to również są takie opcje.

[1] Wiadomo, że są zadania, które trzeba zrealizować, na dodatek w określonym terminie np. odnowienie ubezpieczenia samochodu. Część z nich jednak jest dyskusyjna, np. przetestowanie Cloudflare na blogu. Fajnie byłoby to zrobić, ale nie jest to niezbędne. Ogólnie wiele rzeczy nie jest niezbędne, czasem zwyczajnie warto odpuścić i poczytać książkę w tym czasie.
[2] Tak, dla ortodoksów to już pewnie dwa sposoby, tym bardziej, że wersję odptaszkowywaną stosuję w wersji z trzema wariantami: zrobione (v), częściowo/w trakcie (~) i nie wykonane/porzuć (-).
[3] Np. notatki dla bloga mają swoje TODO w postaci szkiców, więc na listę taskwarriora raczej nie trafią, chyba że jakieś wyjątkowo ważne, powiązane z zadaniem lub do zrobienia w określonym terminie.
[4] I właśnie z wersji standalone w konsoli korzystam. Konsolę mam zawsze pod ręką, jeśli jestem przy komputerze.

Advent of Code

Dowiedziałem się, że jest coś takiego jak Advent of Code. Czyli kalendarz adwentowy, tylko zamiast łakoci są zadania programistyczne do rozwiązania. Dwa dziennie, liczy się i fakt rozwiązania, i czas. Rozwiązywać można w dowolnym języku, weryfikacja rozwiązania jest przez podanie wyniku.

Podobno maja być z różnych dziedzin i o różnym poziomie trudności – dziś były bardzo proste. Zrobiłem w Pythonie, potem lepszą wersję, potem jedno w Perlu, jako krótki oneliner.

Jest rywalizacja globalna, ale można też tworzyć prywatne rywalizacje i porównywać się ze znajomymi. Ja bawię się z ludźmi z pracy, choć sporo z nich utrudniło sobie wyzwanie i poznaje przy okazji nowy język. Ale ja nie jestem programistą… 😉

Trochę skojarzenie z konkursami programistycznymi, którymi bawiłem się na studiach. Żeby nie było samych zalet – mimo, że każdy uczestnik ma inne dane wejściowe, to czas rozwiązania liczy się od publikacji zadania, które ma miejsce o północy w dziwnej strefie czasowej, co pewnie faworyzuje niektóre lokalizacje geograficzne. Ale nie ma to większego znaczenia w przypadku zabawy ze znajomymi.

Polecam zerknięcie – można sobie odświeżyć umiejętności programistyczne, poćwiczyć i przede wszystkim pobawić się.