Zoznam úloh

2. Rozťahaný sklad

Zadanie

Táto úloha je programátorská. Ako svoje riešenie odovzdaj program vo svojom obľúbenom jazyku a automaticky sa dozvieš, koľko si dostal(a) bodov. Ak si takýto typ úloh ešte nikdy neriešil(a), skús sa pozrieť ako by mal vyzerať ideálny program. Ak zatiaľ programovať nevieš, ale chcel(a) by si sa naučiť, môžeš skúsiť našu KSP School.

Keď už máme všetky balíky pozbierané na jednom mieste, môžeme ich doviezť do skladu. Ale kam ich uložíme? Sklad je veľmi veľký a police v ňom sú vysoké, takže vidíme vždy iba priamo vedľa seba a pred seba. Každý balík má svoje miesto, na ktoré sa perfektne hodí. Je to tak jednoznačné, že keď k tomu miestu prídeme, okamžite budeme vedieť, že sme na správnom mieste.

Úloha

Nájdite čo najkratšiu cestu z miesta, kde ste začali, do miesta, na ktoré sa váš balík perfektne hodí. Začínate na pozícii $(S_r, S_c)$, môžete sa otočiť doľava alebo doprava alebo urobiť krok dopredu. Vždy viete, čo je vľavo, pred vami a vpravo od vás.
Miesto na ktoré sa váš balík hodí je označené znakom X a jeho polohu dopredu neviete.

