První série třináctého ročníku KSP
- Termín série: 31. října 2000
- Odevzdávání: elektronicky přes Odevzdávátko
- Jak řešit: viz Návod na teoretické úlohy a Návod na praktické úlohy
- Dotazy ohledně zadání: posílejte na ksp@mff.cuni.cz, nebo se ptejte na Discordu KSP.
Zadání úloh
13-1-1 Logické výrazy (10 bodů)
Firma Paleologic se rozhodla uvést na trh svůj nový kalkulátor pracující mimo jiné i s logickými výrazy. Protože doba uvedení na trh se blíží a modul pracující s výrazy dosud nebyl vytvořen, požádali vás vývojáři této renomované společnosti o pomoc.
Na vstupu váš program dostane logický výraz a nemá udělat nic složitějšího, než
na výstup vypsat jeho výsledek. Zadaný logický výraz (pro jednoduchost
předpokládejte, že je korektní) se skládá z čísel
ve dvojkové soustavě, operátorů (!
, &
, |
, ^
) a závorek ((
a )
).
Operátor !
je unární a má nejvyšší prioritu ze všech operátorů. Ostatní
operátory jsou binární, přičemž &
má vyšší prioritu než |
a ten má
vyšší prioritu než ^
.
Jednotlivé operátory jsou definované pro dvojice bitů resp. pro jeden bit
v případě !
(na definiční tabulky jednotlivých operátorů se můžete podívat
na konci tohoto zadání). Na čísla se pak aplikují po bitech. Tedy např.
1100 & 0101
je 0100
.
Příklad: 101
|
0010&!(111^011) = 111
Tabulky operátorů:
A | B | & | | | ^ |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
A | ! |
0 | 1 |
1 | 0 |
13-1-2 Centrum (10 bodů)
V Railetánii se rozhodli vybudovat nové ředitelství drah. Aby úředníci nemuseli jezdit moc daleko, je třeba ho vybudovat někde „uprostřed“ železniční sítě. Takové místo je ovšem těžké najít. Situaci naštěstí zjednodušuje fakt, že v Railetánii mezi každými dvěma zastávkami vede právě jedna cesta po kolejích.
Váš program dostane na vstupu počet zastávek N. Zastávky si budeme číslovat od jedné do N. Dále dostane seznam tratí mezi zastávkami. Každá trať je charakterizována čísly dvou zastávek, které propojuje. Předpokládáme, že trati se mezi zastávkami nekříží. Jako vzdálenost dvou zastávek pak vezmeme počet tratí, po kterých je nutné projet, abychom se dostali z jedné zastávky do druhé. Vaším úkolem je nalézt takovou zastávku, že k ní nejvzdálenější zastávka je co nejblíže (tedy má minimální maximum ze vzdáleností z ní do ostatních zastávek). Pokud je takových zastávek více, stačí vrátit libovolnou z nich.
13-1-3 Stabilní úsek (11 bodů)
Strýček Skrblík se jednoho dne rozhodl, že budova plná mincí je již přeci jen trochu staromódní způsob uchovávání majetku, a že své peníze tedy investuje do akcií. Nechce ovšem pochopitelně o žádné peníze přijít, a tak potřebuje najít akcie, které jsou dostatečně důvěryhodné. Nakonec se rozhodl, že důvěryhodnost akcií bude posuzovat podle stability jejich ceny. Čím déle má (nebo měla) nějaká akcie stabilní cenu, tím je důvěryhodnější. Nyní, když už pan Skrblík oddřel všechno myšlení, obrátil se na vás (protože s vámi má již dobré zkušenosti z minulého ročníku) s takovým triviálním problémem. Pro každou akcii spočítat, jak nejdéle byla stabilní.
Na vstupu váš program dostane dvě čísla D a N a pak posloupnost celých čísel a1, a2… aN. Jeho úkolem je nalézt nejdelší úsek v posloupnosti takový, že žádné dva prvky v něm se neliší o více jak D (formálně tedy hledáme taková i, j, 1≤ i≤ j≤ N, že pro každé k, l∈{i… j} platí -D≤ ak-al≤ D a j-i je maximální možné).
13-1-4 Stoky (10 bodů)
V jednom městě, které raději nebudeme jmenovat, měli složitý systém odvádění odpadních vod. Po letech přestaveb nakonec vedlo potrubí z každého sběrného místa do každého. Za ta léta ovšem dělníci také zapomněli, ve kterém sběrném místě je vlastně vývod z celého kanalizačního systému do blízké čističky odpadních vod, a nyní by to potřebovali zjistit. Vaším úkolem je napsat program, který jim ono místo pomůže nalézt.
Program na vstupu dostane počet sběrných míst N. Může pak klást dělníkům dotazy, zda teče voda mezi dvěma sběrnými místy daným směrem. Výtok ze systému je v takovém sběrném místě, že ze všech ostatních míst teče voda do něj. Protože počítač správy kanalizací má dosti omezenou paměť a počet sběrných míst je velký, není možné si pamatovat nějakou informaci pro každé sběrné místo (odborně řečeno: vaše programy by měli mít lepší než lineární paměťovou složitost).
Příklad:
N = 5
Dotazy:
2->5: Ne
1->5: Ne
2->4: Ano
1->3: Ano
2->3: Ano
4->3: Ano
5->3: Ano
Odtok je v místě 3.
13-1-5 LISP (10 bodů)
V tomto ročníku jsme se rozhodli ukázat vám trochu netradiční přístup k programování, kterému se říká programování funkcionální (mějte s námi chvilku strpení, za chvíli dospějeme i k tomu, proč má takový podivný název), a tak v každé letošní sérii bude jedna úloha věnována jazyku Lisp.
Lisp je jazykem mnoha dialektů – existuje totiž ohromné množství různých interpreterů a aplikačních programů, které dávají uživateli možnost si dodefinovat své vlastní příkazy právě v Lispu (například známý Unixový textový editor Emacs), a jak už to tak bývá, každý, kdo si programoval svůj interpretr, se rozhodl, že si do jazyka přidá to, co mu tam chybí a že upraví věci, o kterých si myslí, že by se daly udělat lépe. Naštěstí všechny tyto dialekty mají mnoho společného, a tak si tu nadefinujeme náš vlastní velice jednoduchý dialekt, který bude obsahovat více méně jenom ono společné „jádro jazyka“, a nazveme ho Ksp-Lisp. Od září si budete moci z našeho archívu na Internetu stáhnout kompletní interpreter Ksp-Lispu (viz URL KSPáckých stránek v úvodu).
Veškerá data jsou v Ksp-Lispu uložena jako objekty. Každý objekt má svůj typ a svoji hodnotu (což může být i odkaz na nějaký jiný objekt, jak uvidíme za chvíli). Typů existuje jen několik málo:
- nil – od tohoto typu existuje jen jediný objekt, a to
objekt nazývaný
nil
. - integer – pro každé celé číslo existuje právě jeden objekt typu integer, který jej reprezentuje. Tyto objekty je zvykem označovat příslušnými přirozenými čísly.
- symbol – symboly jsou vlastně pojmenované odkazy. Každému
jménu (neprázdná posloupnost libovolných znaků, která neobsahuje
znaky `
(
', `)
', `"
', `.
' ani mezeru a neskládá se pouze z číslic) je přiřazen právě jeden objekt typu symbol, který obsahuje odkaz na jeden libovolný objekt (nebo speciální hodnotu undefined, pokud mu ještě nebyl žádný odkaz přiřazen). `+
', `+1
', `define
' či `nil
' jsou správně utvořená jména symbolů, symbol `nil
' standardně ukazuje na objekt nil. - pár – každý objekt typu pár obsahuje právě dvě položky,
z nichž každá je odkazem na libovolný objekt. První položka se
nazývá
a
, druháb
(v některých dialektechcar
acdr
). - primitivum – to je zabudovaná funkce jazyka, kterou umí interpreter spočítat sám od sebe.
Symboly se často používají jako proměnné: místo toho, abyste si do proměnné (tedy vlastně pojmenovaného místa v paměti) uložili nějakou hodnotu, v Lispu necháte nějaký symbol ukazovat na vaši hodnotu (jinými slovy nepojmenováváte místa v paměti, ale samotné hodnoty; díky tomu také nejsou přiřazeny typy proměnným, nýbrž objektům).
Páry a nil
se používají k tvoření seznamů. Prázdný seznam
se vždy vyjadřuje objektem nil
, neprázdný seznam pak párem, jehož
a
ukazuje na první prvek seznamu a b
na zbytek seznamu. Seznamy
se obvykle zapisují jako posloupnosti objektů uzavřené v závorkách,
přičemž jednotlivé objekty jsou odděleny libovolnou posloupností
mezer, tabulátorů a konců řádků – tak například (+ 1 (2))
je tříprvkový seznam, jehož prvním prvkem je symbol +
, druhým
číslo 1
a třetím seznam obsahující jako svůj jediný prvek číslo 2
.
Tato struktura bude reprezentována párem, jehož a
bude ukazovat
na symbol +
a b
na druhý pár, jeho a
bude ukazovat na integerový
objekt 1
, b
na třetí pár, který bude mít v a
uložen odkaz na
čtvrtý pár (a
ukazuje na integer 2
, b
na nil
) a v b
odkaz
na nil
. ()
je jen jiný název pro nil
.
Mimo `klasických' seznamů můžete vytvářet i složitější struktury
– „nekonečné“ cyklické seznamy (stačí, aby b
posledního prvku ukazovalo na některý z prvků předchozích) nebo
třeba binární stromy (listy reprezentujeme čísly nebo symboly,
vnitřní vrcholy pak páry, jejichž a
se bude odkazovat na levý
podstrom a b
na podstrom pravý).
Důležitým rozdílem oproti klasickým procedurálním jazykům je, že v Lispu nikdy přímo nealokujete paměť (a tím pádem ani neuvolňujete) – jakmile jakýkoliv objekt vznikne, interpreter pro něj sám nějaký kousek paměti přidělí a když už objekt nebude přístupný (to znamená, že se k němu nepůjde nijak dostat pomocí symbolů a odkazů mezi objekty), automaticky paměť uvolní k dalšímu použití (tomuto procesu se říká garbage collection [sbírání smetí]).
A jak se v Lispu programuje? Inu, je to funkcionální jazyk, takže
se všechny programy skládají z funkcí. Každá funkce dostává své
parametry (což jsou vlastně jen odkazy na objekty), vrací nějakou hodnotu
(opět odkaz) a případně způsobí nějaký postranní efekt (side-effect), tedy
založí, zruší čí změní nějaké globálně viditelné objekty nebo třeba
něco vypíše do výstupu. Každé volání funkce se zapíše jako seznam
typu (f 1 2 3)
, tedy prvním prvkem je funkce, která se má zavolat
(obvykle symbol ukazující na nějaký seznam, jenž je definicí funkce)
a všechny ostatní prvky se vyhodnotí jako parametry této funkce (mohou
to být třeba opět volání funkcí popsaná seznamy).
Každý interpreter Lispu má dva módy – mód
interaktivní (v tom přímo zadáváte funkce a on je vyhodnocuje a obratem
vypisuje výsledky; velice šikovné pro ladění programů) a mód dávkový
(tomu předáte nějaký soubor a on jej zpracuje, jako by byl řádek po řádku
zadán interaktivně; tak se spouští již hotové programy). Nastartujete-li
interpreter Ksp-Lispu, objeví se vám prompt oznamující, že se s vámi
komunikuje interaktivně. Když pak zadáte (+ 1 2)
, Ksp-Lisp to pochopí
jako volání funkce +
s parametry 1
a 2
a jelikož funkce +
, jak
již název napovídá, slouží ke sčítání celých čísel, vypíše vám obratem
výsledek 3
. (Přesněji: nejprve se vyhodnotil symbol +
a zjistilo
se, že ukazuje na funkci, té se předaly odkazy na objekty 1
a 2
,
funkce jako výsledek vrátila odkaz na objekt 3
, který byl vzápětí
vypsán.) Seznam (* (+ 1 2) (+ 3 4))
by způsobil zavolání již několika funkcí
a výsledkem, jak asi čekáte, by bylo číslo 21
.
A takhle vypadají v Lispu všechny programy, i podmínky jsou totiž
funkce (lépe řečeno speciální formy, jak uvidíme za chvilku, protože jejich
argumenty se samočinně nevyhodnocují před tím, než jsou předány) –
(if p q r)
způsobí nejprve vyhodnocení podmínky p a poté, pokud
je splněna (to znamená vyšlo-li něco jiného než nil
), vyhodnotí se q,
jinak r a vrátí se jeho hodnota.
Místo cyklů se obvykle používají rekurzivně se volající funkce.
A teď již konečně prozradíme přesná pravidla pro vyhodnocování výrazů, tedy vlastně pro provádění programů. Vyhodnocení je proces, jehož vstupem je nějaký objekt x a výstupem nějaký jiný objekt y, který nazveme hodnotou objektu původního. Vyhodnocování probíhá takto:
- Je-li x
nil
nebo integer, je výsledkem tentýž objekt. - Je-li x symbol a je-li mu přiřazen odkaz na nějaký objekt, je výsledkem tento objekt. Není-li mu přiřazeno nic, dojde k běhové chybě.
- Pokud je x pár, interpretujeme jej jako seznam kódující volání funkce.
Nejprve (rekurzivně) zavoláme vyhodnocení na jeho první prvek, což
nám má dát definici funkce, již voláme (taková definice je seznam, který
začíná buďto symbolem
lambda
pro normální funkci nebospecial
pro speciální formu, jeho druhý prvek je seznam symbolů a, jimž se mají přiřazovat parametry a zbytek seznamu v je samotný vnitřek funkce). Nyní nejedná-li se o speciální formu, rekurzivním voláním této vyhodnocovací procedury vyhodnotíme všechny zbylé prvky seznamu x, uložíme si na zásobník, kam se odkazují symboly zmíněné na seznamu a a necháme je ukazovat na vyhodnocené argumenty, načež opět rekurzivním zavoláním vyhodnocujeme jednotlivé objekty seznamu v. Až toto vyhodnocování skončí, obnovíme uložené obsahy symbolů, v nichž jsme předávali parametry a jako výsledek vrátíme výsledek posledního objektu ze seznamu v. Šlo-li by o speciální formu, zachovali bychom se stejně, pouze bychom parametry předali nevyhodnocené. Pokud není první prvek x pár, jeho složka není požadovaný symbol a nebo nesouhlasí počet argumentů uvedených v a s tím, jaké byly skutečně předány v x, vyhlásíme běhovou chybu. - Pakliže x je primitivum, chováme se k němu stejně jako v předchozím případě, pouze místo rekurzivního vyhodnocování podle definice použijeme speciální znalosti o tomto primitivu, které nám umožní jej vyhodnotit „mimo lispovský svět“.
Vyhodnocování si ukážeme na jednoduchém příkladu: (max 0 (+ 1 2))
zpracujeme
tak, že nejprve zjistíme, že max
je symbol, který ukazuje na definici
funkce max, poté vyhodnotíme rekurzivně její argumenty: 0
se vyhodnotí
na sebe samu, druhý parametr pak jako volání primitiva pojmenovaného
symbolem +
s parametry 1
a 2
. Nakonec zavoláme funkci max
,
předáme jí výsledky (objekty 0
a 3
) a výsledek posledního objektu
v její definici vrátíme jako výsledek našeho vyhodnocování.
Mimo to ještě existují funkce s proměnným počtem argumentů – v jejich
definici je místo úplného seznamu parametrů jen seznam končící symbolem
&rest
následovaným ještě jedním symbolem, kterému se přiřadí všechny
zbývající argumenty jako seznam.
Následuje úplný seznam Ksp-Lispových primitiv a jejich parametrů.
(Uvědomte si ovšem, že primitiva samotná jsou objekty, takže i když
to není dobrým zvykem, si je můžete přejmenovat, jak chcete.
Pod zde uvedenými jmény jsou dostupná při startu interpreteru.)
Latinkou
budeme
označovat jména primitiv a argumenty speciálních forem, které se
samočinně nevyhodnocují, kurzívou pak argumenty funkcí, `…'
značí, že funkce může mít argumentů libovolně mnoho.
(+ x …)
vrátí součet zadaných přirozených čísel. Analogicky -
(s jedním parametrem se chová jako unární minus, jinak od prvního čísla odečte druhé,
od výsledku třetí atd.), *
a /
.
(= x y)
porovná dvě čísla, vrátí t
pokud jsou stejná, nil
pokud nejsou.
Analogicky <
, >
, <=
, >=
a <>
.
(eq x y)
vrátí symbol t
, pokud jak x, tak y ukazují na tentýž
objekt, jinak nil
. Pozor, to, že dva objekty stejně vypadají (například
jsou-li to dva seznamy se stejným obsahem), ještě nemusí znamenat, že
jsou opravdu stejné. Každému číslu odpovídá vždy právě jeden objekt, takže
se pro čísla eq
chová přesně jako =
.
(not x)
vrátí t
pokud x je nil
, jinak nil
.
(block x …)
vrátí poslední ze svých argumentů
(cons x y)
vrátí odkaz na nově vyrobený pár, jehož první složkou bude x a druhou y
(list x …)
vrátí odkaz na nově vyrobený seznam obsahující zadané prvky
(geta x)
vrátí složku a
páru x, analogicky getb
(seta x y)
nastaví složku a
páru x na y, analogicky setb
(get s)
vrátí, na co ukazuje symbol s. Pokud na nic neukazoval,
dojde k běhové chybě.
(defined? s)
vrátí t
, pokud symbol s na něco ukazuje, jinak nil
(set s x)
nastaví symbol s, aby ukazoval na objekt x
(number? x)
vrátí t
, pokud x je číslo, jinak nil
. Obdobné
funkce existují i pro ostatní typy: pair?
, symbol?
, nil?
a primitive?
(eval x)
seznam x vyhodnotí podle popsaných vyhodnocovacích pravidel.
Umožňuje za běhu programu vytvořit funkci a pak ji provést.
(if c
je speciální forma sloužící k větvení výpočtu.
Nejprve vyhodnotí c, pokud vyjde t f
) nil
, vyhodnotí f
a vrátí jeho
výsledek, jinak vyhodnotí t
a vrátí jeho výsledek.
(and
je speciální forma sloužící jako booleovské and. Vyhodnocuje
se tak, že se nejprve vyhodnotí první argument, je-li x…
) nil
, skončí vyhodnocování
celého and
u s výsledkem nil
, jinak se pokračuje dalším argumentem ve stejném
duchu. Pokud ani poslední argument není nil
, vrátí se jeho hodnota jako výsledek.
(or
je obdobná speciální forma, tentokráte fungující jako logický
součet. Opět se postupně vyhodnocují argumenty a vrací se první výsledek, který
není x…
) nil
, pokud žádný takový není, vrátí se nil
.
(quote
je speciální forma, která vrátí svůj první argument. Často
se používá k zamezení vyhodnocení nějakého argumentu (uvědomte si, že argumenty
speciálních forem se nevyhodnocují, takže uvedete-li před nějaký argument funkce
volání x
) quote
, jediným důsledkem je, že argument „propadne“ skrz quote
nezměněný a nevyhodnocený. Například výsledkem (quote (1 2))
je seznam (1 2)
.
Mezi list
a quote
je jeden podstatný rozdíl: list
vám dá pokaždé nový
seznam, kdežto quote
vrací odkaz na stále tentýž. Výrazy typu (quote (...))
lze rovněž zkracovat jako '(...)
.
(define
je zkratka, která ušetří spoustu práce při jinak zdlouhavém
definování funkcí. Nadefinuje funkci (f a…) y…
) f
s parametry a…
a tělem y
,
jinými slovy sestrojí seznam (lambda (a…) y…)
a přiřadí symbolu
f
odkaz na tento seznam. Pokud použijete define
se symbolem místo seznamu
jako druhým parametrem, výsledkem bude pouze nastavení tohoto symbolu, aby ukazoval
na objekt y
(jinými slovy define
pak bude fungovat stejně jako set
, pouze
bude své parametry automaticky quotovat).
(let
je speciální forma, která
nejprve uloží obsah symbolů ((l1 v1) (l2 v2)…) x…
) l1
, l2
atd., načež začne vyhodnocovat výrazy
v1
, v2
atd. a jejich výsledky přiřazovat těmto symbolům. Poté vyhodnotí
posloupnost výrazů x…
a nakonec obnoví původní význam symbolů
a vrátí jako výsledek hodnotu posledního z uvedených výrazů. Tato lokální
definice symbolů se často používá podobně jako lokální proměnné v procedurálních
jazycích, pouze je nutné si dát pozor na to, že pomocí let
symbolům přiřazené
objekty jsou viditelné i ve funkcích zevnitř let
zavolaných.
A nyní si na jednoduchém příkladu ukážeme, jak v Ksp-Lispu něco jednoduchého
naprogramovat – bude to funkce length
, jejíž hodnotou bude délka zadaného seznamu:
(define (length x)
(if x
(+ 1 (length (getb x)))
0)
)
Délku počítáme rekurzivně: nejprve otestujeme, zda je seznam neprázdný (podmínka
u if
je nesplněna jen tehdy, je-li x
objekt nil
), pokud ano, vrátíme o jedničku
zvětšenou délku jeho zbytku (bez prvního prvku), jinak vrátíme nulu jakožto délku
prázdného seznamu.
Naším druhým příkladem bude funkce reverse
, která pro zadaný seznam vrátí jiný
seznam, jenž bude obsahovat tytéž prvky v opačném pořadí (bylo by také možné použitím
seta
a setb
změnit pořadí prvků v seznamu původním, ale to my nechceme):
(define (rev2 x y)
(if x
(rev2 (getb x)
(cons (geta x) y))
y)
)
(define (reverse x)
(rev2 x nil)
)
V řešení jsme si nadefinovali obecnější funkci rev2
, která vrátí seznam, jenž
vznikne připojením seznamu y
za obrácení seznamu x
. Taková funkce se dá snadno
naprogramovat rekurzivně a reverse
je vlastně rev2
s y=nil
.
Soutěžní úlohy: Naprogramujte v Ksp-Lispu:
- funkci
max
, která jako své parametry dostane n celých čísel a vrátí nejmenší z nich. - funkci
equal
, která porovná dva objekty (čísla, seznamy, symboly atd.) na ekvivalenci podle struktury (jinými slovy na číslech a symbolech se bude chovat stejně jakoeq
, ale seznamy a podobné konstrukce z párů bude považovat za stejné i tehdy, pokud jsou vystavěny z různých objektů, ale obsahují totéž). Vrátít
při shodě, jinaknil
. Příklady: ,(equal '(1 2) '(3)) = nil
,(equal 1 1) = t
,(equal '(1 (2 3)) '(1 (2 3))) = t
.
Pokud si své programy chcete vyzkoušet, můžete si stáhnout interpret Ksp-Lispu (tar, ZIP).