Rekurze. Pavel Töpfer, KSVI MFF UK Praha. 1. Co je to rekurze Rekurze v programech Rekurzivní algoritmy... 10

Podobné dokumenty
Rekurze. Pavel Töpfer, 2017 Programování 1-8 1

Programovací jazyk Pascal

Binární soubory (datové, typované)

Test prvočíselnosti. Úkol: otestovat dané číslo N, zda je prvočíslem

Prohledávání do šířky = algoritmus vlny

Vyučovací hodina. 1vyučovací hodina: 2vyučovací hodiny: Opakování z minulé hodiny. Procvičení nové látky

Pascal. Katedra aplikované kybernetiky. Ing. Miroslav Vavroušek. Verze 7

Čtvrtek 8. prosince. Pascal - opakování základů. Struktura programu:

Řešení: PŘENESVĚŽ (N, A, B, C) = přenes N disků z A na B pomocí C

type Obdelnik = array [1..3, 1..4] of integer; var M: Obdelnik;

dovolují dělení velkých úloh na menší = dekompozice

Rekurze. Jan Hnilica Počítačové modelování 12

1 PRVOCISLA: KRATKY UKAZKOVY PRIKLAD NA DEMONSTRACI BALIKU WEB 1

6 Příkazy řízení toku

NPRG030 Programování I, 2015/16 1 / :25:32

Programovací jazyk. - norma PASCAL (1974) - implementace Turbo Pascal, Borland Pascal FreePascal Object Pascal (Delphi)

Obecná informatika. Matematicko-fyzikální fakulta Univerzity Karlovy v Praze. Podzim 2012

1.1 Struktura programu v Pascalu Vstup a výstup Operátory a některé matematické funkce 5

Časová a prostorová složitost algoritmů

Jednoduché cykly

Anotace. Jednotky (tvorba a využití), struktury (typ record),

ALGORITMIZACE A PROGRAMOVÁNÍ

Implementace LL(1) překladů

(Úlohy z MO kategorie P, 32. část)

1. D Y N A M I C K É DAT O V É STRUKTUR Y

Reprezentace aritmetického výrazu - binární strom reprezentující aritmetický výraz

VÝUKOVÝ MATERIÁL. Bratislavská 2166, Varnsdorf, IČO: tel Číslo projektu

Algoritmizace Dynamické programování. Jiří Vyskočil, Marko Genyg-Berezovskyj 2010

Příklady: (y + (sin(2*x) + 1)*2)/ /2 * 5 = 8.5 (1+3)/2 * 5 = /(2 * 5) = 1.3. Pavel Töpfer, 2017 Programování 1-3 1

5 Rekurze a zásobník. Rekurzivní volání metody

Úvod do programování

Vektory a matice. Obsah. Aplikovaná matematika I. Carl Friedrich Gauss. Základní pojmy a operace

Definice. Vektorový prostor V nad tělesem T je množina s operacemi + : V V V, tj. u, v V : u + v V : T V V, tj. ( u V )( a T ) : a u V které splňují

MAXScript výukový kurz

Program a životní cyklus programu

5 Orientované grafy, Toky v sítích

Maturitní otázky z předmětu PROGRAMOVÁNÍ

Vyhledávání. doc. Mgr. Jiří Dvorský, Ph.D. Katedra informatiky Fakulta elektrotechniky a informatiky VŠB TU Ostrava. Prezentace ke dni 21.

- znakové konstanty v apostrofech, např. a, +, (znak mezera) - proměnná zabírá 1 byte, obsahuje kód příslušného znaku

NPRG030 Programování I, 2017/18 1 / :22:16

Algoritmizace a programování

1. Implementace funkce počet vrcholů. Předmět: Algoritmizace praktické aplikace (3ALGA)

Cykly a pole

Sada 1 - Základy programování

Dynamické programování

Logo2 operace, rekurze, větvení výpočtu

Je n O(n 2 )? Je n 2 O(n)? Je 3n 5 +2n Θ(n 5 )? Je n 1000 O(2 n )? Je 2 n O(n 2000 )? Cvičení s kartami aneb jak rychle roste exponenciála.

1 Řešení soustav lineárních rovnic

9 Kolmost vektorových podprostorů

Paměť počítače. alg2 1

PODPROGRAMY PROCEDURY A FUNKCE

for (int i = 0; i < sizeof(hodnoty) / sizeof(int); i++) { cout<<hodonoty[i]<< endl; } cin.get(); return 0; }

2 Strukturované datové typy Pole Záznam Množina... 4

Programování v jazyku LOGO - úvod

