Matematická olympiáda – kategorie P

Zadání úloh ústředního kola (1. soutěžní den) 52. ročníku

Všeobecné pokyny

Na řešení úloh máte 4.5 hodiny čistého času.

Ř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.

P-III-1 Hračkářství

V hračkářství Prcek a otec proběhla velká soutěž "O nejhezčí hračku". Děti měly za úkol nakreslit obrázek své nejoblíbenější hračky. Po ukončení soutěže byla uspořádána výstavka a děti, které nakreslily nejpěknější obrázky, dostaly od hračkářství nějakou hračku. Jak ale asi víte, ne každému dítěti se líbí každá hračka, a tak už před vyhlášením soutěže měl každý malý výtvarník vyhlédnutou tu odměnu, kterou chtěl za svůj obrázek dostat. Tu a žádnou jinou. Svůj názor pak děti po vyhlášení dávaly dost hlasitě najevo. Maminky ječících potomků se tedy rozhodly, že děti si hračky mezi sebou povyměňují tak, aby pokud možno co nejvíce dětí bylo se svou výhrou spokojeno. Situaci ještě navíc komplikuje skutečnost, že k výměně jsou ochotné pouze ty děti, které nakonec dostanou hračku, po níž touží. S tak náročným úkolem si maminky nevěděly rady, a tak poprosily vás, abyste napsali program, který problém vyřeší.

Váš program dostane na vstupu zadán počet odměněných dětí N a dále pro každé dítě číslo hračky, kterou dostalo, a číslo hračky, kterou by chtělo dostat (protože hraček je stejně jako dětí, očíslujeme si je pro jednoduchost od jedné do N). Na výstup program vypíše největší skupinu dětí takovou, že když si děti ve skupině mezi sebou vhodným způsobem vymění hračky, budou všechny spokojené.

P-III-2 Knihovna

Knihovnice Míla opět potřebuje objednat další skříň do své knihovny a protože se jí vaše pomoc osvědčila, opět se na vás obrátila, abyste jí pomohli spočítat optimální rozměry nové skříně. Nová skříň má mít P poliček a Míla by do ní ráda umístila celkem N knih. Každá kniha má přiřazen jednoznačný číselný kód a tyto kódy určují pořadí knih ve skříni. Kniha s menším kódem se má nacházet na stejné nebo výše umístěné poličce než kniha s větším kódem; na každé poličce mají být knihy s menšími kódy umístěny vlevo od knih s většími kódy. Vstupem vašeho programu bude posloupnost N celých čísel ti, 1<=i<=N, kde ti je tloušťka i-té knihy. Můžete předpokládat, že tloušťka ti i-té knihy je z rozmezí od 1 mm do 50 mm. Výška každé knihy je taková, že ji lze bez problémů umístit do libovolné z plánovaných P poliček. Váš program by měl ze zadaných údajů 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 nová skříň má mít 3 poličky a má být do ní umístěno celkem 6 knih s následujícími tloušťkami (seřazeny vzestupně podle svých kódů): 15 mm, 20 mm, 7 mm, 6 mm, 2 mm a 4 mm. Minimální možná šířka skříně v tomto případě je 20 mm - na první poličku se dá pouze první kniha, na druhou pouze druhá kniha a zbylé knihy se umístí na poslední třetí poličku.

P-III-3 Reverzibilní výpočty: Sčítání

(Definice reverzibilních výpočtů je stejná jako v domácím i krajském kole, jen jsme některá důležitá pravidla zvýraznili. Naleznete ji ve studijním textu za touto úlohou.)

Napište reverzibilní proceduru Add(var n:word; var A,B:array [0..n-1] of bit) sloužící ke sčítání dvou n-bitových čísel zapsaných ve dvojkové soustavě (bit číslo 0 odpovídá řádu jednotek). Tato procedura přičte číslo uložené v poli B k číslu uloženému v poli A. Vstup dostane vždy takový, aby nedošlo k přetečení, tedy součet bude vždy také n-bitový. Snažte se dosáhnout co nejmenší prostorové složitosti vaší procedury.

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říkazy ^= 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;