Autorské riešenie
[stiahni riasy.py]

  • Počet riešiteľov: 8 / 9 = 89 %

  • Úspešnosť riešenia: 6,25 / 9 = 69 %

Skákanie po bielych pásoch na priechode pre chodcov alebo stúpanie len na čiary medzi dlaždicami sú zábavné činnosti, ktoré už vyskúšal asi každý z nás. Ak však máme takýto pohyb vopred naplánovať, navyše tak, aby bol najúspornejší, už to nevyzerá tak zábavne. Pozrime sa na to, čo plánovanie takéhoto pohybu po mriežkovom chodníku zahŕňa:

  1. ak vzdialenosť z aktuálneho bodu do cieľa je menšia ako dĺžka kroku, spravíme krok presne do cieľa a končíme, našli sme cestu
    ak nie, pokračujeme krokom 2,

  2. zistime na ktoré časti mriežky je možné z aktuálneho bodu vstúpiť,

    • každý z týchto bodov je z aktuálneho bodu vzdialený presne o dĺžku kroku,

    • keďže tieto body sú na mriežke, minimálne jedna z ich súradníc musí byť celé číslo,

    • dĺžka kroku je vždy menšia ako dĺžka strany boxu,

  3. z týchto bodov si vyberieme ten, ktorý nás najviac priblíži k cieľu,

  4. presunieme sa naň, označíme ho za aktuálny bod a pokračujeme bodom 1. 

Vďaka tomuto postupu sme veľký problém rozdelili na niekoľko menších. Poďme ich postupne riešiť.

Body mriežky, na ktoré je možné z aktuálneho bodu vstúpiť

Na obrázku nižšie je niekoľko prípadov, kde môže smerovať krok z aktuálneho bodu:

mozne kroky

Aj keď to vyzerá komplikovane, vieme to vyriešiť. Poznáme súradnice aktuálneho, červeného bodu. Jednu zo súradníc každého zeleného bodu vieme tiež. Buď je taká istá ako celočíselná súradnica červeného bodu, alebo je najbližšia celočíselná k neceločíselnej súradnici červeného bodu.

Inak povedané, máme dva body v rovine a ich vzdialenosť. Jediné či nepoznáme je jedna zložka jednej zo súradníc jedného bodu. Túto vieme nájsť pomocou Pytagorovej vety:

vypocet suradnic

Riešenie tejto časti môže vyzerať nasledovne:

def ries_kvad_rovnicu(a, b, c):
    d = b ** 2 - 4 * a * c
    if d < 0:
        return []
    if d == 0:
        return [-b / (2 * a)]
    return [(-b + d ** 0.5) / (2 * a), (-b - d ** 0.5) / (2 * a)]


def vypocitaj_suradnicu(x, y, nove_x, nove_y, dlzka_kroku):
    if nove_x is None:
        a = 1
        b = -2 * x
        c = x ** 2 + (y - nove_y) ** 2 - dlzka_kroku ** 2
        x_suradnice = ries_kvad_rovnicu(a, b, c)
        return [[nove_x, nove_y] for nove_x in x_suradnice]
    if nove_y is None:
        a = 1
        b = -2 * y
        c = y ** 2 + (x - nove_x) ** 2 - dlzka_kroku ** 2
        y_suradnice = ries_kvad_rovnicu(a, b, c)
        return [[nove_x, nove_y] for nove_y in y_suradnice]


def suradnice_po_kroku(aktualna_suradnica, dlzka_kroku):
    x = aktualna_suradnica[0]
    y = aktualna_suradnica[1]
    suradnice = []
    if x == int(x):  # x-ova je cele cislo
        suradnice.extend(vypocitaj_suradnicu(x, y, None, math.floor(y), dlzka_kroku))
        suradnice.extend(vypocitaj_suradnicu(x, y, x, None, dlzka_kroku))
        suradnice.extend(vypocitaj_suradnicu(x, y, None, math.floor(y) + 1, dlzka_kroku))

    if y == int(y):  # y-ova je cele cislo
        suradnice.extend(vypocitaj_suradnicu(x, y, math.floor(x), None, dlzka_kroku))
        suradnice.extend(vypocitaj_suradnicu(x, y, None, y, dlzka_kroku))
        suradnice.extend(vypocitaj_suradnicu(x, y, math.floor(x) + 1, None, dlzka_kroku))
    suradnice_bez_duplicit = []
    for suradnica in suradnice:
        if suradnica not in suradnice_bez_duplicit:
            suradnice_bez_duplicit.append(suradnica)
    return suradnice_bez_duplicit 

Funkcia suradnice_po_kroku zistí a vráti zoznam súradníc, kam je možné sa dostať z aktuálnej súradnice pri zadanej dĺžke kroku. Keďže niektoré zo súradníc by mohli byť duplicitné, z výsledného zoznamu ich odstránime.

Funkcia vypocitaj_suradnicu vypočíta a vráti súradnice nových bodov, kde jedna zložka z ich súradníc je neznáma.