Sklad je obdĺžniková mriežka s $R$ riadkami a $C$ stĺpcami, kde každé políčko je buď prechodné (označené .), obsadené policou (označené #) alebo cieľ (označené X). Vašou úlohou je dostať sa na cieľové políčko.

Formát vstupu

Na začiatku dostanete na prvom riadku čísla $R$ a $C$ (počty riadkov a stĺpcov skladu). a v druhom riadku čísla $S_r$ a $S_c$ (pozícia štartu). Vždy začínate smerujúc na spodok.

Všetky ďalšie riadky obsahujú tri znaky oddelené medzerou, popisujúce aké políčko je vľavo, pred a vpravo od aktuálnej pozície (v tomto poradí). Políčka mimo skladu sú označované rovnako ako políčka obsadené policou. Tieto informácie dostanete na začiatku a vždy po vykonaní kroku alebo otočenia.

Pozor: po tom, čo stúpite na miesto X, už ďalší vstup nedostanete (lebo ste už hotoví). Ak sa pokúsite aj tak čítať vstup, tak vám program vyhodí chybu.

Sada 1 2 3 4
$1 \leq R, C \leq$ $20$ $20$ $200$ $200$
Celkový počet bodov $25$ $25$ $50$ $50$

V sadách 1 a 3 môžete navyše predpokladať, že voľné políčka v sklade netvoria žiadne cykly. V sadách 2 a 4 sa cykly môžu vyskytnúť.

Formát výstupu

Na výstup vypísujte akcie, ktoré chcete urobiť (L - otočiť sa doľava, P - otočiť sa doprava, K - krok dopredu). Každú akciu vypíšte na samostatný riadok. Po každej akcii dostanete nové informácie o vašom okolí.

Aby testovanie fungovalo ako má, je nutné po vypísaní každého riadku flushnúť výstup, aby ho testovač uvidel. V C++ by malo stačiť cout << nieco << endl;, ale môžete explicitne použiť príkaz cout.flush();. V Pythone vypisujte príkazom print(cislo, flush=True) alebo po vypísaní zavolajte sys.stdout.flush(). Pre iné jazyky hľadajte ekvivalent k príkazu flush.

Hodnotenie

Ak sa váš program pokúsi ísť mimo skladu alebo do políčka obsadeného policou, bude ukončený so stavom WA. Ak váš program odošle neplatnú inštrukciu, rovnako bude ukončeny s WA.

Váš program môže použiť najviac $10 \cdot F$ krokov (kde $F$ je počet voľných políčok v sklade), po viac krokoch bude testovanie ukončené s výsledkom WA. Na počte otočení nezáleží, avšak ak sa váš program bude dlhšie otáčať na jednom mieste, bude ukončený s WA.

Príklad

>>> 5 5
>>> 0 1
>>> . # .
<<< L
>>> # . #
<<< K
>>> # # .
<<< P
>>> # . .
<<< K
>>> . . #
<<< L
>>> . . .
<<< K
>>> # X #
<<< K

(>>> znamená, že ide o vstup vášho programu, <<< zas, že ide o jeho výstup. Tieto značky sú len ilustračné, na testovači sa nevyskytujú.)

V tomto prípade sa programu podarilo ísť priamo k cieľu bez zbytočných krokov. Sklad v tomto prípade vyzeral nasledovne:

. . . # .
# # . . X
. . . # #
. # # . .
. . . . #

Začnime tým, že si uvedomíme, čo je našou úlohou. Sklad nám predstavuje mriežkové bludisko, v ktorom je každé políčko buď priechodné (prázdne) alebo je stenou (je tam polica). Snažíme sa dostať na políčko X, pričom nevieme kde sa nachádza. Vždy vieme čo je pred nami, vpravo a vľavo.

Držanie sa steny

Jeden zo spôsobov, ktorým to môžeme spraviť je, že sa vždy budeme držať jednej zo stien. V tomto riešení si zvolíme, źe sa budeme držať pravej steny. To znamená, že ak bude dať, vždy pôjdeme doprava. Ak nie, pôjdeme dopredu a ak ani to nebude možné, pôjdeme doľava. Ak nevieme ísť ani do ľava, budeme vedieť, že sme sa dostali do slepej uličky a otočíme sa naspäť.

Toto riešenie by fungovalo v sadách 1 a 3, ale v sadoch 2 a 4 by sme sa narazili na problém tohto riešenia. V týchto sadách sa totiž môžu nachádzať cykly prázdnych políčok, v ktorých by sme sa zasekli. Napr. v takomto bludisku, by sme sa touto taktikou nikdy do cieľa nedostali:

Pamätanie si kde sme boli

Tento problém vieme vyriešiť jednoducho tak, že si budeme pamätať, kde sme už boli. Keď sa budeme rozhodovať, kadiaľ chceme ísť, budeme pri tom uvažovať aj o tom, či sme už niekde boli. Ak sme na danom políćku už raz boli, nechceme tam ísť znovu, lebo tým smerom sme už bludisko prehľadali a cieľ sme nenašli. Ak sa ocitneme na mieste, z ktorého sme už išli každým smerom, vrátime sa naspäť smerom, z ktorého sme prišli.

Vrátenie sa naspäť vieme docieliť viacerými spôsobomi, napr. týmito: - Pre každé políčko si budeme pamätať, odkiaľ (resp. z ktorého smeru) sme sa naň prvý krát dostali. - Pre kaďé políčko si budeme pamätať, koľké v poradí sme ho navštívili. Keď nebudeme mať do žiadnej strany voľný smer, pôjdeme na susediace políčko s čo najmenším poradovým číslom. - Pohyb budeme implementovať rekurzívne a pri skončení rekurzívneho volania sa vrátime naspäť.

V tomto riešení budeme implementovať prvú možnosť ale ľubovolná z týchto možností vie fungovať.

Implementácia

Implementáciu začneme globálnymi premennými, ktoré budeme potrebovať. Okrem nich si zadefinujeme aj konštantný zoznam smerov vo forme (dr, dc), kde dr je posun v riadku a dc je posun v stĺpci.

Odkiaľ sme prišli a kde sme už boli si môžeme pamätať v dvojrozmernom poli alebo pomocou štruktúr dict a set. Aby sme sa nemuseli trápiť s definíciami a indexovaním dvojrozmerného poľa, použijeme radšej dict a set.

SMERY = [
    (0, -1),  # dole
    (-1, 0),  # vlavo
    (0, 1),   # hore
    (1, 0)    # vpravo
]

R, C = list(map(int, input().split()))
r, c = list(map(int, input().split()))

odkial = dict()
navstivene = set()
smer = 0  # Zaciname smerujuc nadol

Teraz si môžeme zadefinovať pomocné funkcie, aby sme nemuseli zbytočne opakovať veľa kódu. Smery, ktoré sme definovali sú zoradené tak, že ďalší smer v poradí je vždy doprava a predchádzajúci je vždy doľava. Otáčanie si tým vieme zjednodušiť na pripočítavanie a odčítavanie 1 z aktuálneho smeru. Nesmieme ale zabudnúť na použite modula 4, aby náš smer vždy zostal v intervale 0-3.

def vlavo_s():
    return (r + SMERY[(smer+3)%4][0], c + SMERY[(smer+3)%4][1])

def vpred_s():
    return (r + SMERY[smer][0], c + SMERY[smer][1])

def vpravo_s():
    return (r + SMERY[(smer+1)%4][0], c + SMERY[(smer+1)%4][1])


def nacitaj():
    # trojica (vlavo, vpred, vpravo)
    return input().split()

def krok(kam):
    global r, c, smer
    print(kam, flush=True)
    if kam == 'L':
        smer = (smer+3)%4
    elif kam == 'R':
        smer = (smer+1)%4
    else:
        nr, nc = r + SMERY[smer][0], c + SMERY[smer][1]
        if (nr, nc) not in odkial:
            # odkial chceme nastavit iba pri prvom navstiveni
            odkial[(nr, nc)] = (r, c)
        r, c = nr, nc

Teraz sa dostávame k najdôležitejšej časti programu. Začnime tým, že urobíme prvú časť programu, ktorá sa bude snažiť držať pravej steny. Ak je políčko vpravo priechodné a ešte sme na ňom neboli, pôjdeme tam. Ak nie, pôjdeme dopredu (samozrejme, iba ak sme tam ešte neboli) alebo ak ani to nie je možné, pôjdeme doľava. Ak ani to nie je možné, vrátime sa naspäť odkiaľ sme prišli.

Celý program dáme do slučky, ktorá sa bude vykonávať, pokiaľ sa nedostaneme do cieľa.

while True:
    navstivene.add((r, c))
    vlavo, vpred, vpravo = nacitaj()

    if vpravo == '.' and (vpravo_s()[0], vpravo_s()[1]) not in navstivene:
        # pri otocke nas netrapi co je pri nas, cize odpoved nacitame ale zahodime
        krok('P'); nacitaj()
        krok('K')
    elif vpred == '.' and (vpred_s()[0], vpred_s()[1]) not in navstivene:
        krok('K')
    elif vlavo == '.' and (vlavo_s()[0], vlavo_s()[1]) not in navstivene:
        krok('L'); nacitaj()
        krok('K')
    else:
        naspat = odkial[(r, c)]
        dr, dc = naspat[0] - r, naspat[1] - c
        # zistime, ktorym smerom sa potrebujeme otocit, otocime sa a urobime krok
        potrebny_smer = SMERY.index((dr, dc))
        for i in range((smer - potrebny_smer)%4):
            krok('R'); nacitaj()
        krok('K')

V tomto kóde ešte nikde nekontrolujeme, či sa pri nás nachádza cieľ. Kontrolu na to umiestnime pred zvyšok rozhodovacieho kódu, keďže ak sme pri cieli chceme ísť priamo k nemu a netrápi nás zvyšok kódu. Keď sme sa tam už presunuli, môžeme ukončiť slučku.

while True:
    navstivene.add((r, c))
    vlavo, vpred, vpravo = nacitaj()

    if vpravo == 'X':
        krok('P'); nacitaj()
        krok('K')
        break
    elif vpred == 'X':
        krok('K')
        break
    elif vlavo == 'X':
        krok('L'); nacitaj()
        krok('K')
        break

    ...
Pre odovzdávanie sa musíš prihlásiť.