Matematická olympiáda – kategorie P

Zadání úloh krajského kola 52. ročníku

Krajské kolo 52. ročníku MO kategorie P se koná v úterý 7. 1. 2003 v dopoledních hodinách. Na řešení úloh máte 4 hodiny čistého času. V krajském kole MO-P se neřeší žádná praktická úloha, pro zajištění rovných podmínek řešitelů ve všech krajích je použití počítačů při soutěži zakázáno.

Řešení každého příkladu musí obsahovat:

Hodnotí se nejen správnost programu, ale také kvalita popisu řešení a efektivita zvoleného algoritmu.

Vzorová řešení úloh naleznete krátce po soutěži na Internetu na adrese http://mo.mff.cuni.cz/. Na stejném místě budou zveřejněny i výsledky všech krajských kol a seznam postupujících do celostátního kola. Naleznete zde také popis prostředí, v němž budete na celostátním kole řešit praktické úlohy.

P-II-1 Tvůrci hvězd

Společnost pro výzkum mimozemského života vypátrala, že náš vesmír byl v minulosti (a možná ještě teď je) osídlen mimořádně vyspělou technickou civilizací. Pracovně byla tato civilizace pojmenována Tvůrci hvězd, protože technická vyspělost rasy umožňovala dokonce i tvořit nové hvězdy. Tvůrci hvězd měli silně vyvinutý smysl pro pořádek a symetrii. Proto stavěli hvězdy středově symetricky okolo bodu, kde se kdysi nalézala jejich domovská hvězda (než vybuchla jako supernova). Vědci by nyní rádi dodali své teorii trochu více věrohodnosti, a tak by chtěli zjistit, jestli hvězdy vytvořené civilizací leží skutečně středově symetricky okolo nějakého bodu (skutečnou pozici domovské hvězdy totiž neznají). Protože hvězd je poměrně hodně, rozhodli se řešit problém s pomocí výpočetní techniky.

Vaším úkolem je napsat program, který dostane na vstupu počet hvězd vytvořených civilizací N a souřadnice oněch N hvězd (poloha každé hvězdy je určena trojicí celých čísel xi, yi, zi). Můžete předpokládat, že žádné dvě hvězdy neleží na stejném místě (tzn. nemají shodné všechny tři souřadnice). Na výstup program vypíše bod, podle kterého jsou hvězdy rozmístěny středově symetricky, popřípadě zprávu, že takový bod neexistuje. Hvězdy jsou rozmístěny symetricky okolo středu S, pokud pro každou hvězdu H existuje hvězda H' taková, že S je středem úsečky HH'. Speciálně může nějaká hvězda ležet přímo ve středu S.

Příklad 1

Pro 6 hvězd na souřadnicích (0,1,-1), (2,0,1), (4,0,3), (0,4,1), (4,3,5), (2,4,3) leží střed na souřadnicích (2,2,2).

Příklad 2

4 hvězdy na souřadnicích (0,0,0), (5,0,0), (2,1,0), (2,-1,0) nejsou umístěny středově symetricky.

P-II-2 Knihovna

Knihovnice Míla opět potřebuje objednat další skříň do své knihovny. Bohužel však zase sama neumí spočítat, jak by tato skříň měla být široká, a tak vás znovu poprosila o pomoc. Míla by ráda do nové skříně umístila celkem N knih, ale narozdíl od úlohy P-I-2 z prvního kola je jí jedno, v jakém pořadí knihy do skříně umístí. Vstupem vašeho programu bude posloupnost N čísel vi, 1<=i<=N, kde vi je výška i-té knihy. Pro zjednodušení předpokládejme, že všechny knihy mají stejnou tloušťku - 1 cm.

Váš program by měl ze zadaných údaju spočítat následující:

Rozmístění knih, které váš program nalezne, musí z pochopitelných důvodů splňovat následující:

Příklad

Předpokládejme, že Míla chce do skříně umístit celkem 14 knih, z nichž devět má výšku 50 cm a pět má výšku 40 cm. Jedno z optimálních řešení by mohlo vypadat následovně: Skříň bude mít šířku pro 3 knihy a celkem 5 poliček - tři z nich budou mít výšku 50 cm a dvě poličky výšku 40 cm. Nalézt jedno konkrétní možné rozmístění knih do skříně je snadné.

P-II-3 Transformace