Ak vyžijeme Pytagorovu vetu, dostaneme sa ku kvadratickej rovnici. Na jej riešenie sme si vytvorili funkciu ries_kvad_rovnicu.

Bod, ktorý nás najviac priblíži k cieľu 1

Jedna z možností ako vybrať ten "správny" bod, je jeho priama vzdialenosť od cieľa. Ak použijeme túto metriku, riešenie môže vyzerať nasledovne:

def vzdialenost1(bod1, bod2):
    return ((bod1[0] - bod2[0]) ** 2 + (bod1[1] - bod2[1]) ** 2) ** 0.5


def vrat_suradnice_pohybu1(start, ciel, dlzka_kroku):
    aktualna_suradnica = start
    suradnice_pohybu = [start]
    while vzdialenost1(aktualna_suradnica, ciel) > dlzka_kroku:
        potencialne_suradnice = suradnice_po_kroku(aktualna_suradnica, dlzka_kroku)
        najlepsia_suradnica = None
        najblizsie_k_cielu = float('inf')
        for potencialna_suradnica in potencialne_suradnice:
            if potencialna_suradnica not in suradnice_pohybu:
                if vzdialenost1(potencialna_suradnica, ciel) < najblizsie_k_cielu:
                    najlepsia_suradnica = potencialna_suradnica
                    najblizsie_k_cielu = vzdialenost1(potencialna_suradnica, ciel)
        suradnice_pohybu.append(najlepsia_suradnica)
        aktualna_suradnica = najlepsia_suradnica
    suradnice_pohybu.append(ciel)
    return suradnice_pohybu 

Všimnime si, že ako kritérium používame priamu vzdialenosť, ale body pohybu vyberáme tak, aby boli umiestnené na mriežke. Takto by s nám mohlo stať, že by sme uviazli v nekonečnej slučke medzi dvoma bodmi:

slucka

Ak začneme v červenom bode, najviac nás k cieľu (modrý bod) priblíži zelený bod. Ak sa presunieme do zeleného bodu, najviac nás k cieľu priblíži červený bod. Aby sme v takejto slučke neuviazli, uvažujeme len tie body, v ktorých sme ešte neboli. 

Bod, ktorý nás najviac priblíži k cieľu 2

Pre výber toho správneho bodu môžeme využiť aj inú metriku. Pozrime sa, ako nás nový bod priblíži k cieľu, ak by sme išli po čiarach mriežky.

Výpočet mriežkovej vzdialenosti bude o niečo náročnejší, pretože musíme uvažovať rôzne prípady:

mriezkove vzdialenosti

V tomto prípade by riešenie mohlo vyzerať takto: 

def vzdialenost2(bod1, bod2):
    x1, y1 = bod1
    x2, y2 = bod2
    if y1 == y2:
        dx = abs(x1 - x2)
    elif math.floor(x1) == math.floor(x2):
        dx = min(x1 % 1 + x2 % 1, 2 - (x1 % 1 + x2 % 1))
    else:
        dx = abs(x1 - x2)

    if x1 == x2:
        dy = abs(y1 - y2)
    elif math.floor(y1) == math.floor(y2):
        dy = min(y1 % 1 + y2 % 1, 2 - (y1 % 1 + y2 % 1))
    else:
        dy = abs(y1 - y2)
    return dx + dy


def vrat_suradnice_pohybu2(start, ciel, dlzka_kroku):
    aktualna_suradnica = start
    suradnice_pohybu = [start]
    while vzdialenost1(aktualna_suradnica, ciel) > dlzka_kroku:
        potencialne_suradnice = suradnice_po_kroku(aktualna_suradnica, dlzka_kroku)
        najlepsia_suradnica = None
        najblizsie_k_cielu = vzdialenost2(aktualna_suradnica, ciel)
        najblizsie_k_cielu = float('inf')
        for potencialna_suradnica in potencialne_suradnice:
            if vzdialenost2(potencialna_suradnica, ciel) < najblizsie_k_cielu:
                najlepsia_suradnica = potencialna_suradnica
                najblizsie_k_cielu = vzdialenost2(potencialna_suradnica, ciel)
        suradnice_pohybu.append(najlepsia_suradnica)
        aktualna_suradnica = najlepsia_suradnica
    suradnice_pohybu.append(ciel)
    return suradnice_pohybu

Otázka je, ktorý z týchto prístupov je lepší? Odpoveď nie je jednoduchá a v podstate neexistuje. V rôznych situáciách môžu byť tieto prístupy rôzne úspešné. Pozrime sa napríklad na nasledujúce dve situácie. Červenou farbou je riešenie prvým prístupom, modrou farbou riešenie druhým prístupom.

prvy lepsi druhy lepsi
štart: [0, 0]
cieľ: [6.8, 3]
dĺžka kroku: 0.9
počet krokov: 10
počet krokov: 11
štart: [0, 0]
cieľ: [7.1, 3]
dĺžka kroku: 0.6
počet krokov: 16
počet krokov: 15