Kombinatorika. Michael Krbek. 1. Základní pojmy. Kombinatorika pracuje se spočitatelnými (tedy obvykle

Programujeme v softwaru Statistica

III/2 Inovace a zkvalitnění výuky prostřednictvím ICT

Úvod do informatiky. Miroslav Kolařík

NPRG030 Programování I, 2018/19 1 / :25:37

Informatika 8. třída/6

2.1 Podmínka typu case Cykly Cyklus s podmínkou na začátku Cyklus s podmínkou na konci... 5

Vyhledávání. doc. Mgr. Jiří Dvorský, Ph.D. Katedra informatiky Fakulta elektrotechniky a informatiky VŠB TU Ostrava. Prezentace ke dni 12.

2 Datové typy v jazyce C

7 Formátovaný výstup, třídy, objekty, pole, chyby v programech

Algoritmizace řazení Bubble Sort

Sada 1 - Základy programování

- speciální symboly + - * / =., < > <> <= >= a další. Klíčová slova jsou chráněnými útvary, které nelze použít ve významu identifikátorů.

5 Přehled operátorů, příkazy, přetypování

South Bohemia Mathematical Letters Volume 23, (2015), No. 1, DĚLENÍ KRUHU NA OBLASTI ÚVOD

Úvod do programování 10. hodina

63. ročník Matematické olympiády 2013/2014

III/2 Inovace a zkvalitnění výuky prostřednictvím ICT

Funkce, elementární funkce.

DUM 06 téma: Tvorba makra pomocí VBA

Řešení úloh z TSP MU SADY S 1

Algoritmus pro hledání nejkratší cesty orientovaným grafem

Už známe datové typy pro representaci celých čísel i typy pro representaci

Pointery II. Jan Hnilica Počítačové modelování 17

VY_32_INOVACE_08_2_04_PR

Poslední nenulová číslice faktoriálu

Aplikovaná informatika. Podklady předmětu Aplikovaná informatika pro akademický rok 2006/2007 Radim Farana. Obsah. Obsah předmětu

Struktura programu v době běhu

Digitální učební materiál

Programovací jazyky. imperativní (procedurální) neimperativní (neprocedurální) assembler (jazyk symbolických instrukcí)

Předmět: Algoritmizace praktické aplikace

NMIN101 Programování 1 2/2 Z --- NMIN102 Programování /2 Z, Zk

VÝUKOVÝ MATERIÁL. Bratislavská 2166, Varnsdorf, IČO: tel Číslo projektu

DobSort. Úvod do programování. DobSort Implementace 1/3. DobSort Implementace 2/3. DobSort - Příklad. DobSort Implementace 3/3

Lokální definice (1) plocha-kruhu

Algoritmizace. 1. Úvod. Algoritmus

for (i = 0, j = 5; i < 10; i++) { // tělo cyklu }

MATURITNÍ TÉMATA Z MATEMATIKY

Sada 1 - Základy programování

Algoritmus pro generování normálních magických čtverců

Základní způsoby: -Statické (přidělění paměti v čase překladu) -Dynamické (přiděleno v run time) v zásobníku na haldě

Syntaktická analýza. Implementace LL(1) překladů. Šárka Vavrečková. Ústav informatiky, FPF SU Opava

PSK3-9. Základy skriptování. Hlavička

8 Třídy, objekty, metody, předávání argumentů metod

Počítačové zobrazování fraktálních množin. J. Bednář*, J. Fábera**, B. Fürstová*** *Gymnázium Děčín **SPŠ Hronov ***Gymnázium Plasy

NP-úplnost problému SAT

Transkript:

Rekurze Pavel Töpfer, KSVI MFF UK Praha OBSAH 1. Co je to rekurze... 2 2. Rekurze v programech... 5 3. Rekurzivní algoritmy... 10 3.1. Matematické rekurzivní vzorce... 10 3.2. Generování všech prvků dané vlastnosti... 16 3.3. Prohledávání s návratem... 30 3.4. Rozděl a panuj... 33 4. Rekurzivní datové struktury... 36 5. Efektivita rekurzivních postupů... 39 5.1. Zrychlení rekurze... 39 5.2. Nahrazení rekurzivních volání zásobníkem... 40 5.3. Odstranění rekurzivního postupu... 41 LITERATURA Tento text vychází ze stejnojmenné publikace vydané v roce 1998 nakladatelstvím Fortuna. Různé další rekurzivní algoritmy naleznete vedle tohoto textu také v učebnicích Pavel Töpfer: Algoritmy a programovací techniky, Prometheus Praha 1995, 2. vyd. 2007 Niklaus Wirth: Algoritmy a štruktúry údajov, Alfa Bratislava 1988

1. Co je to rekurze Tento text podává základní přehled o tom, co je to rekurze a jak se v programování využívá. Obsahuje obecný výklad celé problematiky a řadu řešených úloh. Zabývá se vhodností a nevhodností použití rekurze v různých situacích a také otázkami efektivity rekurzivních algoritmů a programů. V programových ukázkách je zde všude používán programovací jazyk Pascal, resp. jeho implementace Turbo Pascal. Uvedené rekurzivní algoritmy jsou ale samozřejmě nezávislé na konkrétním programovacím jazyce. Algoritmus, datovou strukturu nebo třeba kresbu či jakýkoli jiný předpis označujeme jako rekurzivní, jestliže je definován pomocí sebe sama. Známý rekurzivní obrázek používaný v mnoha učebnicích o rekurzi znázorňuje televizor, na jehož obrazovce vidíme televizor, na jehož obrazovce vidíme televizor, atd. Tento jev jste ostatně mohli vidět na vlastní oči i ve skutečné televizi, pokud se v nějakém přenosu z televizního studia dostane náhodou do záběru televizní kamery obrazovka monitoru, v němž moderátoři sledují průběh vysílání. Popsaný rekurzivní jev je teoreticky nekonečný, ve skutečnosti však vidíme pouze jistý konečný počet obrazovek v sobě. Tato skutečnost je způsobena omezenou rozlišovací schopností jak televizní obrazovky, tak i tiskárny v případě tištěného obrázku. S některými rekurzivními jevy a postupy se setkáváme i jinde v životě. Snad každé malé dítě zná pohádku o kohoutkovi a slepičce. Lakomý kohoutek se nerozdělil se slepičkou o nalezený oříšek, spolknul ho sám a oříšek mu uváznul v krku. Slepička musí pro záchranu kohoutka podniknout složitou cestu. Potřebuje přinést kohoutkovi vodu ze studánky, ale studánka chce za vodu šátek od švadleny. Švadlena jí ale nedá šátek, dokud nedostane střevíce od ševce. A tak putování slepičky pokračuje stále dál. Kdykoli někoho o něco požádá, splnění její prosby je podmíněno dalším požadavkem. Ještě že po mnoha krocích svého putování narazí slepička na hodné nebe, které za rosu pro louku nic nechce. Počet rekurzivních kroků je tím pevně omezen a pohádka může šťastně skončit záchranou kohoutka. A kde že je v pohádce použita rekurze? Slepička opakovaně provádí akci typu přines od X předmět Y, která má rekurzivní charakter. K jejímu provedení je totiž nutné nejprve vykonat akci téhož typu, jen s jinými hodnotami X, Y. S podobným postupem se setkáváme i v dospělosti při jednání s různými úřady. Úřad A potřebuje k vystavení námi požadovaného dokladu nejprve donést potvrzení od úřadu B, úřad B ale žádá nejprve potvrzení od úřadu C, atd. Máme-li alespoň tolik štěstí jako pohádková slepička, narazíme po konečném počtu kroků na úřad, který od nás nic dalšího nepožaduje, a celou svoji záležitost můžeme nakonec úspěšně vyřídit. Uvedené příklady nám tedy zároveň ilustrují důležitost omezení hloubky rekurze pro praktickou použitelnost rekurzivního postupu. Různé rekurzivní předpisy se vyskytují zejména v matematice. Používají se například v definicích některých číselných posloupností. V pořadí n-tý člen posloupnosti {a n } může být zadán buď explicitním vzorcem v závislosti na n, např. a n =2n-1, nebo může být určen rekurzivně pomocí jiného nebo jiných členů téže posloupnosti, např. a n =a n-1 +2. Aby měla smysl takováto rekurzivní definice, musí být ovšem zadány ještě počáteční hodnoty, od kterých se celá rekurze odvíjí. V našem příkladu může tedy úplná definice posloupnosti {a n } vypadat třeba takto: a 1 = 1 a n = a n-1 + 2 pro n > 1. Není stanovena žádná horní mez indexu n, takže v tomto případě máme rekurzivní předpis nekonečné číselné posloupnosti. Snadno zjistíte, že jde o posloupnost všech lichých celých čísel, 2

tedy o stejnou posloupnost, kterou jsme již dříve definovali také explicitním předpisem a n = 2n-1, n > 0. V případě posloupnosti lichých čísel bychom se bez rekurzivního předpisu klidně obešli, při výpočtech stejně raději použijeme jednoduchý explicitní vzorec. U některých posloupností však takovouto jednoduchou možnost nemáme. Buď explicitní vzorec vůbec neznáme, nebo je pro naše potřeby příliš komplikovaný. K nejznámějším posloupnostem, které bývá v matematice zvykem definovat rekurzivně, patří třeba faktoriál nebo posloupnost Fibonacciho čísel. Faktoriál celého kladného čísla n označujeme symbolem n!. Představuje součin všech kladných celých čísel od 1 do n. Faktoriál se používá ve velké míře například v kombinatorice. Formálně ho definujeme rekurzivním předpisem 1! = 1 n! = n.(n-1)! pro n > 1. Fibonacciho čísla patří k nejzajímavějším číselným posloupnostem. Setkáme se s nimi nejen v matematice, ale v různých nečekaných souvislostech i v biologii, ve výtvarném umění a také při návrhu algoritmů a programů. Posloupnost začíná dvěma jedničkami a dále pokračuje tak, že každé další Fibonacciho číslo je rovno součtu dvou bezprostředně předcházejících Fibonacciho čísel. Formálně tento rekurzivní vztah zapíšeme následovně: F 0 = 0 F 1 = 1 F n = F n-1 + F n-2 pro n > 1. Rekurzivní formule se objevují i v definicích některých geometrických útvarů a křivek. Nejznámější rekurzivně definované geometrické křivky patří mezi tzv. fraktální obrazce. Problematika fraktální geometrie je velmi bohatá a nemůžeme se jí zde podrobně věnovat. Ukážeme si proto alespoň dva typické příklady geometrických útvarů, jejichž definice jsou založeny na rekurzi. Prvním z nich bude slavná sněhová vločka Kochové. Při její konstrukci vyjdeme od rovnostranného trojúhelníka. Každou jeho stranu rozdělíme na třetiny a nad prostředním dílem každé strany sestrojíme menší rovnostranný trojúhelník (směrem ven od středu útvaru). Tím dostaneme křivku Kochové 2. řádu ve tvaru jednoduché pravidelné šesticípé hvězdy. Dále postupujeme stejným způsobem, tj. každou stranu vždy rozdělíme na třetiny a nad prostřední z nich sestrojíme rovnostranný trojúhelník směrem ven. Počet iterací není teoreticky nijak pevně omezen. Jako druhý příklad si můžeme uvést neméně známou Hilbertovu křivku. Hilbertova křivka 1. řádu je definována jednoduše pomocí tří spojených stejně dlouhých úseček. Hilbertovy křivky vyšších řádů jsou pak zadány rekurzivním předpisem. Křivku k-tého řádku pro k > 1 sestrojíme složením čtyř křivek řádu (k-1), které vůči sobě vhodně natočíme a spojíme třemi úsečkami. Názorně vidíme celou situaci na obrázku s prvními třemi Hilbertovými křivkami: 3

My se budeme v této knize zajímat o použití rekurze v programování. Rekurze se používá jak při návrhu algoritmů, tak i při realizaci algoritmů formou rekurzivních volání procedur a funkcí. Rekurzivní principy se uplatňují i při návrhu a používání některých datových struktur. Zaměříme se pouze na využití rekurze při práci s běžnými programovacími jazyky. Jejich typickým představitelem je Pascal, který budeme také používat ve všech programových ukázkách. Přesněji řečeno, budeme používat Turbo Pascal jakožto dnes nejrozšířenější implementaci Pascalu na PC. Nebudeme se věnovat ani neprocedurálním programovacím jazykům (jako jsou Lisp nebo Prolog), pro které je použití rekurze typické, ani jazykům speciálním (např. školní výukový jazyk Karel), které rovněž rekurzi bohatě využívají. Později se ještě vrátíme k některým příkladům uvedeným v této úvodní kapitole a ukážeme si možnosti jejich programové realizace. 4

2. Rekurze v programech Rekurze představuje velmi silný prostředek v rukou programátora. S využitím rekurze můžeme často napsat krátký a elegantní program, zatímco nerekurzivní řešení téhož problému by bylo mnohem pracnější. Použití rekurze má však také svá úskalí. Každý vysoce účinný nástroj se nám může stát nebezpečným, pokud s ním nepracujeme dostatečně opatrně a používáme ho neuváženě při práci, pro kterou není určen. Jestliže v programu použijeme rekurzi na nevhodném místě, můžeme dostat program, který bude sice funkčně správný, ale velmi nepřehledný a nesrozumitelný. V takovém programu se pak jen těžko provádějí nějaké pozdější úpravy nebo změny a je také pravděpodobnější, že se při zápisu programu dopustíme chyby. Hledání a odstraňování běhových chyb v rekurzivních programech bývá navíc obtížnější než v programech bez rekurze. Nešikovné použití rekurze nám však přináší ještě další nebezpečí. Často vede k programům, které jsou sice věcně správné, ale jsou příliš pomalé. V krajním případě se může snadno stát, že teoreticky dobře navržený program je prakticky nepoužitelný, neboť doba potřebná k jeho provedení na počítači přesahuje naše časové možnosti. Rekurzi bychom proto měli v programování používat vždy opatrně a pouze tam, kde je její použití účelné a přirozené, kde nám zjednoduší zápis programu a nezpůsobí přílišné zpomalení výpočtu. V programování se rekurze objevuje ve dvou odlišných rovinách. První z nich je rekurzivní návrh algoritmu, druhou pak použití rekurzivního volání procedury nebo funkce jakožto prostředku programovacího jazyka. Tyto dvě roviny spolu zpravidla úzce korespondují, rekurzivní volání využíváme zejména pro realizaci rekurzivních algoritmů. Nemusí tomu ale tak být vždycky. Algoritmus, který je svou povahou rekurzivní, můžeme naprogramovat i bez použití rekurzivních volání podprogramů tak, že mechanismus rekurzivního volání nahradíme vlastním programovým zásobníkem a cyklem v programu. Zápis programu se tím obvykle trochu zkomplikuje, výpočet ale bývá dokonce o něco rychlejší. Ušetří se totiž čas, který je při výpočtu rekurzivní verze programu potřebný pro vlastní režii rekurze, tj. pro každé zavolání rekurzivního podprogramu a pro jeho ukončení. Někdy rekurzivní volání procedur a funkcí ani použít nemůžeme, neboť zvolený programovací jazyk něco takového vůbec nepřipouští (např. původní verze jazyka Fortran). Naopak rekurzivní proceduru můžeme použít v programu i v situaci, kdy algoritmus žádnou rekurzi nevyžaduje. V krajním případě můžeme dokonce rekurzivní procedurou nahradit jakýkoli cyklus. Místo zápisu cyklu ve tvaru while P do S; je možné zavolat rekurzivní proceduru Q, kterou si předem deklarujeme takto: procedure Q; if P then S; Q Takovéto použití rekurze patří samozřejmě do kategorie těch naprosto nevhodných. Zápis programu zbytečně znepřehledňuje a navíc zpomaluje výpočet programu. Rekurze jakožto prvek programovacího jazyka spočívá v tom, že procedura nebo funkce může ve svém těle volat sama sebe. Tomuto volání pak říkáme rekurzivní volání procedury, resp. funkce. Je vykonáno stejným způsobem jako kterékoli jiné volání procedury. Mechanismus volání procedur a funkcí je v programovacích jazycích podobných Pascalu realizován pomocí vyhrazené oblasti operační paměti počítače zvané systémový zásobník. Při každém zavolání procedury (příp. funkce) se na vrcholu tohoto zásobníku vymezí místo pro tzv. aktivační záznam volané procedury. Ten obsahuje prostor pro uložení všech parametrů a lokálních proměnných procedury. Dále 5

obsahuje některé technické údaje, jako např. návratovou adresu (tj. adresu v kódu programu, odkud byla procedura zavolána). Tyto další údaje jsou potřebné pro správné provádění odkazů na globální proměnné během práce procedury a pro korektní ukončení procedury s předáním řízení zpět do místa jejího volání. V každém okamžiku výpočtu programu je na vrcholu systémového zásobníku umístěn aktivační záznam právě prováděné procedury. Při ukončení výpočtu této procedury je její aktivační záznam zrušen a výpočet programu pokračuje bezprostředně za příkazem volání právě skončené procedury. Každé zavolání rekurzivní procedury vede k vytvoření jejího nového aktivačního záznamu a tedy také ke vzniku nové, zcela samostatné sady jejích lokálních proměnných. Proměnné každého rozpočítaného exempláře rekurzivní procedury mají stejné identifikátory a stejnou strukturu, jsou však umístěny v paměti počítače na jiném místě a mohou proto mít odlišné hodnoty. Při rekurzivním volání se zaznamená místo, z něhož byl nový exemplář procedury zavolán, a příkazy procedury se pak začnou provádět znovu od začátku. Po ukončení výpočtu procedury se zruší její aktivační záznam umístěný na vrcholu zásobníku a provede se návrat do místa, odkud byla procedura zavolána. Tam se bude pokračovat v provádění příkazů staršího exempláře procedury, a to od zaznamenaného místa, kde předtím došlo k přerušení výpočtu. Přitom se bude používat původní sada proměnných s hodnotami, které v nich byly zanechány v okamžiku rekurzivního volání. Popsaný mechanismus rekurzivních volání si můžeme ukázat na rekurzivním tvaru funkce pro výpočet faktoriálu. Funkce nemá žádné lokální proměnné, ale má jeden parametr předávaný hodnotou. Parametr představuje kladné celé číslo, jehož faktoriál chceme spočítat. Zápis funkce přesně kopíruje rekurzivní definici faktoriálu uvedenou v úvodní kapitole: function Faktorial(N: integer): integer; if N = 1 then Faktorial := 1 else Faktorial := N * Faktorial(N-1) Jestliže v hlavním programu zavoláme funkci Faktorial s parametrem rovným 3, vytvoří se v systémovém zásobníku záznam s uloženým parametrem N=3 a funkce se začne provádět. Jelikož N je různé od 1, bude zavolána funkce Faktorial s parametrem hodnoty 2. Přitom se vytvoří druhý aktivační záznam s údajem N=2 a funkce Faktorial bude prováděna opět od začátku. Znovu je N různé od 1, a proto bude funkce Faktorial zavolána potřetí, tentokrát s parametrem 1. To vede k vytvoření třetího aktivačního záznamu na zásobníku s uloženou hodnotou N=1. Při zahájení výpočtu funkce Faktorial bude tentokrát splněna rovnost N = 1 a výpočet funkce proto ihned skončí s funkční hodnotou 1. Ze zásobníku se přitom odstraní vrchní záznam a výpočet pokračuje ve druhém exempláři funkce Faktorial, v němž N=2. Pokračuje se bezprostředně za místem právě ukončeného rekurzivního volání. Spočítá se funkční hodnota rovná 2 a výpočet funkce skončí. Přitom se ze zásobníku odstraní druhý aktivační záznam a řízení se vrátí do prvního, tedy nejstaršího exempláře funkce Faktorial, v jehož záznamu je uložen údaj N=3. Tam se spočítá funkční hodnota 3 * 2 = 6 a tato výsledná hodnota je předána hlavnímu programu. Zároveň s ukončením výpočtu funkce Faktorial je zrušen i její nejstarší aktivační záznam. Při psaní rekurzivních procedur a funkcí nesmíme nikdy zapomenout na ukončení rekurze. Každé rekurzivní volání musí být vázáno na splnění nějaké podmínky. Pokud by se v proceduře objevilo bezpodmínečné rekurzivní volání sama sebe, zavolání této procedury by vedlo nutně k nekonečnému výpočtu. Procedura by stále znovu a znovu volala sama sebe a žádné její volání by nebylo nikdy ukončeno. Vzhledem k tomu, že každé zavolání procedury nebo funkce je provázeno přidělením jistého paměťového prostoru v systémovém zásobníku a že pro celý zásobník je 6

vyhrazena předem pevně vymezená oblast v paměti počítače, výpočet takovéto procedury by po jisté době skončil běhovou chybou přeplnění systémového zásobníku. Procedura by totiž stále volala sama sebe a každé takovéto zavolání by si vyžadovalo vytvoření dalšího aktivačního záznamu v zásobníku, až by jednou došlo k úplnému zaplnění té části paměťového prostoru počítače, která je pro systémový zásobník vyhrazena. Tělo rekurzivní procedury proto zpravidla zahajujeme testem vhodně zvolené podmínky, která bývá vázána na hodnoty vstupních parametrů a která zajišťuje, že pro některé hodnoty již k dalšímu rekurzivnímu zavolání nedojde. Příklad takového testu vidíme ve funkci Faktorial a uvidíme ho ještě v mnoha dalších rekurzivních procedurách a funkcích. Vedle právě popsané tzv. přímé rekurze, kdy procedura nebo funkce volá ve svém těle sama sebe, existuje ještě rekurze nepřímá. Ta spočívá v tom, že je sice také v jednom okamžiku rozpočítáno více exemplářů téže procedury, nikoli však přímým zavoláním sama sebe. Procedura A může například volat proceduru B a procedura B zase proceduru A. Tento řetězec vzájemných volání procedur nebo funkcí může být i delší (např. A volá B, B volá C, C volá D, D volá A). Ze zběžného pohledu na zápis programu tudíž nemusí být ihned zřejmé, zda a ve kterém místě programu se vyskytuje rekurze. Chceme-li použít nepřímou rekurzi v programu zapsaném programovacím jazykem Pascal, musíme vhodně zvolit pořadí deklarací procedur, mezi nimiž se vztah nepřímé rekurze uplatňuje. V Pascalu totiž platí zásada, že každý identifikátor (tedy i identifikátor procedury) může být použit (tj. procedura může být zavolána) až za místem své deklarace. Vzájemnou závislost několika procedur můžeme řešit více způsoby. Zcela obecným a přehledným prostředkem pro deklaraci procedur či funkcí ve vztahu nepřímé rekurze je direktiva forward. V programu nejprve deklarujeme samotné hlavičky podprogramů včetně jejich parametrů, místo těl podprogramů však zapíšeme pouze klíčové slovo forward. Poté mohou již bez problémů následovat deklarace celých procedur a funkcí, a to dokonce v libovolném pořadí. Díky předsunuté deklaraci forward jsou při jejich překladu známa všechna jména podprogramů, které jsou z nich nepřímou rekurzí volány. Příklad: Mějme v programu tři procedury nazvané A, B, C ve vztahu nepřímé rekurze - procedura A volá proceduru B, ta volá proceduru C a procedura C volá proceduru A i proceduru B. Zápis deklarací těchto procedur může vypadat následovně: procedure A; forward; procedure B; forward; procedure C; forward; procedure A;... B;... procedure B;... C;... 7

procedure C;... A;... B;... Ne vždy je nutné zapisovat v programu předsunuté deklarace všech procedur, mezi nimiž se uplatňuje nepřímá rekurze. Je-li ve vztahu mezi procedurami pouze cyklická závislost, což je dosti častý případ, vystačíme s jedinou předsunutou deklarací a vhodně zvoleným pořadím zápisu procedur. Opět si to ukážeme na příkladu. Příklad: Mějme v programu tři procedury nazvané A, B, C ve vztahu nepřímé rekurze - procedura A volá proceduru B, ta volá proceduru C a procedura C volá proceduru A. Pořadí deklarací procedur může být následující: procedure A; forward; procedure C;... A;... procedure B;... C;... procedure A;... B;... Někdy se stává, že mezi rekurzivními procedurami je pouze cyklická závislost a navíc z vnějšku (např. z hlavního programu) je volána pouze jediná z těchto procedur. V takovém případě direktivu forward vůbec nepotřebujeme, stačí deklarovat všechny procedury ve vhodném pořadí jako lokální uvnitř té z nich, která je používána z vnějšku. Příklad: Mějme v programu tři procedury nazvané A, B, C ve vztahu nepřímé rekurze - procedura A volá proceduru B, ta volá proceduru C a procedura C volá proceduru A. Z hlavního programu je volána pouze procedura A. Soustavu těchto tří procedur můžeme deklarovat následovně: 8

procedure A; procedure C;... A; {smí volat, A je pro ni globální symbol}... procedure B;... C; {smí volat, C bylo deklarováno dříve}... {A}... B; {smí volat, B je její lokální procedura}... {A} S praktickým využitím nepřímé rekurze se v programech setkáváme o dost méně, než s rekurzí přímou. V této knize si předvedeme alespoň dva ukázkové programy založené na technice nepřímé rekurze. Půjde jednak o program na vykreslování Hilbertových křivek, o nichž jsme se již zmínili v úvodu, jednak o algoritmus vyhodnocování aritmetických výrazů. Oba tyto příklady užití nepřímé rekurze najdete hned v následující kapitole. 9

3. Rekurzivní algoritmy Pokusíme se nyní ukázat alespoň některé základní oblasti, kde se při návrhu algoritmů využívá rekurze a kde je její použití přirozené a výhodné. Jednotlivé rekurzivní postupy řešení úloh budeme demonstrovat na konkrétních příkladech. Nejprve si předvedeme algoritmy odvozené z různých matematických rekurzivních předpisů, dále rekurzivní řešení úloh typu nalézt všechny prvky jisté vlastnosti a s tím související algoritmy na prohledávání s návratem. Závěrečný oddíl této kapitoly je věnován rekurzivnímu postupu nazývanému rozděl a panuj. V následující čtvrté kapitole se seznámíme se základními rekurzivně definovanými datovými strukturami a také s algoritmy pro jejich zpracování. 3.1. Matematické rekurzivní vzorce Některé matematické objekty bývají definovány nebo popsány rekurzivními předpisy. Jako typický příklad nám poslouží třeba definice číselných posloupností, v nichž je n-tý člen posloupnosti a n popsán pomocí jednoho nebo více předcházejících členů. S několika konkrétními příklady takových posloupností jsme se již setkali v první kapitole, kde jsme uvedli rekurzivní definici posloupnosti lichých čísel, faktoriálu a Fibonacciho čísel. Při návrhu algoritmu pro výpočet n-tého členu rekurzivně definované posloupnosti máme dvě možnosti. Nejpohodlnější cestou bývá přímé přepsání rekurzivního předpisu do podoby rekurzivní funkce programovacího jazyka. To jsme si již ukázali v kap. 2 pro případ výpočtu faktoriálu. Z hlediska průběhu výpočtu bývá ale výhodnější postupovat jinak. Pokud se nám podaří odvodit z rekurzivní definice posloupnosti explicitní vzorec vyjadřující hodnotu členu a n pouze v závislosti na n, dostaneme rychlejší a mnohdy i paměťově méně náročný algoritmus. Buď se výpočet řádově zrychlí, nebo se alespoň odstraní zbytečná rekurzivní volání funkce, která sama o sobě vedou k dodatečným časovým i paměťovým nárokům vytvořeného programu. Jednoduchým příkladem demonstrujícím zrychlení výpočtu je již zmíněná posloupnost lichých čísel. Výpočet n-tého členu posloupnosti na základě rekurzivní definice a 1 = 1 a n = a n-1 + 2 pro n > 1 má lineární časovou složitost, zatímco při použití explicitního vzorce a n = 2n - 1 pro n > 0 dosáhneme složitosti konstantní. Jako ukázka úspory zbytečných rekurzivních volání nám poslouží faktoriál. Explicitní vzorec n! = 1.2.3....n pro n > 0 vede stejně jako rekurzivní definice faktoriálu k výpočtu s lineární časovou složitostí. Je však rychlejší o ušetřená rekurzivní volání funkce. function Faktorial(N: integer): integer; var I, F: integer; F := 1; for I:=2 to N do F := F * I; Faktorial := F 10

Rekurzivní jsou i některé geometrické předpisy. V kap. 1 jsme uvedli definici tzv. Hilbertovy křivky k-tého řádu, která je popsána složením čtyř křivek řádu o 1 nižšího. Ukážeme si nyní program pro grafické vykreslení Hilbertovy křivky daného řádu na obrazovce počítače. Program je založen na bezprostřední realizaci daného rekurzivního předpisu. Je zároveň hezkou ukázkou použití nepřímé rekurze. Různě natočené segmenty vytvářené křivky jsou vykreslovány samostatnými procedurami, které se podle potřeby navzájem volají. program Hilbert; {Vykreslení Hilbertovy křivky zvoleného řádu} uses Graph; var N: integer; {řád křivky} Gd, Gm: integer; {nastavení režimu grafické karty} Max: integer; {rozlišení grafické karty} H: integer; {velikost úsečky} Z: integer; {počet úseček na straně oblasti} I: integer; procedure UseckaDolu(H: integer); {kreslí úsečku směrem dolů délky H bodů} LineRel(0,H) procedure UseckaNahoru(H: integer); {kreslí úsečku směrem nahoru délky H bodů} LineRel(0,-H) procedure UseckaVlevo(H: integer); {kreslí úsečku směrem vlevo délky H bodů} LineRel(-H,0) procedure UseckaVpravo(H: integer); {kreslí úsečku směrem vpravo délky H bodů} LineRel(H,0) procedure ObloukDolu (Rad, H: integer); forward; procedure ObloukNahoru(Rad, H: integer); forward; procedure ObloukVlevo (Rad, H: integer); forward; procedure ObloukVpravo(Rad, H: integer); forward; procedure ObloukDolu(Rad, H: integer); {kreslí úsek H. křivky - oblouk dolů pravotočivý} {Rad - řád Hilbertovy křivky, H - velikost hrany} var R: integer; {o jeden řád méně} if Rad > 0 then 11

R := Rad - 1; ObloukVlevo(R,H); UseckaDolu(H); ObloukDolu(R,H); UseckaVlevo(H); ObloukDolu(R,H); UseckaNahoru(H); ObloukVpravo(R,H); {procedure ObloukDolu} procedure ObloukNahoru(Rad, H: integer); {kreslí úsek H. křivky - oblouk nahoru pravotočivý} {Rad - řád Hilbertovy křivky, H - velikost hrany} var R: integer; {o jeden řád méně} if Rad > 0 then R := Rad - 1; ObloukVpravo(R,H); UseckaNahoru(H); ObloukNahoru(R,H); UseckaVpravo(H); ObloukNahoru(R,H); UseckaDolu(H); ObloukVlevo(R,H); {procedure ObloukNahoru} procedure ObloukVlevo(Rad, H: integer); {kreslí úsek H. křivky - oblouk vlevo levotočivý} {Rad - řád Hilbertovy křivky, H - velikost hrany} var R: integer; {o jeden řád méně} if Rad > 0 then R := Rad - 1; ObloukDolu(R,H); UseckaVlevo(H); ObloukVlevo(R,H); UseckaDolu(H); ObloukVlevo(R,H); UseckaVpravo(H); ObloukNahoru(R,H); {procedure ObloukVlevo} procedure ObloukVpravo(Rad, H: integer); {kreslí úsek H. křivky - oblouk vpravo levotočivý} {Rad - řád Hilbertovy křivky, H - velikost hrany} var R: integer; {o jeden řád méně} if Rad > 0 then R := Rad - 1; 12

ObloukNahoru(R,H); UseckaVpravo(H); ObloukVpravo(R,H); UseckaNahoru(H); ObloukVpravo(R,H); UseckaVlevo(H); ObloukDolu(R,H); {procedure ObloukVpravo} write('řád Hilbertovy křivky: '); readln(n); Gd := Detect; Gm := Detect; InitGraph(Gd, Gm, ''); Max := GetMaxY; {počet bodů na obrazovce na výšku} {počet úseček na straně oblasti pro N-tý řád: 2^N-1} Z := 1; for I:=1 to N do Z := Z * 2; Z := Z - 1; {Z = počet úseček tvořících stranu} H := Max div Z; {délka jedné úsečky} MoveTo(Max,0); ObloukVlevo(N,H); readln; CloseGraph;. Jiným typickým postupem využívajícím vztahu nepřímé rekurze je jeden z algoritmů na vyhodnocení aritmetického výrazu. Budeme mít zadán aritmetický výraz, který bude pro jednoduchost tvořen pouze celočíselnými konstantami, binárními operátory +, -, *, / (znak / bude představovat celočíselné dělení) a kulatými závorkami s možností libovolného vnoření do sebe. Naším úkolem bude spočítat hodnotu tohoto výrazu. Pro řešení této úlohy existuje řada různých postupů (viz např. [5]). Jeden z nejlepších algoritmů s optimální lineární časovou složitostí je založen na rekurzivní definici aritmetického výrazu. Místo přesné formální definice si řekneme raději její hlavní myšlenky. Výraz je buď člen, nebo je to součet (příp. rozdíl) několika členů. Každý člen je buď faktor, nebo je to součin (příp. podíl) několika faktorů. Každý faktor je buď tvořen přímo celočíselnou konstantou, nebo je to výraz uzavřený v kulatých závorkách. V této definici je zachycen jak správný tvar zápisu výrazu, tak také postup správného vyhodnocování s ohledem na strukturu závorek a prioritu operátorů (násobení a dělení má vyšší prioritu než sčítání a odčítání). Algoritmus si ukážeme naprogramovaný v Turbo Pascalu ve tvaru funkce Vyhodnoceni. Funkce dostane ve svém parametru znakový řetězec obsahující vyhodnocovaný výraz a jako svou funkční hodnotu vrátí hodnotu tohoto výrazu. Funkce Vyraz, Clen a Faktor, které se navzájem volají metodou nepřímé rekurze, jsou lokálními funkcemi deklarovanými uvnitř funkce Vyhodnoceni. Jako jediný parametr typu řetězec si předávají tu část vyhodnocovaného výrazu, která ještě zbývá ke zpracování. Řetězec je zleva stále zkracován, až z něj zbude pouze jediný speciální znak $, který k původně zadanému výrazu připojuje z technických důvodů sama funkce Vyhodnoceni. program AritmetVyraz; {Vyhodnocení aritmetického výrazu soustavou procedur ve vztahu nepřímé rekurze podle formální gramatiky 13

popisující stavbu výrazu} var S: string; {uložení vyhodnocovaného výrazu} function Vyhodnoceni(S:string): integer; {funkce vyhodnocující aritmetický výraz} {metoda - nepřímá rekurze funkcí Vyraz, Clen, Faktor} {vyhodnocovaný výraz je zadán ve vstupním parametru S} {funkce předpokládá, že výraz je syntakticky správný} function Vyraz(var S: string): integer; forward; function Faktor(var S: string); integer; {pomocná funkce na vyhodnocení jednoho faktoru} {faktorem je číselná hodnota nebo výraz v závorkách} var H: integer; {číslo ve výrazu} Z: boolean; {znaménko minus u čísla} while S[1] = ' ' do Delete(S,1,1); if S[1] = '(' then Delete(S,1,1); {zrušit levou závorku} Faktor := Vyraz(S); while S[1] = ' ' do Delete(S,1,1); Delete(S,1,1); {zrušit pravou závorku} else {číselná konstanta} Z := false; if S[1] = '+' then Delete(S,1,1) else if S[1] = '-' then Delete(S,1,1); Z := true H := 0; while S[1] in ['0'..'9'] do H := H * 10 + ord(s[1]) - ord('0'); Delete(S,1,1) if Z then H := -H; Faktor := H {function Faktor} function Clen(var S: string): integer; {pomocná funkce na vyhodnocení jednoho členu} {členem je jeden faktor nebo součin/podíl více faktorů} var C: integer; {hodnota členu} C := Faktor(S); while S[1] = ' ' do Delete(S,1,1); while S[1] in ['*','/'] do if S[1] = '*' then {součin faktorů} 14

Delete(S,1,1); C := C * Faktor(S); while S[1] = ' ' do Delete(S,1,1); else if S[1] = '/' then {podíl faktorů} Delete(S,1,1); C := C div Faktor(S); while S[1] = ' ' do Delete(S,1,1); Clen := C {function Clen} function Vyraz(var S: string): integer; {funkce na vyhodnocení výrazu} {výraz je člen nebo součet/rozdíl členů} var V: integer; {hodnota výrazu} {function Vyraz} while S[1] = ' ' do Delete(S,1,1); V := Clen(S); while S[1] = ' ' do Delete(S,1,1); while S[1] in ['+','-'] do if S[1] = '+' then {součet členů} Delete(S,1,1); V := V + Clen(S); while S[1] = ' ' do Delete(S,1,1); else if S[1] = '-' then {rozdíl členů} Delete(S,1,1); V := V - Clen(S); while S[1] = ' ' do Delete(S,1,1); Vyraz := V {function Vyraz} {function Vyhodnoceni} S := S + '$'; {technický trik pro ukončení} Vyhodnoceni := Vyraz(S) {function Vyhodnoceni} write('vyhodnocovaný výraz: '); readln(s); writeln('hodnota výrazu: ', Vyhodnoceni(S)); readln. 15

3.2. Generování všech prvků dané vlastnosti Při řešení některých úloh je zapotřebí vygenerovat všechny prvky, k-tice, množiny či rozklady zadané vlastnosti. Úlohy tohoto typu musíme často řešit prostým zkoušením všech možností. Například při hledání všech k-tic celých čísel splňujících nějakou danou podmínku by však bylo nevýhodné a zbytečně pomalé vytvořit nejprve mechanicky všechny existující k-tice čísel a každou z nich pak dodatečně testovat, zda vyhovuje podmínce ze zadání úlohy. Lepší je vytvářet přímo pouze vyhovující k-tice. K tomu obvykle použijeme rekurzivní proceduru, která při svém prvním zavolání postupně umístí na první místo ve vytvářené k-tici všechna čísla, která se tam mohou objevit, a pro každý takovýto začátek zajistí dokončení celé k-tice pomocí rekurzivního volání sebe sama. Parametrem rekurzivního volání bude údaj, od kolikátého prvku je třeba pokračovat ve vytváření k-tice. Součástí algoritmu procedury musí být samozřejmě i podmínka pro ukončení rekurze. Jakmile bude ve vytvářené k-tici zvolena hodnota posledního, tj. k-tého prvku, procedura místo dalšího rekurzivního volání nalezenou k-tici předá jako výsledek (někam ji uloží nebo třeba přímo vytiskne). Celý postup si předvedeme na několika konkrétních úlohách. Začneme příklady z kombinatoriky. Následující programy slouží k vypsání všech k-prvkových kombinací bez opakování z N-prvkové množiny celých čísel {1, 2,..., N}, dále kombinací s opakováním, variací bez opakování a variací s opakováním. Připomeňme si ještě ve stručnosti význam těchto základních kombinatorických pojmů. K-prvkové kombinace z N prvků jsou všechny k-prvkové podmnožiny dané základní množiny {1, 2,..., N}, zatímco k-prvkové variace z N prvků jsou všechny uspořádané k-tice tvořené prvky této základní množiny. U variací tedy na rozdíl od kombinací záleží na pořadí prvků. Slova bez opakování a s opakováním udávají, zda se v kombinaci či variaci mohou některé prvky opakovat. Příklad: Všechny dvouprvkové kombinace, resp. variace, ze čtyř prvků vypadají následovně. kombinace bez opakování: {1,2} {1,3} {1,4} {2,3} {2,4} {3 4} kombinace s opakováním: {1,1} {1,2} {1,3} {1,4} {2,2} {2,3} {2,4} {3,3} {3 4} {4,4} variace bez opakování: (1,2) (1,3) (1,4) (2,1) (2,3) (2,4) (3,1) (3,2) (3,4) (4,1) (4,2) (4,3) variace s opakováním: (1,1) (1,2) (1,3) (1,4) (2,1) (2,2) (2,3) (2,4) (3,1) (3,2) (3,3) (3,4) (4,1) (4,2) (4,3) (4,4) program KombinaceBezOpakovani; {vypíše všechny K-prvkové kombinace bez opakování z N prvků (1,2,...,N) pro zadané hodnoty K, N} const MaxK = 20; {maximální přípustné K} var C: array[0..maxk] of byte; {uložení kombinace} N, K: byte; procedure Comb(p:byte); {p - pořadí vytvářeného prvku kombinace} {procedura používá globální proměnné C, K, N} {kombinace jsou vytvářeny s prvky vzestupně uspořádanými} var i:byte; if p > K then {hotovo} for i:= 1 to K do write(c[i]:3); writeln else {doplnit C[p]} 16

for i:=c[p-1]+1 to N-(K-p) do C[p] := i; Comb(p+1) write('výpočet K-prvkových kombinací bez opakování '); writeln('z N prvků'); write('zadejte hodnoty K a N: '); readln(k,n); C[0]:=0; {technický trik} Comb(1); readln;. program KombinaceSOpakovanim; {vypíše všechny K-prvkové kombinace s opakováním z N prvků (1,2,...,N) pro zadané hodnoty K, N} const MaxK = 20; {maximální přípustné K} var C: array[0..maxk] of byte; {uložení kombinace} N, K: byte; procedure Comb(p:byte); {p - pořadí vytvářeného prvku kombinace} {procedura používá globální proměnné C, K, N} {kombinace jsou vytvářeny s prvky vzestupně uspořádanými} var i:byte; if p > K then {hotovo} for i:= 1 to K do write(c[i]:3); writeln else {doplnit C[p]} for i:=c[p-1] to N do C[p] := i; Comb(p+1) write('výpočet K-prvkových kombinací s opakováním '); writeln('z N prvků'); write('zadejte hodnoty K a N: '); readln(k,n); C[0]:=1; {technický trik} Comb(1); readln;. 17

program VariaceBezOpakovani; {vypíše všechny K-prvkové variace bez opakování z N prvků (1,2,...,N) pro zadané hodnoty K, N} const MaxK = 20; {maximální přípustné K} var C: array[1..maxk] of byte; {uložení variace} N, K: byte; procedure Vari(p:byte); {p - pořadí vytvářeného prvku variace} {procedura používá globální proměnné C, K, N} var i, j: byte; nalez: boolean; if p > K then {hotovo} for i:= 1 to K do write(c[i]:3); writeln else {doplnit C[p]} for i:=1 to N do nalez := false; j:=1; while not nalez and (j < p) do if C[j] = i then nalez := true else j := j + 1 if not nalez then C[p] := i; Vari(p+1) write('výpočet K-prvkových variací bez opakování '); writeln('z N prvků'); write('zadejte hodnoty K a N: '); readln(k,n); Vari(1); readln;. program VariaceSOpakovanim; {vypíše všechny K-prvkové variace s opakováním z N prvků (1,2,...,N) pro zadané hodnoty K, N} const MaxK = 20; {maximální přípustné K} var C: array[1..maxk] of byte; {uložení variace} N, K: byte; procedure Vari(p:byte); {p - pořadí vytvářeného prvku variace} 18

{procedura používá globální proměnné C, K, N} var i, j: byte; if p > K then {hotovo} for i:= 1 to K do write(c[i]:3); writeln else {doplnit C[p]} for i:=1 to N do C[p] := i; Vari(p+1) write('výpočet K-prvkových variací s opakováním '); writeln('z N prvků'); write('zadejte hodnoty K a N: '); readln(k,n); Vari(1); readln;. Dalším základním kombinatorickým pojmem jsou permutace. Permutací N-prvkové množiny čísel {1, 2,..., N} rozumíme každé seřazení jejích prvků do uspořádané N-tice. Existuje přesně N! různých permutací dané N-prvkové množiny. Chceme-li je všechny vypsat, máme dvě základní možnosti, jak lze postupovat. První řešení je rekurzivní a vychází ze skutečnosti, že permutace N prvků jsou vlastně N-prvkové variace z daných N prvků bez opakování. Můžeme proto postupovat stejně jako při hledání variací. program Permutace_1; {vypíše všechny permutace N prvků (1,2,...,N) } {rekurzivní verze - postupné generování} (* uses Dos; *) const MaxN = 20; {maximální přípustné N} type Cisla = set of 1..MaxN; var A: array[1..maxn] of 1..MaxN; {uložení permutace} N: integer; {počet prvků} Pocet: integer; {počet permutací} (* procedure WriteTime; {Pomocná procedura pro výpis času} var H,M,S,S100: word; GetTime(H,M,S,S100); writeln('čas: ',H:4,M:4,S:4,S100:4) {procedure WriteTime} *) 19

procedure Perm(p: integer; S: Cisla); {p - pořadí vytvářeného prvku permutace} {S - čísla dosud nepoužitá v permutaci} {procedura používá globální proměnné A, N, Pocet} var i: integer; if p > N then {hotovo} for i:= 1 to N do write(a[i]:3); writeln; Pocet := Pocet + 1 else {doplnit A[p]} for i:=1 to N do if not (i in S) then A[p] := i; Perm(p+1, S+[i]) {procedure Perm} writeln('výpočet permutací N prvků'); write('zadejte hodnotu N: '); readln(n); Pocet := 0; (* WriteTime; *) Perm(1,[]); writeln('počet permutací: ', Pocet); (* WriteTime; *) readln;. Druhý, nerekurzivní postup řešení je o něco šikovnější a několikanásobně rychlejší. Místo postupného mechanického zkoušení všech možných rozložení prvků permutace pomocí rekurze budeme vytvářet přímo celé jednotlivé permutace, a to v tzv. lexikografickém uspořádání. Lexikografické uspořádání permutací je takové pořadí, které známe z řazení hesel ve slovníku. Ze dvou permutací stojí dříve ta, která má menší číslo na první pozici, v případě shody na první pozici rozhoduje menší číslo na druhém místě v permutaci zleva, atd. Například pro N=3 vypadá lexikografické uspořádání všech permutací takto: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1 První v pořadí je permutace s prvky uspořádanými vzestupně, poslední permutace má prvky seřazené v sestupném pořadí. Algoritmus na vypsání všech permutací N prvků využívá proceduru, která ze zadané permutace N prvků vytvoří permutaci bezprostředně po ní následující v lexikografickém uspořádání. Začneme tedy od první permutace a touto procedurou ji budeme přetvářet tak dlouho, dokud nezískáme permutaci poslední. Zbývá vysvětlit, jak pracuje zmíněná procedura na nalezení bezprostředně následující permutace. Snaží se zvýšit zadanou permutaci, ale jen o co nejméně, jak je to možné. Změna se proto musí 20

odehrát v permutaci co nejvíce vpravo. Budeme tedy postupovat od pravého konce permutace a 1 a 2...a N směrem doleva a porovnávat dvojice sousedních prvků, tzn. postupně dvojice čísel (a N-1 a N ), (a N-2 a N-1 ), atd. Dokud je levý prvek takové dvojice větší než pravý, nemůžeme zatím pro zvýšení permutace nic udělat a postupujeme dál vlevo. Jakmile najdeme poprvé dvojici (a i a i+1 ), v níž a i <a i+1, průchod permutací zastavíme. Prvek a i je totiž konečně tím, který můžeme zvýšit. Hledáme nejbližší vyšší permutaci, a proto musíme a i nahradit nejbližším vyšším z prvků a i+1, a i+2,..., a N. Nechť je to a k. Zaměníme tedy v permutaci prvky a i, a k. Zbývá již jen uspořádat vzestupně prvky stojící vpravo od i-té pozice, aby vytvořená permutace s novou hodnotou a i byla co nejmenší. Nemusíme ale čísla pracně třídit, neboť víme, že platí a i+1 >a i+2 >...>a N-1 >a N. Na tomto uspořádání nic nepokazila ani výměna prvků a i, a k, neboť za a k bylo vybráno nejbližší vyšší číslo než bývalé a i. Stačí tedy jednoduše obrátit pořadí prvků a i+1, a i+2,..., a N a jsme hotovi. program Permutace_2; {vypíše všechny permutace N prvků (1,2,...,N) } {nerekurzivní verze - pomocí následníka v lexik. uspořádání} (* uses Dos; *) const MaxN = 20; {maximální přípustné N} type Pole = array[1..maxn] of 1..MaxN; var A: Pole; {uložení permutace} N: integer; {počet prvků} Pocet: integer; {počet permutací} i: integer; (* procedure WriteTime; {Pomocná procedura pro výpis času} var H,M,S,S100: word; GetTime(H,M,S,S100); writeln('čas: ',H:4,M:4,S:4,S100:4) {procedure WriteTime} *) function Dalsi (var P: Pole): boolean; {z permutace v poli P vytvoří nejbližší další v lexikografickém uspořádání} {vrací true, když se to podařilo vrací false, pokud P obsahovalo nejvyšší permutaci} {používá globální proměnné N, Pocet} var i, j, k, x: integer; i := N-1; while (i > 1) and (A[i] > A[i+1]) do i := i-1; if A[i] > A[i+1] then Dalsi := false else {číslo A[i] zvětšíme - nahradíme nejbližším vyšším číslem z úseku od A[i+1] do A[N]:} j := N; while A[j] < A[i] do j := j-1; 21

{vyměníme A[i] a A[j]:} x := A[i]; A[i] := A[j]; A[j] := x; {otočíme klesající úsek A[i+1]..A[N] na rostoucí:} j := i+1; k := N; while j < k do x := A[j]; A[j] := A[k]; A[k] := x; j := j+1; k := k-1; Dalsi := true {function Dalsi} writeln('výpočet permutací N prvků'); write('zadejte hodnotu N: '); readln(n); Pocet := 0; for i:=1 to N do A[i] := i; (* WriteTime; *) repeat for i:=1 to N do write(a[i]:3); writeln; Pocet := Pocet + 1 until not Dalsi(A); writeln('počet permutací: ', Pocet); (* WriteTime; *) readln;. Také v následujících příkladech půjde o vytváření všech skupin zadané vlastnosti. Mějme dáno N celých čísel bez znaménka, kde N není větší než 100. Dále je dáno celé číslo C. Naším úkolem je doplnit k zadaným číslům znaménka + nebo - tak, aby byl jejich součet roven číslu C. Chceme nalézt všechna přípustná řešení. Postup řešení je podobný jako u výše uvedených kombinatorických úloh. V jednom poli budeme mít pevně uložena sčítaná čísla, do druhého pole si budeme postupně ukládat jejich znaménka. Vytvoříme všechny možné N-tice za znamének + a - a pro každou z nich otestujeme odpovídající součet čísel. Sekvence znamének vytváříme rekurzivně. Jedno zavolání rekurzivní procedury má na starosti volbu znaménka u jednoho z čísel. Vyzkouší obě možnosti znaménka a pro každou z nich nechá prozkoumat všechna rozložení zbývajících znamének pomocí rekurzivního volání. Parametrem procedury je tudíž pořadové číslo umisťovaného znaménka. Ukončení rekurze je zajištěno kontrolou hodnoty tohoto parametru, zda nepřekročila N. Po vytvoření celé N-tice znamének je již možné projít pole čísel a znamének a zkontrolovat výsledný součet. Jinou stejně dobrou možností je předávat průběžný mezisoučet čísel s již stanovenými znaménky ve druhém parametru rekurzivní procedury. program Soucet_Cisel_V_Poli; {Je dáno N celých čísel a požadovaný součet C. Program doplní před každé z čísel znaménko + nebo - tak, aby byl součet takto upravených čísel roven danému C. Hledáme všechna možná řešení.} const MaxN = 100; {maximální počet čísel} 22

var H: array[1..maxn] of integer; {sčítaná čísla} N: integer; {počet čísel} C: integer; {hledaný součet} Z: array[1..maxn] of char; {uložení znamének} I: integer; procedure X (K:integer; Soucet:integer); {K - kolikáté znaménko hledáme, Soucet - průběžný součet čísel až do K-tého} {Procedura používá globální proměnné N, C, H, Z} var I:integer; if K = N+1 then {pole znamének zcela obsazeno} if Soucet = C then {našli jsme řešení} for I:=1 to N do write(z[i],h[i]); writeln else {zkusíme hodnoty K-tého znaménka} Z[K] := '+'; X(K+1, Soucet+H[K]); Z[K] := '-'; X(K+1, Soucet-H[K]); {procedure X} write('počet sčítaných čísel: '); readln(n); writeln('sčítaná čísla: '); for I:=1 to N do read(h[i]); write('požadovaný součet: '); readln(c); X(1, 0); {začínáme od prvního znaménka, dosud součet 0} readln;. V další úloze budeme studovat, jak mohou vypadat uzávorkování správně zapsaných aritmetických výrazů. Uvažujeme výraz obsahující N párů závorek. Z celého výrazu nás bude zajímat právě jen to, jaký tvar může mít struktura jeho závorek. Například pro N=3 existuje těchto pět možností: ((())) (()()) (())() ()(()) ()()() 23

Naším úkolem bude nalézt a vypsat všechna takováto správná uzávorkování výrazu pro zadaný počet párů závorek N. Úlohu budeme řešit opět pomocí rekurze. Posloupnosti závorek budeme vytvářet postupně zleva doprava. Procedura řešící úlohu převezme již vytvořenou úvodní část posloupnosti, prodlouží ji o jednu závorku a pak zajistí dokončení celé posloupnosti rekurzivním zavoláním sebe sama. Hloubka rekurze je omezena délkou vytvářené posloupnosti. Při každém rekurzivním zavolání je posloupnost prodloužena o jednu závorku, takže po provedení 2N volání v sobě může procedura místo dalšího rekurzivního volání přímo vypsat výsledek. Musíme se ale ještě vrátit ke slovům prodlouží ji o jednu závorku a zamyslet se, jakou závorku je možné k nějakému úvodnímu úseku posloupnosti přidat. V některých situacích je totiž možné přidat jedině levou závorku, v některých pouze pravou a v některých musíme vyzkoušet obě možnosti, chceme-li nalézt všechna přípustná řešení úlohy. Levou závorku můžeme připojit vždy, pokud úvodní úsek ještě neobsahuje N levých závorek. Pravou závorku lze použít tehdy, jestliže je co uzavírat, tzn. jestliže úvodní úsek obsahuje více levých závorek než pravých. Všechno ostatní je již jenom technickou záležitostí. Vytvářenou posloupnost závorek musíme podobně jako při řešení předchozí úlohy ukládat do pole. Toto pole musí být všemi exempláři procedury sdíleno, a proto ho budeme deklarovat jako globální (mohlo by stát také na místě parametru předávaného odkazem). Prostřednictvím vstupního parametru se musí procedura dozvědět, jak dlouhý úsek posloupnosti je již vytvořen a uložen v poli. Přímo v poli by pak bylo možné spočítat, kolik je v tomto úseku levých a kolik pravých závorek. Šikovnější je však zavést parametry jinak a přímo v parametrech předávat proceduře informaci, kolik levých a kolik pravých závorek úvodní úsek obsahuje. Ušetříme tím práci spojenou s opakovaným procházením pole při každém zavolání procedury. program Uzavorkovani; {Vygeneruje všechna správná uzávorkování výrazu pomocí N párů kulatých závorek} const MaxN = 100; {maximální přípustné N} var Z: array[1..maxn*2] of char; {uložení závorek} N: integer; {počet párů závorek} I: integer; procedure Zavorky(L, P: integer); {L, P - kolik jsme už umístili levých a pravých závorek} {používá globální proměnnou N} if L < N then {můžeme dát levou} Z[L+P+1] := '('; Zavorky(L+1,P) if P < L then {můžeme dát pravou} Z[L+P+1] := ')'; Zavorky(L,P+1) if P = N then {uzávorkování vytvořeno} for I:=1 to 2*N do write(z[i]); writeln {procedure Zavorky} 24

write('počet párů závorek: '); readln(n); Zavorky(0,0); readln;. Další ukázková úloha pojednává o placení. Úkolem je nalézt všechny způsoby, jimiž je možné zaplatit danou částku pomocí dané sady platidel. Předpokládáme, že od každé hodnoty platidla máme v zásobě dostatečný počet kusů. Úlohu si nejlépe přiblížíme na konkrétním příkladu. Budeme uvažovat platidla o hodnotách 1 Kč, 2 Kč, 5 Kč, 10 Kč, 20 Kč a 50 Kč. Částku 12 Kč můžeme zaplatit těmito patnácti způsoby: 1 x 10 Kč + 1 x 2 Kč 1 x 10 Kč + 2 x 1 Kč 2 x 5 Kč + 1 x 2 Kč 2 x 5 Kč + 2 x 1 Kč 1 x 5 Kč + 3 x 2 Kč + 1 x 1 Kč 1 x 5 Kč + 2 x 2 Kč + 3 x 1 Kč 1 x 5 Kč + 1 x 2 Kč + 5 x 1 Kč 1 x 5 Kč + 7 x 1 Kč 6 x 2 Kč 5 x 2 Kč + 2 x 1 Kč 4 x 2 Kč + 4 x 1 Kč 3 x 2 Kč + 6 x 1 Kč 2 x 2 Kč + 8 x 1 Kč 1 x 2 Kč + 10 x 1 Kč 12 x 1 Kč Úlohu je možné řešit různými způsoby. Budeme-li uvažovat pouze pevnou sadu platidel nebo alespoň sadu platidel o předem známém počtu různých hodnot, můžeme napsat řešení založené na příslušném počtu cyklů vnořených do sebe. Pokud pracujeme s N platidly, vystačíme s N-1 cykly. Víme-li navíc, že je mezi platidly vždy jednokoruna, takže libovolnou celou částku bude možné zaplatit, budeme v těchto cyklech zkoumat všechny přípustné počty vyšších platidel, uvnitř cyklů pak doplatíme zbytek částky jednokorunami a vytiskneme výsledek. Pokud by nebylo zaručeno, že jednokoruna bude v sadě platidel obsažena, museli bychom do vnitřního cyklu doplnit navíc jeden test pro kontrolu, zda má úloha řešení. program Platidla_1; {Zaplatit danou částku všemi způsoby danou sadou platidel. Nerekurzivní verze s pevným počtem platidel. První hodnotou platidla je vždy 1, dalších pět hodnot platidel je zadáno na vstupu.} var H1, H2, H3, H4, H5, H6: integer; {hodnoty platidel} P1, P2, P3, P4, P5, P6: integer; {počty platidel} 25

Z1, Z2, Z3, Z4, Z5, Z6: integer; {zbývá zaplatit} Zapl: integer; {počet různých zaplacení} writeln('nejmenší platidlo hodnoty 1 se nezadává'); H1 := 1; write('hodnoty dalších 5 platidel: '); readln(h2, H3, H4, H5, H6); write('částka k zaplacení: '); readln(z6); Zapl := 0; for P6:=0 to Z6 div H6 do Z5 := Z6 - P6 * H6; for P5:=0 to Z5 div H5 do Z4 := Z5 - P5 * H5; for P4:=0 to Z4 div H4 do Z3 := Z4 - P4 * H4; for P3:=0 to Z3 div H3 do Z2 := Z3 - P3 * H3; for P2:=0 to Z2 div H2 do Z1 := Z2 - P2 * H2; P1 := Z1; {neboť H1 = 1} write(p1,' x ',H1,' + ',P2,' x ',H2,' + '); write(p3,' x ',H3,' + ',P4,' x ',H4,' + '); writeln(p5,' x ',H5,' + ',P6,' x ',H6); Zapl := Zapl + 1; writeln('počet možných zaplacení: ', Zapl); readln;. Právě popsané řešení s velkým množstvím cyklů vnořených do sebe není zrovna nejelegantnější. Nemůžeme ho navíc použít v případě, je-li úloha zadána zcela obecně a předem neznáme počet druhů platidel, s nimiž je třeba pracovat. V této situaci nám opět pomůže rekurze. Každé zavolání procedury bude odpovídat jednomu z platidel, hloubka rekurze bude tedy omezena počtem platidel. Procedura vyzkouší v cyklu všechny vhodné počty toho platidla, které má na starosti, a pro každý z těchto počtů zajistí doplacení zbývající částky dalšími platidly pomocí rekurzivního volání. Údaje o počtech platidel jednotlivých druhů v právě vytvářeném rozkladu se budou opět ukládat do globálního pole. Parametry procedury budou pořadové číslo právě zkoumaného platidla a částka, která ještě zbývá k zaplacení. Ukážeme si dvě varianty řešení. První z nich je jednodušší a předpokládá, že každá použitá sada platidel obsahuje jednokorunu. Druhé řešení je zcela obecné. program Platidla_2; 26

{Zaplatit danou částku všemi způsoby danou sadou platidel. Rekurzivní verze s obecným počtem platidel. První hodnotou platidla je vždy 1, další platidla jsou dána na vstupu.} const Max = 100; {maximální počet platidel} var H: array[1..max] of integer; {hodnoty platidel} P: array[1..max] of integer; {počty platidel} N: integer; {počet platidel} Castka: integer; {částka k zaplacení} Zapl: integer; {počet různých zaplacení} i: integer; procedure Plat(J, Z: integer); {J - index právě používaného platidla Z- zbývá ještě zaplatit} {používá globální proměnné H, P, N} var i: integer; if J = 1 then P[1] := Z; {neboť H[1]=1} for i:=1 to N-1 do write(p[i],' x ',H[i],' + '); writeln(p[n],' x ',H[N]); Zapl := Zapl + 1; else for i:=0 to Z div H[J] do {i = počet použitých platidel indexu J} P[J] := i; Plat(J-1, Z-i*H[J]) {procedure Plat} write('počet platidel: '); readln(n); writeln('nejmenší platidlo hodnoty 1 se nezadává'); H[1] := 1; write('hodnoty dalších ', N-1, ' platidel: '); for i:=2 to N do read(h[i]); write('částka k zaplacení: '); readln(castka); Zapl := 0; Plat(N, Castka); writeln('počet možných zaplacení: ', Zapl); readln;. program Platidla_3; {Zaplatit danou částku všemi způsoby danou sadou platidel. Rekurzivní verze s obecným počtem platidel. Sada platidel nemusí obsahovat platidlo s hodnotou 1.} const Max = 100; {maximální počet platidel} var H: array[1..max] of integer; {hodnoty platidel} 27