Jedna z metod zpracování textu používá následující transformační algoritmus: Na vstupu mějme n-znakový řetězec C=c1c2...cn. Řetězec C'=ck+1ck+2...cnc1...ck nazýváme řetězcem C zrotovaným o k (tedy např. akaabr je řetězec abraka zrotovaný o 3). Vezměme si zadaný řetězec C a napišme si pod sebe C, C zrotovaný o 1, ..., C zrotovaný o n-1. Tím jsme získali tabulku n řetězců. Ty setřídíme v běžném lexikografickém pořadí (tzn. podle abecedy). Z výsledné tabulky si vybereme poslední sloupec S; dále si také zapamatujeme číslo řádku ř, na němž se po setřídění nachází náš původní řetězec (je-li těchto řádků více, libovolný z nich). Dvojice (S,ř) je výsledek transformace zadaného vstupu. Jakkoli magicky to vypadá, tyto dva údaje stačí k rekonstrukci původního řetězce.

Příklad

Na vstupu máme slovo abraka. Transformace probíhá takto:
abraka aabrak brakaa abraka * rakaab -> akaabr akaabr brakaa kaabra kaabra aabrak rakaab
Výsledkem tedy je slovo karaab a informace, že původně zadané slovo je na druhém řádku setříděné tabulky.

Soutěžní úloha

Program dostane na vstupu řetězec S délky n (1<=n<=10000) a číslo ř (1<=ř<=n). Úkolem je najít řetězec C takový, že dvojice (S,ř) je výsledkem aplikace výše popsané transformace na řetězec C (máte zaručeno, že takový existuje).

Poznámka

Budete-li psát program v Pascalu, můžete předpokládat, že do stringu se řetězec této délky vejde.

Uvědomte si, že při použití v praxi se délky zpracovávaných vstupů pohybují řádově ve stovkách kilobytů; je tedy nevhodné, aby váš program měl kvadratické časové nebo paměťové nároky.

Příklad

Vstupní soubor bw.in:
karaab 2
Výstupní soubor bw.out:
abraka

P-II-4 Reverzibilní výpočty: Ouřad

Definice reverzibilních výpočtů je stejná jako v domácím kole, naleznete ji ve studijním textu na konci této úlohy.

Oblastní Ouřad sídlí v městě M v n budovách. Aby si ouředníci sídlící na různých místech mohli rychle posílat všechny ty dopisy, přípisy, zápisy, dobropisy, vrubopisy, tiskopisy a vůbec všelijaké spisy s podpisy, vybudovali si potrubní poštu - systém rour spojujících některé dvojice budov. Těmito rourami se pomocí stlačeného vzduchu posílají zásilky. Aby nedocházelo ke kolizím, je každá roura využívána jenom jedním směrem. Nejsou si ale jisti, jestli již postavili všechna potřebná potrubí, a tak je zajímá, jak zjistit, zda mezi nějakými dvěma zadanými místy je možné dopravit zásilku, a to buďto přímo nebo s přeložením zásilky v nějaké mezistanici, případně mezistanicích.

Napište reverzibilní proceduru Zkoumej(var n:word; var A:array [1..n] of array [1..n] of bit; var x,y,d:word), která dostane jako vstup počet budov n, matici A popisující v prvku A[i][j], zda vede (1) či nevede (0) roura z i-té do j-té budovy a čísla budov x a y. Poté do proměnné d přičte, s jakým nejmenším počtem přeložení je možno přepravit zásilku z budovy x do budovy y, případně přičte číslo větší než n, pokud to možné není. Snažte se dosáhnout co nejmenší prostorové složitosti výpočtu při zachování polynomiální časové složitosti.

Studijní text

Při hledání nových, úspornějších polovodičových technologií se zjistilo, že nejvíce energie se spotřebovává při mazání informací, tudíž že optimální jsou ty výpočty, při nichž se žádné informace neztrácejí. Takovým výpočtům se říká reverzibilní, protože díky této vlastnosti mohou probíhat oběma směry - dokáží nejen spočítat ze vstupu výstup, ale také z výstupu jednoznačně určit vstup. Vydejme se proto i my do tohoto zvláštního symetrického světa a prozkoumejme, jak se programuje "ekologicky".

Začněme tím nejjednodušším, co se v klasických programovacích jazycích vyskytuje, a to je přiřazovací příkaz. Nic takového si bohužel dovolit nemůžeme, ztratili bychom totiž původní obsah proměnné, do níž se přiřazuje. Místo toho zavedeme několik příkazů modifikujících proměnnou vratně:

Abychom se vyhnuli problémům s přetečením (co by pak byla inverzní operace?), dohodněme se, že budeme počítat pouze s nezápornými celými čísly v rozsahu 0... maxword (takovým číslům budeme říkat přirozená) a všechny operace budou vydávat výsledky modulo maxword+1, tedy opět přirozené číslo. Příkaz += provedený pozpátku je pak totéž, co -= a opačně; přikazy ^= a =:= jsou inverzní samy k sobě.