Natíska sa otázka, či existuje spôsob, ktorý by našiel prechod na najmenší možný počet krokov bez ohľadu na situáciu. Ukážme si jeden z nich.

V situáciách, kde potrebujeme nájsť najlepšie riešenie a nemáme dostatok vedomostí na to, aby sme ho priamo našli, môžeme využiť iný prístup. Nájdime všetky prípustné riešenia a vyberme z nich to najlepšie. Prípustné v tomto prípade znamená, že neuvažujeme riešenia, kde by sme sa od cieľa vzďaľovali (podľa akej metriky?), kde by sme opakovane navštevovali rovnaké body a pod.

Z každého aktuálne bodu teda nevykročíme len jedným smerom, ale postupne vykročíme všetkými relevantnými smermi. Keď sa nám podarí dostať do cieľa tak skontrolujeme, či táto práve zavŕšená cesta nie je tou najlepšou doposiaľ nájdenou. Ak áno, zapamätáme si ju.

Riešenie týmto prístupom môže vyzerať napr. takto: 

def najdi_najlepsiu_cestu(start, ciel, dlzka_kroku, 
                          suradnice_pohybu=[], naj_suradnice_pohybu=[]):
    suradnice_pohybu.append(start)
    if start == ciel:
        if naj_suradnice_pohybu == [] or \
           len(suradnice_pohybu) < len(naj_suradnice_pohybu):
            naj_suradnice_pohybu.clear()
            naj_suradnice_pohybu.extend(suradnice_pohybu)
    elif vzdialenost1(start, ciel) <= dlzka_kroku:
        najdi_najlepsiu_cestu(ciel, ciel, dlzka_kroku, 
                              suradnice_pohybu, naj_suradnice_pohybu)
    else:
        suradnice_kroku = suradnice_po_kroku(start, dlzka_kroku)
        for suradnica in suradnice_kroku:
            if vzdialenost2(suradnica, ciel) < vzdialenost2(start, ciel):
                najdi_najlepsiu_cestu(suradnica, ciel, dlzka_kroku, 
                                      suradnice_pohybu, naj_suradnice_pohybu)
    suradnice_pohybu.pop()


def vrat_suradnice_pohybu3(start, ciel, dlzka_kroku):
    suradnice_pohybu = []
    naj_suradnice_pohybu = []
    najdi_najlepsiu_cestu(start, ciel, dlzka_kroku, 
                          suradnice_pohybu, naj_suradnice_pohybu)
    return naj_suradnice_pohybu

Funkcia vrat_suradnice_pohybu3 ku svojim trom parametrom vytvorí ešte dve premenné. V zozname suradnice_pohybu si budeme zaznamenávať súradnice cesty, ktorú práve skúmame a v zozname naj_suradnice_pohybu  si budeme uchovávať najlepšiu z ciest, ktoré sme doposiaľ preskúmali. Funkcia najdi_najlepsiu_cestu sa postará o systematické prehľadanie všetkých ciest.

Všimnime si ešte, ako sme vyriešili požiadavku na prípustné riešenia. Z možných krokov preskúmame len tie, ktoré znížia mriežkovú vzdialenosť k cieľu. Vieme totiž, že ak by sme uvažovali priamu vzdialenosť, niekedy by krok, ktorý by nás priblížil k cieľu, nebol možný.

Porovnajme výsledky, ktoré nájde takéto riešenie (zelená farba):

treti najlepsi 1 treti najlepsi 2
štart: [0, 0]
cieľ: [6.8, 3]
dĺžka kroku: 0.9
počet krokov: 10
počet krokov: 11
počet krokov: 10
štart: [0, 0]
cieľ: [7.1, 3]
dĺžka kroku: 0.6
počet krokov: 16
počet krokov: 15
počet krokov: 15

Aj keď posledný prístup nájde najlepšie riešenie, netešme sa predčasne. Preskúmať všetky cesty je pomerne náročné. Ak by sme z každého bodu uvažovali len dve možnosti (v skutočnosti ich môže byť aj viac) ako vykročiť, pri dĺžke cesty 10 krokov by to bolo 1024 možných ciest. Pri dĺžke cesty 20 krokov je to už 1048576 možností. Inak povedané, pri väčších vzdialenostiach by sme sa výsledku nemuseli prakticky ani dočkať. Prakticky je tak lepšie rýchlo dostupné, aj keď nie najlepšie riešenie, než najlepšie riešenie, ktoré je časovo nedostupné.

Aby sme nekončili takto pesimisticky. Existujú prístupy, ktoré nájdu najlepšie riešenie a navyše v dostupnom čase. Záujemcom odporúčame pozrieť sa napr. na dynamické programovanie.

Vaše zaujímavé riešenia a najčastejšie chyby

Toto je optimalizačná úloha, v ktorej bolo potrebné najskôr identifikovať možné kroky a potom optimalizovať, ktoré z nich použiť. S týmto si poradila, alebo takmer poradila polovica z vás.

Niektorí z vás uvažovali len konkrétny smer pohybu alebo konkrétnu súradnicu štartu.