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

Była część pierwsza i część druga, pora na kolejną, niezupełnie planowaną. Jak pamiętamy w części drugiej udało się ograniczyć sprawdzane liczby do 49 sztuk. To, co chodziło mi od czasu do czasu po głowie to pytanie, czy da się rozwiązać tę zagadkę „na piechotę”, bez użycia komputera?

Rozejrzałem się za możliwymi uproszczeniami i zauważyłem kolejne potencjalne pole do optymalizacji, czyli zmniejszenia liczby potrzebnych obliczeń. Jak wiadomo, 711 jest liczbą nieparzystą. Aby suma dwóch liczb była nieparzysta, jedna z nich musi być parzysta, druga nieparzysta. Z kolei aby suma dwóch liczb była parzysta, albo obie muszą być parzyste, albo nieparzyste. Tu mamy do czynienia z sumą czterech liczb, więc są dwa przypadki. Albo jedna z liczb jest nieparzysta, a trzy są parzyste, albo odwrotnie.

Z naszych 49 liczb, 37 jest parzystych, a 12 nieparzystych. Jak to wpływa na przestrzeń rozwiązań? Z 49^4, czyli ok. 5,8 mln przechodzimy na 12*37^3 + 37*12^3 czyli ok. 672 tys. Nadal trochę dużo jak na ręczne liczenie, ale jak to wpłynie na czas obliczeń? Nasz skrypt będzie miał postać:

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

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

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

Niezależnie od kolejności bloków (najpierw 1 liczba parzysta i 3 nieparzyste, co jest teoretycznie korzystniejszym wariantem, czy odwrotnie), potrzebować będziemy poniżej 95 tys. iteracji. Czas to 0,08 sekundy dla zwykłego interpretera Pythona lub 0,12 sekundy dla Pypy.

Możliwa jest też wersja „w dół”:

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

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

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

Ilość potrzebnych iteracji waha się od 18 do 28 tys. w zależności od kolejności bloków. Natomiast czas wykonania to 0,06 sekundy dla zwykłego interpretera Pythona i 0,1 sekundy dla Pypy.

Nadal nie jest to optymalizacja powodująca, że da się policzyć „na piechotę”, ale… coraz bliżej.

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.