Co všechno ale může být hodnota? Jistě libovolná konstanta nebo proměnná (ovšem různá od té, do které přiřazujeme, jinak bychom mohli napsat třeba a -= a, což určitě reverzibilní není). Také bychom měli povolit nějaké další aritmetické operace - ty samy nemusí být reverzibilní, důležité je, aby se jejich výsledek zpracoval reverzibilně. Každý složitější výraz pak už můžeme přepsat na výrazy s jedinou operací, například x^=(a*b)+(c*d) rozepíšeme takto:

t1 += a*b; t2 += c*d; x ^= t1+t2; t2 -= c*d; t1 -= a*b; Zde t1 a t2 jsou pomocné proměnné, které jsou na počátku výpočtu nulové a po dopočítání výrazu se opět k nulovým hodnotám vrátí, takže je můžeme používat pro všechny výrazy v celém programu. Podobně se vypořádáme s každým výrazem - nejdříve si spočítáme všechny mezivýsledky do pomocných proměnných, pak hlavní výsledek použijeme, načež mezivýsledky opět "odpočítáme". Takže můžeme používat i slozité výrazy a spolehnout se na překladač, že je sám rozepíše. Trik s odpočítáváním mezivýsledků a spouštěním částí programu pozadu je, zdá se, velice šikovný, tak si rovnou nadefinujeme, že undo příkaz znamená spustit příkaz pozpátku a wrap příkaz1 on příkaz2 provede nejdříve příkaz1, pak příkaz2 a nakonec undo příkaz1 pro odpočítání mezivýsledků. Náš příklad s výrazem pak snadno zapíšeme takto: wrap begin t1 += a*b; t2 += c*d end on x ^= t1+t2 Podmíněné příkazy if-then-else můžeme používat bez obav, pokud zaručíme, že po provedení podmíněného příkazu dopadne podmínka úplně stejně jako předtím (třeba proto, že žádná z proměnných, které v ní vystupují, není v podmíněné části programu měněna). Pak totiž i při provádění výpočtu pozpátku rozpoznáme, kterou z větví se výpočet má vydat.

S cykly je situace svízelnější, protože tam si s neměnícími se podmínkami nevystačíme (to by každý cyklus buďto neproběhl nikdy nebo by se opakoval do nekonečna). Dalo by se to, pravda, zachránit tím, že by každý cyklus měl jednu podmínku, která by fungovala současně jako vstupní i výstupní - sami si rozmyslete, jak by takové cykly vypadaly. My si ale pro naše účely vystačíme s cykly for, ty určitě reverzibilní jsou, pokud řídící proměnnou cyklu ani její meze žádný příkaz uvnitř cyklu nemodifikuje, a to se koneckonců nesmí ani v mnoha jiných programovacích jazycích. Navíc abychom nemuseli řešit, co se v řídící proměnné musí vyskytovat před začátkem cyklu a co po jeho konci, domluvíme se, že příkaz for si tuto proměnnou sám vytvoří a na konci ji zase zruší.

Příkaz goto pro jistotu zakážeme úplně.

Procedury mohou také fungovat reverzibilně, ale musíme se vyhnout kopírování parametrů a výsledků, budeme proto vše vždy předávat odkazem (pascalské var). Lokální proměnné budou při spuštění procedury vždy nulové a procedura sama je musí, než skončí, opět do tohoto stavu vrátit. Rekurze je bez problémů.

Nyní již máme vše potřebné, abychom si vybudovali reverzibilní programovací jazyk. Ten náš bude vzdáleným příbuzným Pascalu. Vypadá takto:

Příklad 1

Procedura pro prohození obsahu dvou proměnných (která ukazuje, že =:= se dá snadno odvodit pomocí ostatních operací). Časová i prostorová složitost jsou konstantní, tedy O(1). procedure Prohod(var x,y:word); begin { x = X, y = Y (X,Y jsou pův. hodnoty) } x ^= y; { x = X xor Y, y = Y } y ^= x; { x = X xor Y, y = Y xor (X xor Y) = X } x ^= y { x = (X xor Y) xor X = Y, y = X } end;

Příklad 2

Procedura pro výpočet maxima ze zadaných n čísel. Je dáno pole X celých čísel a proměnná max, k níž máme spočtené maximum přičíst. To dokážeme takto: Nejprve si předpočítáme do M[i] maximum z čísel X[1],...,X[i], pak přičteme M[n] k max a nakonec M[i] opět vyprázdníme, což snadno zapíšeme pomocí příkazu wrap. Časová i prostorová složitost jsou O(n) čili lineární. procedure Maximum(var n:word; var X:array [1..n] of word; var max:word); var M:array [0..n] of word; begin wrap for var i=1 to n do if X[i]>M[i-1] then M[i] += X[i] else M[i] += M[i-1] on max += M[n]; end;