Alert RCB przed wyborami

Alert

W sobotę, dzień przed drugą turą wyborów prezydenckich w 2020 dostałem, podobnie jak miliony Polaków, takiego oto SMSa zawierającego alert RCB:

SMS alert RCB
Screenshot SMSa od ALERT RCB

Zdziwiłem się. Jakiż to alert i czemu jest wysyłany w trakcie trwania ciszy wyborczej? Dziwna sprawa. A może w ogóle jakiś fake i dezinformacja? W końcu pojawiały się już nieprawdziwe informacje, że jeśli ktoś głosował w pierwszej turze na danego kandydata, to w drugiej nie musi, jeśli chce głosować na tego samego. Sprawdziłem na stronie rcb.gov.pl i tam znalazłem taką informację:

Lipiec 11, 2020 Kategoria: KOMUNIKATY

W związku ze zmianą zasad obsługi wyborców przez obwodowe komisje wyborcze,  do obywateli został wysłany Alert RCB o treści:

II tura wyborów prezydenckich w niedzielę 12.07. Osoby 60+, kobiety w ciąży oraz osoby niepełnosprawne będą mogły głosować w komisjach wyborczych bez kolejki.

I dodatkowo obrazek:

Jest też wyjaśnienie, czemu został wysłany:

Państwowa Komisja Wyborcza zwróciła się z apelem do organów władzy i administracji publicznej o jak najszersze rozpowszechnienie informacji o nowych przepisach wśród wyborców.

Do czego służy RCB?

Ale czy w ogóle RCB służy do tego? Popatrzyłem w FAQ, a tam:

Komunikaty będą wydawane tylko w wyjątkowych sytuacjach, które realnie mogą zagrażać życiu i zdrowiu człowieka.

Wiadomości tekstowe (SMS) są wysyłane tylko w nadzwyczajnych sytuacjach, gdy zagrożone jest bezpośrednio życie i zdrowie.

Alert RCB powstaje na podstawie informacji o potencjalnych zagrożeniach otrzymywanych z ministerstw, służb np. policji, straży pożarnej, urzędów i instytucji centralnych np. IMGW oraz urzędów wojewódzkich.

Alert RCB dotyczy wszystkich zdarzeń bezpośrednio zagrażających zdrowiu i życiu. Jest rozsyłany tylko w sytuacjach nadzwyczajnych.

Nijak mi nie pasuje informacja o zmianie zasad uczestnictwa w wyborach do żadnego z powyższych. Nie ma przecież żadnego zagrożenia, sytuacja nie jest wyjątkowa – zwykłe wybory. Wygląda więc, że ktoś zrobił sobie z RCB tubę propagandową. Chyba, że FAQ jest wyjątkowo słabe.

Wtopy

Czy uważam, że sytuacja była niejednoznaczna etycznie i mogła wpłynąć na wynik wyborów? Oczywiście.

Po pierwsze, głosowanie poza kolejnością dotyczy tylko niektórych grup społecznych. W szczególności osób w wieku 60 i więcej lat, czyli grupy, w której partia rządząca ma największe poparcie. Przypadek? Nie sądzę.

Po drugie, wszystko wskazuje na to, że wykorzystano struktury państwowych niezgodnie z przeznaczeniem do wysyłania informacji. Ciekawe, ile kosztowała ta akcja?

Zmiana zasad tylko w II turze, wysłanie informacji w ostatniej chwili również nie budzą zaufania.

Informacja była niepełna, nie zawierała informacji o tym, że do głosowania poza kolejnością uprawnione są osoby z dziećmi do lat 3. W komunikacie nie było również informacji o tym, że przesyłana informacja jest częściowa i gdzie można się zapoznać z pełną wersją. Próba manipulacji czy zwykła niekompetencja? Podobno nie należy przypisywać złych intencji, jeśli zachowanie da się wytłumaczyć głupotą…

Podobno motywacją była pandemia, więc konia z rzędem temu, kto wytłumaczy mi w jaki sposób osoby z dzieckiem do lat 3, niepełnosprawni czy wymagający kształcenia specjalnego są bardziej podatni na zarażenie.

Jak się dokładniej zastanowić, to przepisy promowały wypożyczanie dzieci do lat trzech w celu uniknięcia stania w kolejce.

Niebezpieczeństwa uprzywilejowania

Ogólnie uważam, że faworyzowanie jakichkolwiek grup społecznych podczas wyborów jest niebezpieczne. W przypadku zorganizowanej akcji może powodować tworzenie np. sztucznych kolejek, które będą zniechęcały ludzi spoza uprzywilejowanych grup do głosowania.

Mechanizm nadużycia jest prosty: stoi kolejka, przychodzi kilkadziesiąt albo więcej osób w sposób oczywisty należących do uprzywilejowanej grupy, zajmują miejsca na początku kolejki. I tak ciągle. W sumie nie muszą nawet głosować – kto im zabroni stanąć i odejść, nie oddawszy głosu? Czy osoby nie będące w uprzywilejowanej grupie, czekające w kolejce mogą się zniechęcić w takiej sytuacji do oddania głosu? Ano mogą.

PS Wpis niezbyt na czasie, bo ostatnie dwa tygodnie spędziłem bez komputera praktycznie – z racji sytuacji urlop pod namiotem. Ale podobno lepiej późno, niż wcale.

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

Tym, co nie czytali polecam lekturę części pierwszej. Tymczasem pojawił się wpis z rozwiązaniem i pojawiło się tam znacznie lepsze podejście do tematu. Optymalizacja polega na tym, że 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 dalsza optymalizacja nie ma sensu, 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.

UPDATE Dostępna jest kolejna część traktująca o optymalizacji.

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ść. Poza tym, liczba 711 jak najbardziej jest na miejscu, a sam wpis 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ę jako wpis zagadka o siódmej jedenaście na zaprzyjaźnionym blogu. Z moją niewielką pomocą. 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, mamy do sprawdzenia maksymalnie 711^4 kombinacji. Czyli 225 miliardów. Jest to wersja wyjściowa, bez żadnych optymalizacji. 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, pierwsza optymalizacja wygląda następująco:

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 temu 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ł: 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.

UPDATE Dostępne są kolejne wpisy na ten temat.