Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


Prednáška 11: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
 
(27 medziľahlých úprav od 2 ďalších používateľov nie je zobrazených)
Riadok 1: Riadok 1:
 
== Oznamy ==
 
== Oznamy ==
  
* DÚ2 je zverejnená, odovzdávajte do piatka 13.11. 22:00
+
* DÚ2 bude zverejnená po prednáške, odovzdávajte do utorka 19.11. 22:00
* Nezabudnite, že ak z cvičení za týždeň neodovzdáte úspešne ani jeden príklad, dostanete -5 bodov. Takisto je vhodné nazbierať z DÚ a cvičení aspoň polovicu bodov, lebo inak budete mať problém získať celkovo dosť bodov z predmetu.
+
* V utorok 5.11. na cvičeniach bude krátky test podobne ako na prednáške 7.10.
* Na DÚ pracujte samostatne. Ak nájdeme prípad opisovania, všetky zúčastnené strany dostanú z úlohy 0 bodov.
+
** Bude zahŕňať učivo po minulú prednášku.
 +
** Môžete si priniesť ťahák 1 list A4. Používanie počítača nebude počas testu povolené.
 +
** Ide o súčasť cvičení 7, takže študenti, ktorí majú cvičenia 7 uznané na základe testu pre pokročilých, nemusia prísť.
 +
** Po teste pokračujete s riešením príkladov z cvičení na počítači.
 
* Plán na dnes: najskôr si ukážeme dva rekurzívne algoritmy na triedenie, potom sa vrátime k minulej téme prehľadávania s návratom.
 
* Plán na dnes: najskôr si ukážeme dva rekurzívne algoritmy na triedenie, potom sa vrátime k minulej téme prehľadávania s návratom.
* Od budúceho týždňa prednáša kolega Kostolányi, začínate novú tému práca so smerníkmi a pamäťou. Rekurzia sa ale ešte párkrát počas semestra vráti.
 
  
 
== Rýchle triedenia prostredníctvom paradigmy „rozdeľuj a panuj” ==
 
== Rýchle triedenia prostredníctvom paradigmy „rozdeľuj a panuj” ==
Riadok 16: Riadok 18:
  
 
Rozdeľuj a panuj je paradigma rekurzívneho riešenia problémov pracujúca v troch fázach:
 
Rozdeľuj a panuj je paradigma rekurzívneho riešenia problémov pracujúca v troch fázach:
* ''Rozdeľuj'' – problém rozdelíme na menšie časti (t.j. podproblémy), ktoré sa dajú riešiť samostatne.
+
* '''Rozdeľuj''': problém rozdelíme na menšie časti (t.j. podproblémy), ktoré sa dajú riešiť samostatne.
* ''Vyrieš podproblémy'' – rekurzívne vyriešime úlohu pre každý podproblém.
+
* '''Vyrieš podproblémy''': rekurzívne vyriešime úlohu pre každý podproblém.
* ''Panuj'' – riešenia podproblémov spojíme do riešenia pôvodného problému.
+
* '''Panuj''': riešenia podproblémov spojíme do riešenia pôvodného problému.
  
 
== Triedenie zlučovaním (''Merge Sort'') ==
 
== Triedenie zlučovaním (''Merge Sort'') ==
Riadok 27: Riadok 29:
 
* Vo fáze „panuj” sa tieto dve utriedené postupnosti zlúčia (angl. ''merge'') do výsledného utriedeného poľa.
 
* Vo fáze „panuj” sa tieto dve utriedené postupnosti zlúčia (angl. ''merge'') do výsledného utriedeného poľa.
  
Triedenie zlučovaním tak bude vyzerať nasledovne (pričom zostáva doimplementovať funkciu ''merge''):   
+
Príklad:
 +
<pre>
 +
Vstupné pole rozdelené na polovicu:
 +
|6 1 5 7 2|4 8 9 3 0|
 +
Po rekurzívnom utriedení ľavej časti
 +
|1 2 5 6 7|4 8 9 3 0|
 +
Po rekurzívnom utriedení pravej časti
 +
|1 2 5 6 7|0 3 4 8 9|
 +
Po zlúčení
 +
|0 1 2 3 4 5 6 7 8 9|
 +
</pre>
 +
 
 +
Triedenie zlučovaním tak bude vyzerať nasledovne (pričom zostáva implementovať funkciu ''merge''):   
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 52: Riadok 66:
 
=== Zlúčenie dvoch utriedených podpostupností ===
 
=== Zlúčenie dvoch utriedených podpostupností ===
  
Zostáva doprogramovať zlúčenie dvoch utriedených podpostupností <tt>a[left..middle]</tt>, <tt>a[middle+1..right]</tt> do jednej utriedenej postupnosti. Zlúčenú postupnosť budeme postupne ukladať do pomocného poľa <tt>aux</tt>, pričom postupovať budeme nasledovne:
+
Zostáva naprogramovať zlúčenie dvoch utriedených postupností <tt>a[left..middle]</tt>, <tt>a[middle+1..right]</tt> do jednej utriedenej postupnosti.  
 +
 
 +
Príklad:
 +
<pre>
 +
|1 2 5 6 7|0 3 4 8 9| -> |0 1 2 3 4 5 6 7 8 9|
 +
</pre>
 +
 
 +
Zlúčenú postupnosť budeme postupne ukladať do pomocného poľa <tt>aux</tt>, pričom postupovať budeme nasledovne:
  
 
* Prvým prvkom poľa <tt>aux</tt> bude menší z prvkov <tt>a[left]</tt> a <tt>a[middle+1]</tt>.  
 
* Prvým prvkom poľa <tt>aux</tt> bude menší z prvkov <tt>a[left]</tt> a <tt>a[middle+1]</tt>.  
 
** Ostanú nám postupnosti <tt>a[left+1..middle]</tt> a <tt>a[middle+1..right]</tt> alebo <tt>a[left..middle]</tt> a <tt>a[middle+2..right]</tt>.
 
** Ostanú nám postupnosti <tt>a[left+1..middle]</tt> a <tt>a[middle+1..right]</tt> alebo <tt>a[left..middle]</tt> a <tt>a[middle+2..right]</tt>.
 
* Vo všeobecnosti máme postupnosti <tt>a[i..middle]</tt> a <tt>a[j..right]</tt>.  
 
* Vo všeobecnosti máme postupnosti <tt>a[i..middle]</tt> a <tt>a[j..right]</tt>.  
** Ďalším prvkom poľa <tt>aux</tt> bude menčí z prvkov <tt>a[i]</tt> a <tt>a[j]</tt>
+
** Ďalším prvkom poľa <tt>aux</tt> bude menší z prvkov <tt>a[i]</tt> a <tt>a[j]</tt>
 
** Ostanú nám postupnosti <tt>a[i+1..middle]</tt> a <tt>a[j..right]</tt> alebo <tt>a[i..middle]</tt> a <tt>a[j+1..right]</tt>.
 
** Ostanú nám postupnosti <tt>a[i+1..middle]</tt> a <tt>a[j..right]</tt> alebo <tt>a[i..middle]</tt> a <tt>a[j+1..right]</tt>.
 
* Toto robíme dovtedy, kým niektorú z postupností nevyčerpáme celú. Potom už len na koniec poľa <tt>aux</tt> dokopírujeme zvyšok druhej postupnosti.
 
* Toto robíme dovtedy, kým niektorú z postupností nevyčerpáme celú. Potom už len na koniec poľa <tt>aux</tt> dokopírujeme zvyšok druhej postupnosti.
Riadok 98: Riadok 119:
 
      
 
      
 
     for (int t = left; t <= right; t++) {  
 
     for (int t = left; t <= right; t++) {  
         // Prekopiruj pole aux naspat do pola a
+
         // Prekopiruj pole aux naspat do pola a
 +
        // pozicia 0 v aux pojde na poziciu left v a
 
         a[t] = aux[t - left];
 
         a[t] = aux[t - left];
 
     }
 
     }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
=== Ukážka na príklade ===
 +
Uvažujme pole <tt>a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}</tt>.
 +
 +
Volanie <tt>mergesort(a,0,9)</tt> potom utriedi pole <tt>a</tt> pomocou nasledujúcich rekurzívnych volaní (<tt>mergesort(a,l,h)</tt> je na obrázku skrátené na <tt>sort(l,h)</tt> a <tt>merge(a,l,m,h)</tt> na <tt>merge(l,m,h)</tt>, žltou sú vyznačené pozície v poli, ktoré sa v danom volaní triedia):
 +
 +
[[Súbor:Mergesort.png|500px]]
 +
 +
Pole <tt>a</tt> sa počas týchto volaní mení nasledovne:
 +
<pre>
 +
merge(a,0,0,1): |6|1|5 7 2 4 8 9 3 0  -> |1 6|5 7 2 4 8 9 3 0
 +
merge(a,0,1,2): |1 6|5|7 2 4 8 9 3 0  -> |1 5 6|7 2 4 8 9 3 0
 +
merge(a,3,3,4):  1 5 6|7|2|4 8 9 3 0  ->  1 5 6|2 7|4 8 9 3 0
 +
merge(a,0,2,4): |1 5 6|2 7|4 8 9 3 0  -> |1 2 5 6 7|4 8 9 3 0
 +
merge(a,5,5,6):  1 2 5 6 7|4|8|9 3 0  ->  1 2 5 6 7|4 8|9 3 0
 +
merge(a,5,6,7):  1 2 5 6 7|4 8|9|3 0  ->  1 2 5 6 7|4 8 9|3 0
 +
merge(a,8,8,9):  1 2 5 6 7 4 8 9|3|0| ->  1 2 5 6 7 4 8 9|0 3|
 +
merge(a,5,7,9):  1 2 5 6 7|4 8 9|0 3| ->  1 2 5 6 7|0 3 4 8 9|
 +
merge(a,0,4,9): |1 2 5 6 7|0 3 4 8 9| -> |0 1 2 3 4 5 6 7 8 9|
 +
</pre>
 +
 +
===Odhad zložitosti===
 +
Zlučovanie dvoch postupností, ktoré spolu obsahujú ''N'' prvkov, trvá čas ''O(N)''. Prečo?
 +
 +
V algoritme máme ''log<sub>2</sub> N'' úrovní rekurzie:
 +
* na prvej spracovávame úseky dĺžky ''N'',
 +
* na druhej ''N/2'', na tretej ''N/4'' atď,
 +
* po ''log<sub>2</sub> N'' úrovniach dostaneme úseky dĺžky 1.
 +
Na každej úrovni je každý prvok najviac v jednom zlučovaní, teda celkový čas zlučovaní na každej úrovni je ''O(N)''. Celkový čas výpočtu je ''O(N log N)''.
  
 
=== Výsledný program ===
 
=== Výsledný program ===
Riadok 155: Riadok 206:
 
            
 
            
 
     mergesort(a, left, middle);
 
     mergesort(a, left, middle);
     mergesort(a, middle+1, right);
+
     mergesort(a, middle + 1, right);
 
        
 
        
 
     merge(a, left, middle, right);
 
     merge(a, left, middle, right);
Riadok 180: Riadok 231:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
=== Ukážka na príklade ===
 
Uvažujme pole <tt>a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}</tt>.
 
 
Volanie <tt>mergesort(a,0,9)</tt> potom utriedi pole <tt>a</tt> pomocou nasledujúcich rekurzívnych volaní (namiesto <tt>mergesort(a,l,h)</tt> budeme písať vždy len <tt>sort(l,h)</tt> a namiesto <tt>merge(a,l,m,h)</tt> len <tt>merge(l,m,h)</tt>):
 
<pre>
 
sort(0,9) sort(0,4) sort(0,2) sort(0,1) sort(0,0)
 
          .        .        .        sort(1,1)
 
          .        .        .        merge(0,0,1)
 
          .        .        sort(2,2)
 
          .        .        merge(0,1,2)
 
          .        sort(3,4) sort(3,3)
 
          .        .        sort(4,4)
 
          .        .        merge(3,3,4)
 
          .        merge(0,2,4)
 
          sort(5,9) sort(5,7) sort(5,6) sort(5,5)
 
          .        .        .        sort(6,6)
 
          .        .        .        merge(5,5,6)
 
          .        .        sort(7,7)
 
          .        .        merge(5,6,7)
 
          .        sort(8,9) sort(8,8)
 
          .        .        sort(9,9)
 
          .        .        merge(8,8,9)
 
          .        merge(5,7,9)
 
          merge(0,4,9)
 
</pre>
 
 
Pole <tt>a</tt> sa počas týchto volaní mení nasledovne:
 
<pre>
 
merge(a,0,0,1): |6|1|5 7 2 4 8 9 3 0  -> |1 6|5 7 2 4 8 9 3 0
 
merge(a,0,1,2): |1 6|5|7 2 4 8 9 3 0  -> |1 5 6|7 2 4 8 9 3 0
 
merge(a,3,3,4):  1 5 6|7|2|4 8 9 3 0  ->  1 5 6|2 7|4 8 9 3 0
 
merge(a,0,2,4): |1 5 6|2 7|4 8 9 3 0  -> |1 2 5 6 7|4 8 9 3 0
 
merge(a,5,5,6):  1 2 5 6 7|4|8|9 3 0  ->  1 2 5 6 7|4 8|9 3 0
 
merge(a,5,6,7):  1 2 5 6 7|4 8|9|3 0  ->  1 2 5 6 7|4 8 9|3 0
 
merge(a,8,8,9):  1 2 5 6 7 4 8 9|3|0| ->  1 2 5 6 7 4 8 9|0 3|
 
merge(a,5,7,9):  1 2 5 6 7|4 8 9|0 3| ->  1 2 5 6 7|0 3 4 8 9|
 
merge(a,0,4,9): |1 2 5 6 7|0 3 4 8 9| -> |0 1 2 3 4 5 6 7 8 9|
 
</pre>
 
 
===Odhad zložitosti===
 
Zlučovanie dvoch postupností, ktoré spolu obsahujú ''N'' prvkov, trvá čas ''O(N)''. Prečo?
 
 
V algoritme máme ''log<sub>2</sub> N'' úrovní rekurzie, lebo na prvej spracovávame úseky dĺžky ''N'', na druhej ''N/2'', na tretej ''N/4'' atď, až po ''log<sub>2</sub> N'' úrovniach dostaneme úseky dĺžky 1. Na každej úrovni je každý prvok najviac v jednom zlučovaní, teda celkový čas zlučovaní na každej úrovni je ''O(N)''. Celkový čas výpočtu je ''O(N log N)''.
 
  
 
== ''Quick Sort'' ==
 
== ''Quick Sort'' ==
Riadok 231: Riadok 238:
 
* ''Rekurzívne utriedi'' prvú a tretiu skupinu (druhá skupina má iba jeden prvok)
 
* ''Rekurzívne utriedi'' prvú a tretiu skupinu (druhá skupina má iba jeden prvok)
 
* Vo fáze ''panuj'' už potom nemusí robiť nič &ndash; po utriedení spomínaných dvoch skupín totiž vznikne utriedené pole.
 
* Vo fáze ''panuj'' už potom nemusí robiť nič &ndash; po utriedení spomínaných dvoch skupín totiž vznikne utriedené pole.
 +
 +
Príklad:
 +
<pre>
 +
Vstupné pole:
 +
|6 1 5 7 2 4 8 9 3 0|
 +
Po rozdelení s pivotom 6:
 +
|0 1 5 2 4 3|6|9 7 8|
 +
Po rekurzívnom utriedení ľavej časti:
 +
|0 1 2 3 4 5|6|9 7 8|
 +
Po rekurzívnom utriedení pravej časti:
 +
|0 1 2 3 4 5|6|7 8 9|
 +
</pre>
  
 
Základ triedenia tak bude vyzerať nasledovne (pričom zostáva implementovať funkciu <tt>partition</tt>):
 
Základ triedenia tak bude vyzerať nasledovne (pričom zostáva implementovať funkciu <tt>partition</tt>):
Riadok 247: Riadok 266:
 
     // a[left..middle-1] su mensie ako pivot  
 
     // a[left..middle-1] su mensie ako pivot  
 
     // a[middle] je pivot
 
     // a[middle] je pivot
     // a[middle+1..right] su vacsie ako pivot
+
     // a[middle+1..right] su vacsie ako pivot
 
      
 
      
 
     /* Rekurzivne utried a[left..middle-1], a[middle+1..right]: */     
 
     /* Rekurzivne utried a[left..middle-1], a[middle+1..right]: */     
Riadok 298: Riadok 317:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
=== Ukážka na príklade ===
 +
Opäť uvažujme pole <tt>a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}</tt>.
 +
 +
Volanie <tt>quicksort(0,9)</tt> potom utriedi pole <tt>a</tt> pomocou nasledujúcich rekurzívnych volaní (namiesto <tt>quicksort(a,l,h)</tt> píšeme zakaždým len <tt>sort(l,h)</tt>):
 +
<pre>
 +
sort(0,9) sort(0,5) sort(0,-1)
 +
          .        sort(1,5) sort(1,0)
 +
          .        .        sort(2,5) sort(2,4) sort(2,2)
 +
          .        .        .        .        sort(4,4)
 +
          .        .        .        sort(6,5)
 +
          sort(7,9) sort(7,8) sort(7,7)
 +
                    .        sort(9,8)
 +
                    sort(10,9)
 +
</pre>
 +
 +
Volania funkcie <tt>partition</tt> sú počas tohto behu nasledovné:
 +
<pre>
 +
partition(a,0,9): |6 1 5 7 2 4 8 9 3 0| -> |0 1 5 2 4 3|6|9 7 8|
 +
partition(a,0,5): |0 1 5 2 4 3|6 9 7 8  -> |0|1 5 2 4 3|6 9 7 8
 +
partition(a,1,5):  0|1 5 2 4 3|6 9 7 8  ->  0|1|5 2 4 3|6 9 7 8
 +
partition(a,2,5):  0 1|5 2 4 3|6 9 7 8  ->  0 1|3 2 4|5|6 9 7 8
 +
partition(a,2,4):  0 1|3 2 4|5 6 9 7 8  ->  0 1|2|3|4|5 6 9 7 8
 +
partition(a,7,9):  0 1 2 3 4 5 6|9 7 8| ->  0 1 2 3 4 5 6|8 7|9|
 +
partition(a,7,8):  0 1 2 3 4 5 6|8 7|9  ->  0 1 2 3 4 5 6|7|8|9
 +
</pre>
 +
 +
''Cvičenie'':
 +
* Ako sa bude Quick Sort správať, keď na vstupe dostane už utriedené pole?
 +
* Ako sa bude správať, keď na vstupe dostane ''zostupne'' utriedené pole?
 +
 +
=== Odhad zložitosti ===
 +
 +
V ideálnom prípade pivot rozdelí pole na dve rovnako veľké časti. Vtedy je čas výpočtu ''O(N log N)'', podobne ako pre Mergesort.
 +
 +
Nepríjemné je, keď pivot je vždy najmenší alebo najväčší prvok v danom úseku. Vtedy je čas  ''O(N<sup>2</sup>)''.
 +
* Aby sme sa vyhli problémom, v praxi sa ako pivot často vyberá náhodný prvok z intervalu.
  
 
=== Výsledný program ===
 
=== Výsledný program ===
Riadok 358: Riadok 414:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
=== Ukážka na príklade ===
 
Opäť uvažujme pole <tt>a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}</tt>.
 
 
Volanie <tt>quicksort(0,9)</tt> potom utriedi pole <tt>a</tt> pomocou nasledujúcich rekurzívnych volaní (namiesto <tt>quicksort(a,l,h)</tt> píšeme zakaždým len <tt>sort(l,h)</tt>):
 
<pre>
 
sort(0,9) sort(0,5) sort(0,-1)
 
          .        sort(1,5) sort(1,0)
 
          .        .        sort(2,5) sort(2,4) sort(2,2)
 
          .        .        .        .        sort(4,4)
 
          .        .        .        sort(6,5)
 
          sort(7,9) sort(7,8) sort(7,7)
 
                    .        sort(9,8)
 
                    sort(10,9)
 
</pre>
 
 
Volania funkcie <tt>partition</tt> sú počas tohto behu nasledovné:
 
<pre>
 
partition(a,0,9): |6 1 5 7 2 4 8 9 3 0| -> |0 1 5 2 4 3|6|9 7 8|
 
partition(a,0,5): |0 1 5 2 4 3|6 9 7 8  -> |0|1 5 2 4 3|6 9 7 8
 
partition(a,1,5):  0|1 5 2 4 3|6 9 7 8  ->  0|1|5 2 4 3|6 9 7 8
 
partition(a,2,5):  0 1|5 2 4 3|6 9 7 8  ->  0 1|3 2 4|5|6 9 7 8
 
partition(a,2,4):  0 1|3 2 4|5 6 9 7 8  ->  0 1|2|3|4|5 6 9 7 8
 
partition(a,7,9):  0 1 2 3 4 5 6|9 7 8| ->  0 1 2 3 4 5 6|8 7|9|
 
partition(a,7,8):  0 1 2 3 4 5 6|8 7|9  ->  0 1 2 3 4 5 6|7|8|9
 
</pre>
 
 
''Cvičenie'':
 
* Ako sa bude Quick Sort správať, keď na vstupe dostane už utriedené pole?
 
* Ako sa bude správať, keď na vstupe dostane ''zostupne'' utriedené pole?
 
 
=== Odhad zložitosti ===
 
 
V ideálnom prípade pivot rozdelí pole na dve rovnako veľké časti. Vtedy je čas výpočtu O(N log N), podobne ako pre Mergesort.
 
 
Nepríjemné je, keď pivot je vždy najmenší alebo najväčší prvk v danom úseku. Vtedy je čas  <math>O(N^2)</math>.
 
* Aby sme sa vyhli problémom, ak je vstupné pole (takmer) utriedené, môžeme ho pred triedením náhodne premiešať alebo vyberať ako pivot náhodný prvok z intervalu.
 
  
 
===Iná implementácia===
 
===Iná implementácia===
Riadok 428: Riadok 447:
  
 
Jednoduché triedenia: Bubble Sort, Insertion Sort, Max Sort.
 
Jednoduché triedenia: Bubble Sort, Insertion Sort, Max Sort.
* Jednoduché, ale pomalé: zložitosť <math>O(n^2)</math>.
+
* Jednoduché, ale pomalé: zložitosť ''O(n<sup>2</sup>)''.
  
 
Rekurzívne triedenia založené na technike rozdeľuj a panuj.
 
Rekurzívne triedenia založené na technike rozdeľuj a panuj.
 
* Rýchlejšie, zložitejšie.
 
* Rýchlejšie, zložitejšie.
* Merge Sort: zložitosť <math>O(n\log n)</math>.
+
* Merge Sort: zložitosť ''O(n log n)''.
* Quick Sort: zložitosť <math>O(n^2)</math> v najhoršom prípade, pre väčšinu vstupov <math>O(n\log n)</math>, väčšinou rýchlejší ako Merge Sort.
+
* Quick Sort: zložitosť ''O(n<sup>2</sup>)'' v najhoršom prípade, pre väčšinu vstupov ''O(n log n)'', väčšinou rýchlejší ako Merge Sort.
  
 
Reálnu rýchlosť triedení na náhodne zvolenom veľkom vstupe možno porovnať napríklad nasledujúcim programom:
 
Reálnu rýchlosť triedení na náhodne zvolenom veľkom vstupe možno porovnať napríklad nasledujúcim programom:
Riadok 581: Riadok 600:
 
</pre>
 
</pre>
  
== Prehľadávanie s návratom (backtracking) ==
+
== Problém batoha (''Knapsack problem'') ==
* Videli sme ako rekurzívne generovať všetky postupnosti spĺňajúce určité požiadavky
 
* Ak máme špeciálne požiadavky, napr. že žiadne číslo sa neopakuje, môžeme buď generovať všetky k-tice a testovať to pred výpisom, alebo už počas generovania urezávať neperspektívne vetvy výpočtu, čo je rýchlejšie
 
* Táto technika sa dá použiť na riešenie rôznych hlavolamov, videli sme problém 8 dám.
 
* Dá sa použiť aj na reálne problémy, ale pozor, čas výpočtu prudko (exponenciálne) rastie s dĺžkou postupností, takže vhodné len pre malé vstupy.
 
* Dnes ešte jeden problém riešený prehľadávaním s návratom, kde medzi možnými riešeniami hľadáme najlepšie.
 
  
==Generovanie všetkých podmnožín==
+
Dnes sme videli ako použiť rekurziu v rýchlych algoritmoch, teraz sa však vráťme k prehľadávaniu s návratom z minulej prednášky, čo je pomalá metóda pre prípady, keď nepoznáme lepší algoritmus.
Chceme vypísať všetky podmnožiny množiny ''{0,..,m-1}''. Na rozdiel od variácií nám v podmnožine nezáleží na poradí (napr. ''{0,1} = {1,0}''), prvky teda budeme vždy vypisovať od najmenšieho po najväčší. Napr. pre ''m=2'' máme podmnožiny
 
<pre>
 
{}
 
{0}
 
{0,1}
 
{1}
 
</pre>
 
Podmnožinu vieme vyjadriť ako binárne pole dĺžky ''m'', kde <tt>a[i]=0</tt> znamená, že ''i'' nepatrí do množiny a <tt>a[i]=1</tt> znamená, že patrí. Teda môžeme použiť predchádzajúci program pre ''n=2'', ''k=m'' a zmeniť iba výpis:
 
  
<syntaxhighlight lang="C++">
+
Metódu prehľadávania s návratom využijeme na riešenie problému batoha. Ide o dôležitý problém, s ktorým sa ešte počas štúdia stretnete. Predstaviť si ho môžeme napríklad takto:
void vypis(int a[], int m) {
+
* Zlodej sa vlúpal do obchodu, v ktorom našiel niekoľko predmetov.
    cout << "{";
 
    bool prve = true;
 
    for (int i = 0; i < m; i++) {
 
        if (a[i] == 1) {
 
            if (prve) {
 
                cout << "" << i;
 
                prve=false;
 
            } else {
 
                cout << "," << i;
 
            }
 
        }
 
    }
 
    cout << "}" << endl;
 
}
 
</syntaxhighlight>
 
* V premennej <tt>prve</tt> si pamätáme, či máme oddeliť ďalšie vypisované číslo od predchádzajúceho.
 
** Ak ešte žiadne nebolo, oddeľovač je prázdny reťazec.
 
** Ak už sme niečo vypísali, oddeľovač je čiarka.
 
 
 
Namiesto poľa intov môžeme použiť pole boolovských hodnôt a celý program trochu prispôsobiť tomu, že generujeme podmnožiny:
 
<syntaxhighlight lang="C++">
 
#include <iostream>
 
#include <cstring>
 
using namespace std;
 
 
 
void vypis(bool a[], int m) {
 
    cout << "{";
 
    bool prve = true;
 
    for (int i = 0; i < m; i++) {
 
        if (a[i]) {
 
            if (prve) {
 
                cout << "" << i;
 
                prve=false;
 
            } else {
 
                cout << "," << i;
 
            }
 
        }
 
    }
 
    cout << "}" << endl;
 
}
 
 
 
void generuj(bool a[], int i, int m) {
 
    /* v poli a dlzky k mame rozhodnutie o prvych i
 
    * prvkoch, chceme vygenerovat vsetky podmnoziny
 
    * prvkov {i..m-1} */
 
    if (i == m) {
 
        vypis(a, m);
 
    } else {
 
        a[i] = true;
 
        generuj(a, i + 1, m);
 
        a[i] = false;
 
        generuj(a, i + 1, m);
 
    }
 
}
 
 
 
int main() {
 
    const int maxM = 100;
 
    int m;
 
    cin >> m;
 
    bool a[maxM];
 
    generuj(a, 0, m);
 
}
 
</syntaxhighlight>
 
 
 
Pre n=3 program vypíše:
 
<pre>
 
{0,1,2}
 
{0,1}
 
{0,2}
 
{0}
 
{1,2}
 
{1}
 
{2}
 
{}
 
</pre>
 
 
 
Cvičenie: Čo by program vypísal, ak by sme prehodili <tt>true</tt> a <tt>false</tt> v rekurzii?
 
 
 
== Problém batoha (''Knapsack problem'') ==
 
 
 
Metódu prehľadávania s návratom využijeme na riešenie tzv. ''problému batoha''. Ide o dôležitý problém, s ktorým sa ešte počas štúdia stretnete. Populárne ho možno sformulovať napríklad takto:
 
* Zlodej sa vlúpal do obchodu, v ktorom našiel nejaký počet &bdquo;odcudziteľných&rdquo; predmetov
 
 
* Pozná cenu aj hmotnosť predmetov.  
 
* Pozná cenu aj hmotnosť predmetov.  
 
* Z obchodu dokáže odniesť iba lup nepresahujúci nosnosť svojho batoha.
 
* Z obchodu dokáže odniesť iba lup nepresahujúci nosnosť svojho batoha.
* Ktoré predmety má zlodej odcudziť, aby ich celková hmotnosť nepresahovala nosnosť batoha a aby odišiel s <em>čo najcennejším</em> lupom?
+
* Ktoré predmety má zlodej odcudziť, aby ich celková hmotnosť nepresahovala nosnosť batoha a aby odišiel s čo najcennejším lupom?
  
 
Vstup nášho programu bude vyzerať napríklad nejako takto:
 
Vstup nášho programu bude vyzerať napríklad nejako takto:
 
<pre>
 
<pre>
 
Zadaj pocet predmetov v obchode: 3
 
Zadaj pocet predmetov v obchode: 3
Zadaj hmotnost a cenu predmetu cislo 1: 5 9
+
Zadaj hmotnost a cenu predmetu 1: 5 9
Zadaj hmotnost a cenu predmetu cislo 2: 4 6
+
Zadaj hmotnost a cenu predmetu 2: 4 6
Zadaj hmotnost a cenu predmetu cislo 3: 4 4
+
Zadaj hmotnost a cenu predmetu 3: 4 4
 
Zadaj nosnost batoha: 8
 
Zadaj nosnost batoha: 8
 
</pre>
 
</pre>
 
Výstup programu na horeuvedenom vstupe potom bude takýto:
 
Výstup programu na horeuvedenom vstupe potom bude takýto:
 
<pre>
 
<pre>
Zober nasledujuce predmety: 2, 3.
+
Zober nasledujuce predmety: 2 3
Celkova hodnota lupu: 10.
+
Celkova hodnota lupu: 10
 
</pre>
 
</pre>
  
 
Pri reálnom použití nosnosť batoha môže reprezentovať dostupné zdroje, napr. výpočtový čas na serveri, dostupných pracovníkov, veľkosť rozpočtu a pod, a predmety sú potenciálne úlohy, z ktorých si chceme vybrať podmnožinu, ktorú by sme s danými zdrojmi vedeli vykonať a dosiahnuť čo najvyšší zisk alebo iný ukazovateľ.
 
Pri reálnom použití nosnosť batoha môže reprezentovať dostupné zdroje, napr. výpočtový čas na serveri, dostupných pracovníkov, veľkosť rozpočtu a pod, a predmety sú potenciálne úlohy, z ktorých si chceme vybrať podmnožinu, ktorú by sme s danými zdrojmi vedeli vykonať a dosiahnuť čo najvyšší zisk alebo iný ukazovateľ.
  
=== Prvé riešenie: preskúmanie všetkých možných výberov ===
+
=== Prvé riešenie: preskúmanie všetkých podmnožín ===
 +
 
 +
* Preskúmame všetky podmnožiny množiny predmetov v obchode, čiže všetky potenciálne lupy.
 +
* Na to upravíme program generujúci všetky podmnožiny danej množiny.
 +
* Pre každú podmnožinu namiesto výpisu spravíme nasledovné:
 +
** Spočítame celkovú hmotnosť a cenu nájdeného potenciálneho lupu.
 +
** Ak hmotnosť tohto lupu nepresahuje nosnosť batoha, porovnáme jeho cenu s najlepším doposiaľ nájdeným lupom.
 +
** Ak je cennejší, ako doposiaľ najlepší lup, ide o nového kandidáta na optimálny lup a zapamätáme si ho.
 +
 
 +
 
 +
* Pre jednoduchosť použijeme v programe globálne premenné, lebo potrebujeme veľa  údajov
 +
** Globálne premenné spôsobujú problémy vo väčších programoch: mená premenných sa môžu "biť", môžeme si omylom prepísať číslo dôležité v inej časti programu
 +
** Mohli by sme si tiež spraviť <tt>struct</tt> obsahujúci všetky premenné potrebné v rekurzii a odovzdávať si ten
  
Najjednoduchšie riešenie problému batoha spočíva v preskúmaní všetkých podmnožín množiny predmetov v obchode &ndash; čiže všetkých potenciálnych lupov. Program vypisujúci všetky podmnožiny danej množiny mierne poupravíme &ndash; nebudeme nájdené podmnožiny predmetov (potenciálne lupy) vypisovať, ale zakaždým:
 
* Spočítame celkovú hmotnosť a cenu nájdeného potenciálneho lupu.
 
* Ak hmotnosť tohto lupu nepresahuje cenu batoha, porovnáme jeho cenu s najlepším doposiaľ nájdeným lupom.
 
* Ak je cennejší, ako doposiaľ najlepší lup, ide o nového kandidáta na optimálny lup.
 
  
 
Podmnožiny budeme reprezentovať poľom typu <tt>bool</tt>, v ktorom si pre každý predmet pamätáme, či do danej podmnožiny patrí.
 
Podmnožiny budeme reprezentovať poľom typu <tt>bool</tt>, v ktorom si pre každý predmet pamätáme, či do danej podmnožiny patrí.
Riadok 731: Riadok 663:
 
int nosnost;                     
 
int nosnost;                     
  
// najcennejsi doposial najdeny potencialny lup (na uvod neinicializovany)
+
// najcennejsi doposial najdeny lup
 
bool najlepsiLup[maxN];           
 
bool najlepsiLup[maxN];           
 
// jeho cena (kazdy lup bude urcite cennejsi ako -1)
 
// jeho cena (kazdy lup bude urcite cennejsi ako -1)
Riadok 757: Riadok 689:
  
 
void vypisLup(bool lup[]) {
 
void vypisLup(bool lup[]) {
     cout << "Zober nasledujuce predmety: ";
+
     cout << "Zober nasledujuce predmety:";
    bool prvy = true;
 
 
     for (int i = 0; i < N; i++) {
 
     for (int i = 0; i < N; i++) {
 
         if (lup[i]) {
 
         if (lup[i]) {
            if (prvy) {
+
            cout << " " << i + 1;
                cout << i + 1;
+
         }          
                prvy = false;
 
            } else {
 
                cout << ", " << i + 1;
 
            }       
 
         }  
 
 
     }
 
     }
     cout << "." << endl;
+
     cout << endl;
 
}
 
}
  
 
/* Generovanie vsetkych moznych lupov (podmnozin predmetov) */
 
/* Generovanie vsetkych moznych lupov (podmnozin predmetov) */
 
void generujLupy(bool lup[], int index) {
 
void generujLupy(bool lup[], int index) {
     /* V poli lup[] dlzky N postupne generujeme podmnoziny predmetov.
+
     /* V poli lup[] dlzky N postupne generujeme podmnoziny.
       O hodnotach prvkov lup[0],...,lup[index-1] uz je rozhodnute.
+
       O hodnotach lup[0],...,lup[index-1] uz je rozhodnute.
       Postupne vygenerujeme vsetky moznosti pre lup[index],...,lup[N-1].
+
       Postupne vygenerujeme vsetky moznosti  
       Kazdy vysledny lup porovname s doposial najlepsim  
+
      pre lup[index],...,lup[N-1].
 +
       Kazdy lup porovname s doposial najlepsim  
 
       a v pripade potreby optimum aktualizujeme.     
 
       a v pripade potreby optimum aktualizujeme.     
 
     */
 
     */
Riadok 783: Riadok 710:
 
         // Lup je vygenerovany; zisti, ci ho batoh unesie.                               
 
         // Lup je vygenerovany; zisti, ci ho batoh unesie.                               
 
         if (spocitajHmotnostLupu(lup) <= nosnost) {   
 
         if (spocitajHmotnostLupu(lup) <= nosnost) {   
             // Ak ano, porovnaj cenu lupu s doposial najlepsim.
+
             // Ak ano, porovnaj cenu s doposial najlepsim.
 
             int cenaLupu = spocitajCenuLupu(lup);
 
             int cenaLupu = spocitajCenuLupu(lup);
 
             if (cenaLupu > cenaNajlepsiehoLupu) {     
 
             if (cenaLupu > cenaNajlepsiehoLupu) {     
Riadok 795: Riadok 722:
 
     } else {                                         
 
     } else {                                         
 
         // Lup este nie je vygenerovany,
 
         // Lup este nie je vygenerovany,
         // skus postupne obe moznosti pre lup[index].  
+
         // skus obe moznosti pre lup[index].  
 
         lup[index] = false;
 
         lup[index] = false;
 
         generujLupy(lup, index+1);
 
         generujLupy(lup, index+1);
Riadok 807: Riadok 734:
 
     cin >> N;
 
     cin >> N;
 
     for (int i = 0; i < N; i++) {
 
     for (int i = 0; i < N; i++) {
         cout << "Zadaj hmotnost a cenu predmetu cislo " << (i+1) << ": ";
+
         cout << "Zadaj hmotnost a cenu predmetu "  
 +
            << (i+1) << ": ";
 
         cin >> a[i].hmotnost >> a[i].cena;
 
         cin >> a[i].hmotnost >> a[i].cena;
 
     }
 
     }
Riadok 816: Riadok 744:
 
     generujLupy(lup, 0);
 
     generujLupy(lup, 0);
 
      
 
      
    cout << endl;
 
 
     vypisLup(najlepsiLup);
 
     vypisLup(najlepsiLup);
     cout << "Celkova hodnota lupu: " << cenaNajlepsiehoLupu << "." << endl;
+
     cout << "Celkova hodnota lupu: "  
 +
        << cenaNajlepsiehoLupu << endl;
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
Cvičenie: Čo bude program robiť, keď každý predmet má hmotnosť väčšiu ako nosnosť batoha?
  
 
=== Optimalizácia č. 1: ukončenie prehľadávania vždy, keď je prekročená nosnosť ===
 
=== Optimalizácia č. 1: ukončenie prehľadávania vždy, keď je prekročená nosnosť ===
  
Keď je už po vygenerovaní nejakej časti lupu (čiže prvých niekoľko hodnôt poľa <tt>lup</tt>) jasné, že jeho hmotnosť bude presahovať nosnosť batoha, možno túto vetvu prehľadávania ukončiť.
+
Keď je už po vygenerovaní nejakej podmnožiny (čiže prvých niekoľko hodnôt poľa <tt>lup</tt>) jasné, že hmotnosť lupu bude presahovať nosnosť batoha, možno túto vetvu prehľadávania ukončiť.
  
Okrem samotnej funkcie <tt>generujLupy</tt> je potrebné prispôsobiť aj funkciu <tt>spocitajHmotnostLupu</tt> tak, aby ju bolo možné aplikovať aj na &bdquo;neúplne vygenerované lupy&rdquo;.
+
Okrem samotnej funkcie <tt>generujLupy</tt> je potrebné prispôsobiť aj funkciu <tt>spocitajHmotnostLupu</tt> tak, aby ju bolo možné aplikovať aj na neúplne vygenerované podmnožiny.
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
Riadok 863: Riadok 793:
 
}  
 
}  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
Cvičenie: Čo bude program robiť, keď každý predmet má hmotnosť väčšiu ako nosnosť batoha?
  
 
=== Optimalizácia č. 2: hmotnosť a cenu lupu netreba zakaždým počítať odznova ===
 
=== Optimalizácia č. 2: hmotnosť a cenu lupu netreba zakaždým počítať odznova ===
Riadok 887: Riadok 819:
  
 
void vypisLup(bool lup[]) {
 
void vypisLup(bool lup[]) {
     cout << "Zober nasledujuce predmety: ";
+
     cout << "Zober nasledujuce predmety:";
    bool prvy = true;
 
 
     for (int i = 0; i < N; i++) {
 
     for (int i = 0; i < N; i++) {
 
         if (lup[i]) {
 
         if (lup[i]) {
             if (prvy) {
+
             cout << " " << i + 1;
                cout << i + 1;
 
                prvy = false;
 
            } else {
 
                cout << ", " << i + 1;
 
            }       
 
 
         }     
 
         }     
 
     }
 
     }
     cout << "." << endl;
+
     cout << endl;
 
}
 
}
  
Riadok 917: Riadok 843:
 
         generujLupy(lup, index+1, hmotnostLupu, cenaLupu);
 
         generujLupy(lup, index+1, hmotnostLupu, cenaLupu);
 
         lup[index] = true;
 
         lup[index] = true;
         generujLupy(lup, index+1, hmotnostLupu + a[index].hmotnost,  
+
         generujLupy(lup, index+1,  
 +
                    hmotnostLupu + a[index].hmotnost,  
 
                     cenaLupu + a[index].cena);
 
                     cenaLupu + a[index].cena);
 
     }
 
     }
Riadok 926: Riadok 853:
 
     cin >> N;
 
     cin >> N;
 
     for (int i = 0; i < N; i++) {
 
     for (int i = 0; i < N; i++) {
         cout << "Zadaj hmotnost a cenu predmetu cislo " << (i+1) << ": ";
+
         cout << "Zadaj hmotnost a cenu predmetu "  
 +
            << (i+1) << ": ";
 
         cin >> a[i].hmotnost >> a[i].cena;
 
         cin >> a[i].hmotnost >> a[i].cena;
 
     }
 
     }
Riadok 933: Riadok 861:
 
      
 
      
 
     bool lup[maxN];
 
     bool lup[maxN];
     // Doposial nie je nic vygenerovane; hmotnost aj cena lupu su zatial nulove
+
     // Doposial nie je nic vygenerovane;  
 +
    // hmotnost aj cena lupu su zatial nulove
 
     generujLupy(lup, 0, 0, 0);  
 
     generujLupy(lup, 0, 0, 0);  
 
      
 
      
 
     cout << endl;
 
     cout << endl;
 
     vypisLup(najlepsiLup);
 
     vypisLup(najlepsiLup);
     cout << "Celkova hodnota lupu: " << cenaNajlepsiehoLupu << "." << endl;
+
     cout << "Celkova hodnota lupu: "  
 +
        << cenaNajlepsiehoLupu << endl;
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>

Aktuálna revízia z 08:43, 30. október 2024

Oznamy

  • DÚ2 bude zverejnená po prednáške, odovzdávajte do utorka 19.11. 22:00
  • V utorok 5.11. na cvičeniach bude krátky test podobne ako na prednáške 7.10.
    • Bude zahŕňať učivo po minulú prednášku.
    • Môžete si priniesť ťahák 1 list A4. Používanie počítača nebude počas testu povolené.
    • Ide o súčasť cvičení 7, takže študenti, ktorí majú cvičenia 7 uznané na základe testu pre pokročilých, nemusia prísť.
    • Po teste pokračujete s riešením príkladov z cvičení na počítači.
  • Plán na dnes: najskôr si ukážeme dva rekurzívne algoritmy na triedenie, potom sa vrátime k minulej téme prehľadávania s návratom.

Rýchle triedenia prostredníctvom paradigmy „rozdeľuj a panuj”

Doposiaľ sme prebrali tri triediace algoritmy: Bubble Sort, Insertion Sort a Max Sort. Všetky sú jednoduché, ale pomalé: majú kvadratickú zložitosť O(n2).

Dnes pridáme ďalšie dve triedenia, ktoré budú pre veľké polia omnoho rýchlejšie: Merge Sort a Quick Sort. Obe sú založené na paradigme rozdeľuj a panuj (angl. divide and conquer, lat. divide et impera).

Rozdeľuj a panuj je paradigma rekurzívneho riešenia problémov pracujúca v troch fázach:

  • Rozdeľuj: problém rozdelíme na menšie časti (t.j. podproblémy), ktoré sa dajú riešiť samostatne.
  • Vyrieš podproblémy: rekurzívne vyriešime úlohu pre každý podproblém.
  • Panuj: riešenia podproblémov spojíme do riešenia pôvodného problému.

Triedenie zlučovaním (Merge Sort)

Triedenie zlučovaním (angl. Merge Sort) pracuje nasledovne:

  • Pole rozdelíme na dve približne rovnaké časti
  • Každú časť zvlášť rekurzívne utriedime
  • Vo fáze „panuj” sa tieto dve utriedené postupnosti zlúčia (angl. merge) do výsledného utriedeného poľa.

Príklad:

Vstupné pole rozdelené na polovicu:
|6 1 5 7 2|4 8 9 3 0|
Po rekurzívnom utriedení ľavej časti
|1 2 5 6 7|4 8 9 3 0|
Po rekurzívnom utriedení pravej časti
|1 2 5 6 7|0 3 4 8 9|
Po zlúčení
|0 1 2 3 4 5 6 7 8 9|

Triedenie zlučovaním tak bude vyzerať nasledovne (pričom zostáva implementovať funkciu merge):

void mergesort(int a[], int left, int right) {
/* Funkcia utriedi prvky pola a[] od indexu left po index right (vratane). */
    
    /* Trivialne pripady: */
    if (left >= right) {
        return;           
    }
    
    /* Rozdeluj -- spocitaj priblizny stred triedeneho useku: */
    int middle = (left + right) / 2;
          
    /* Rekurzivne vyries podproblemy: */
    mergesort(a, left, middle);
    mergesort(a, middle + 1, right);
       
    /* Panuj -- zluc obe utriedene casti do jednej: */ 
    merge(a, left, middle, right);
}

Zlúčenie dvoch utriedených podpostupností

Zostáva naprogramovať zlúčenie dvoch utriedených postupností a[left..middle], a[middle+1..right] do jednej utriedenej postupnosti.

Príklad:

|1 2 5 6 7|0 3 4 8 9| -> |0 1 2 3 4 5 6 7 8 9|

Zlúčenú postupnosť budeme postupne ukladať do pomocného poľa aux, pričom postupovať budeme nasledovne:

  • Prvým prvkom poľa aux bude menší z prvkov a[left] a a[middle+1].
    • Ostanú nám postupnosti a[left+1..middle] a a[middle+1..right] alebo a[left..middle] a a[middle+2..right].
  • Vo všeobecnosti máme postupnosti a[i..middle] a a[j..right].
    • Ďalším prvkom poľa aux bude menší z prvkov a[i] a a[j]
    • Ostanú nám postupnosti a[i+1..middle] a a[j..right] alebo a[i..middle] a a[j+1..right].
  • Toto robíme dovtedy, kým niektorú z postupností nevyčerpáme celú. Potom už len na koniec poľa aux dokopírujeme zvyšok druhej postupnosti.

Po spojení oboch postupností do utriedeného poľa aux toto pole prekopírujeme naspäť do poľa a.

void merge(int a[], int left, int middle, int right) {
    int aux[maxN];
    int i = left;       // index v prvej postupnosti
    int j = middle + 1; // index v druhej postupnosti
    int k = 0;          // index v poli aux
    
    while (i <= middle && j <= right) { 
        // Kym su obe postupnosti a[i..middle], a[j..right] neprazdne,
        // mensi z prvkov a[i], a[j] uloz do aux[k] a posun indexy
        if (a[i] <= a[j]) {                         
            aux[k] = a[i];
            i++;
            k++;
        } else {
            aux[k] = a[j];
            j++;
            k++;
        }
    }
    
    while (i <= middle) {  
        // Ak nieco ostalo v prvej postupnosti, dokopiruj ju na koniec
        aux[k] = a[i];
        i++;
        k++;
    }
    
    while (j <= right) { 
        // Ak nieco ostalo v druhej postupnosti, dokopiruj ju na koniec
        aux[k] = a[j];
        j++;
        k++;
    }
    
    for (int t = left; t <= right; t++) { 
        // Prekopiruj pole aux naspat do pola a 
        // pozicia 0 v aux pojde na poziciu left v a
        a[t] = aux[t - left];
    }
}

Ukážka na príklade

Uvažujme pole a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}.

Volanie mergesort(a,0,9) potom utriedi pole a pomocou nasledujúcich rekurzívnych volaní (mergesort(a,l,h) je na obrázku skrátené na sort(l,h) a merge(a,l,m,h) na merge(l,m,h), žltou sú vyznačené pozície v poli, ktoré sa v danom volaní triedia):

Mergesort.png

Pole a sa počas týchto volaní mení nasledovne:

merge(a,0,0,1): |6|1|5 7 2 4 8 9 3 0  -> |1 6|5 7 2 4 8 9 3 0
merge(a,0,1,2): |1 6|5|7 2 4 8 9 3 0  -> |1 5 6|7 2 4 8 9 3 0
merge(a,3,3,4):  1 5 6|7|2|4 8 9 3 0  ->  1 5 6|2 7|4 8 9 3 0
merge(a,0,2,4): |1 5 6|2 7|4 8 9 3 0  -> |1 2 5 6 7|4 8 9 3 0
merge(a,5,5,6):  1 2 5 6 7|4|8|9 3 0  ->  1 2 5 6 7|4 8|9 3 0
merge(a,5,6,7):  1 2 5 6 7|4 8|9|3 0  ->  1 2 5 6 7|4 8 9|3 0
merge(a,8,8,9):  1 2 5 6 7 4 8 9|3|0| ->  1 2 5 6 7 4 8 9|0 3|
merge(a,5,7,9):  1 2 5 6 7|4 8 9|0 3| ->  1 2 5 6 7|0 3 4 8 9|
merge(a,0,4,9): |1 2 5 6 7|0 3 4 8 9| -> |0 1 2 3 4 5 6 7 8 9|

Odhad zložitosti

Zlučovanie dvoch postupností, ktoré spolu obsahujú N prvkov, trvá čas O(N). Prečo?

V algoritme máme log2 N úrovní rekurzie:

  • na prvej spracovávame úseky dĺžky N,
  • na druhej N/2, na tretej N/4 atď,
  • po log2 N úrovniach dostaneme úseky dĺžky 1.

Na každej úrovni je každý prvok najviac v jednom zlučovaní, teda celkový čas zlučovaní na každej úrovni je O(N). Celkový čas výpočtu je O(N log N).

Výsledný program

#include <iostream>
using namespace std;

const int maxN = 1000;

void merge(int a[], int left, int middle, int right) {
    int aux[maxN];
    int i = left;
    int j = middle + 1;
    int k = 0;
    
    while (i <= middle && j <= right) {
        if (a[i] <= a[j]) {
            aux[k] = a[i];
            i++;
            k++;
        } else {
            aux[k] = a[j];
            j++;
            k++;
        }
    }
    
    while (i <= middle) {
        aux[k] = a[i];
        i++;
        k++;
    }
    
    while (j <= right) {
        aux[k] = a[j];
        j++;
        k++;
    }
    
    for (int t = left; t <= right; t++) {
        a[t] = aux[t - left];
    }
}

void mergesort(int a[], int left, int right) {
    if (left >= right) {
        return;
    }
    
    int middle = (left + right) / 2;
          
    mergesort(a, left, middle);
    mergesort(a, middle + 1, right);
       
    merge(a, left, middle, right);
}

int main() {
    int N;
    int a[maxN];
    
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    cout << "Zadaj " << N << " cisel: ";
    for (int i = 0; i < N; i++) {
        cin >> a[i];
    }
    
    mergesort(a,0,N-1);
    
    cout << "Utriedene cisla:";
    for (int i = 0; i < N; i++) {
        cout << " " << a[i];
    }                        
    cout << endl;
}

Quick Sort

Quick Sort je tiež založený na metóde rozdeľuj a panuj. Postupuje ale nasledovne:

  • V rámci fázy rozdeľuj vyberie niektorý prvok poľa (napríklad jeho prvý prvok), ktorý nazve pivotom. Prvky poľa následne preusporiada na tri skupiny: v ľavej časti poľa budú prvky menšie ako pivot, za nimi pivot samotný a napokon v pravej časti prvky väčšie alebo rovné ako pivot.
  • Rekurzívne utriedi prvú a tretiu skupinu (druhá skupina má iba jeden prvok)
  • Vo fáze panuj už potom nemusí robiť nič – po utriedení spomínaných dvoch skupín totiž vznikne utriedené pole.

Príklad:

Vstupné pole:
|6 1 5 7 2 4 8 9 3 0| 
Po rozdelení s pivotom 6:
|0 1 5 2 4 3|6|9 7 8|
Po rekurzívnom utriedení ľavej časti:
|0 1 2 3 4 5|6|9 7 8|
Po rekurzívnom utriedení pravej časti:
|0 1 2 3 4 5|6|7 8 9|

Základ triedenia tak bude vyzerať nasledovne (pričom zostáva implementovať funkciu partition):

void quicksort(int a[], int left, int right) {
/* Utriedi cast pola a[] od indexu left po index right (vratane) */

    /* Trivialne pripady: */ 
    if (left >= right) {
        return; 
    }
    
    /* Rozdel pole na tri podpostupnosti: */ 
    int middle = partition(a, left, right); 
    // Po vykonani funkcie: 
    // a[left..middle-1] su mensie ako pivot 
    // a[middle] je pivot
    // a[middle+1..right] su vacsie ako pivot
    
    /* Rekurzivne utried a[left..middle-1], a[middle+1..right]: */    
    quicksort(a, left, middle-1);  
    quicksort(a, middle+1, right);   
}

Funkcia partition

Funkcia partition na vstupe dostane pole a spolu s hraničnými indexami left a right. Prvok a[left] vyberie ako pivot a postupnosť a[left..right] preusporiada tak, aby pre nejakú hodnotu middle takú, že left <= middle <= right platilo nasledovné:

  • Prvky a[left],...,a[middle-1] sú menšie, než pivot.
  • Prvok a[middle] je pivot.
  • Prvky a[middle+1],...,a[right] sú väčšie alebo rovné ako pivot.

Hodnotu middle potom funkcia partition vráti ako svoj výstup.

Funkcia partition udržiava nasledujúce invarianty:

  • Prvok a[left] je pivot.
  • Prvky a[left+1],...,a[lastSmaller] sú menšie ako pivot.
  • Prvky a[lastSmaller+1],...,a[unknown-1] sú väčšie alebo rovné ako pivot.
  • Prvky a[unknown],...,a[right] sa ešte s pivotom neporovnávali.

Funkcia partition zakaždým porovnáva prvok a[unknown] s pivotom:

  • Ak je menší ako pivot, je nutné „presunúť ho doľava”; vymení ho teda s a[lastSmaller+1] a hodnotu lastSmaller zvýši o jedna.
  • Ak je väčší alebo rovný ako pivot, môže ostať na svojom mieste.

Následne zvýši index unknown o jedna a tento postup opakuje, až kým prejde cez všetky prvky danej časti poľa.

Nakoniec je ešte nutné vymeniť a[left] s a[lastSmaller], čím sa pivot dostane na svoje miesto.

void swap (int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int left, int right) {
    // Ak za pivot chceme zvolit iny prvok, vymenime ho najprv s a[left]
    int pivot = a[left];     
    int lastSmaller = left;
    
    for (int unknown = left + 1; unknown <= right; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }   
    swap(a[left],a[lastSmaller]); 
    return lastSmaller;
}

Ukážka na príklade

Opäť uvažujme pole a = {6, 1, 5, 7, 2, 4, 8, 9, 3, 0}.

Volanie quicksort(0,9) potom utriedi pole a pomocou nasledujúcich rekurzívnych volaní (namiesto quicksort(a,l,h) píšeme zakaždým len sort(l,h)):

sort(0,9) sort(0,5) sort(0,-1)
          .         sort(1,5) sort(1,0)
          .         .         sort(2,5) sort(2,4) sort(2,2)
          .         .         .         .         sort(4,4)
          .         .         .         sort(6,5)
          sort(7,9) sort(7,8) sort(7,7)
                    .         sort(9,8)
                    sort(10,9)

Volania funkcie partition sú počas tohto behu nasledovné:

partition(a,0,9): |6 1 5 7 2 4 8 9 3 0| -> |0 1 5 2 4 3|6|9 7 8|
partition(a,0,5): |0 1 5 2 4 3|6 9 7 8  -> |0|1 5 2 4 3|6 9 7 8
partition(a,1,5):  0|1 5 2 4 3|6 9 7 8  ->  0|1|5 2 4 3|6 9 7 8
partition(a,2,5):  0 1|5 2 4 3|6 9 7 8  ->  0 1|3 2 4|5|6 9 7 8
partition(a,2,4):  0 1|3 2 4|5 6 9 7 8  ->  0 1|2|3|4|5 6 9 7 8
partition(a,7,9):  0 1 2 3 4 5 6|9 7 8| ->  0 1 2 3 4 5 6|8 7|9|
partition(a,7,8):  0 1 2 3 4 5 6|8 7|9  ->  0 1 2 3 4 5 6|7|8|9

Cvičenie:

  • Ako sa bude Quick Sort správať, keď na vstupe dostane už utriedené pole?
  • Ako sa bude správať, keď na vstupe dostane zostupne utriedené pole?

Odhad zložitosti

V ideálnom prípade pivot rozdelí pole na dve rovnako veľké časti. Vtedy je čas výpočtu O(N log N), podobne ako pre Mergesort.

Nepríjemné je, keď pivot je vždy najmenší alebo najväčší prvok v danom úseku. Vtedy je čas O(N2).

  • Aby sme sa vyhli problémom, v praxi sa ako pivot často vyberá náhodný prvok z intervalu.

Výsledný program

#include <iostream>
using namespace std;

const int maxN = 1000;

void swap (int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int left, int right) {
    int pivot = a[left];
    int lastSmaller = left;
    
    for (int unknown = left + 1; unknown <= right; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }   
    swap(a[left],a[lastSmaller]); 
    return lastSmaller;
}

void quicksort(int a[], int left, int right) {
    if (left >= right) {
        return; 
    }
    
    int middle = partition(a, left, right);
        
    quicksort(a, left, middle-1);  
    quicksort(a, middle+1, right);   
}

int main() {
    int N;
    int a[maxN];
    
    cout << "Zadaj pocet cisel: ";
    cin >> N;
    cout << "Zadaj " << N << " cisel: ";
    for (int i = 0; i <= N-1; i++) {
        cin >> a[i];
    }
    
    quicksort(a,0,N-1);
    
    cout << "Utriedene cisla:";
    for (int i = 0; i <= N-1; i++) {
        cout << " " << a[i];
    }                        
    cout << endl;
}

Iná implementácia

Občas sa možno stretnúť aj s nasledujúcou implementáciou triedenia Quick Sort. Skúste samostatne odôvodniť jej správnosť.

void quicksort(int a[], int left, int right) {
    if (left >= right) {
        return;
    }
    
    /* partition */
    int pivot = a[(left + right)/2];
    int i = left;
    int j = right;
    
    while (i <= j) {
        while (a[i] < pivot) i++;
        while (a[j] > pivot) j--;
        if (i <= j) {
            swap(a[i],a[j]);
            i++; j--;
        }
    }
    
    /* rekurzia */    
    quicksort(a, left, j);
    quicksort(a, i, right);   
}

Triediace algoritmy: zhrnutie

Jednoduché triedenia: Bubble Sort, Insertion Sort, Max Sort.

  • Jednoduché, ale pomalé: zložitosť O(n2).

Rekurzívne triedenia založené na technike rozdeľuj a panuj.

  • Rýchlejšie, zložitejšie.
  • Merge Sort: zložitosť O(n log n).
  • Quick Sort: zložitosť O(n2) v najhoršom prípade, pre väčšinu vstupov O(n log n), väčšinou rýchlejší ako Merge Sort.

Reálnu rýchlosť triedení na náhodne zvolenom veľkom vstupe možno porovnať napríklad nasledujúcim programom:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

const int maxN = 100000;

void insertionsort(int a[], int n) {
    for (int i = 1; i < n; i++) {
        int prvok = a[i];
        int kam = i;
        while (kam > 0 && a[kam - 1] > prvok) {
            a[kam] = a[kam - 1];
            kam--;
        }
        a[kam] = prvok;
    }
}

void merge(int a[], int left, int mid, int right) {
    int aux[maxN];
    int i = left;
    int j = mid + 1;
    int k = 0;

    while ((i <= mid) && (j <= right)) {
        if (a[i] <= a[j]) {
            aux[k] = a[i];
            i++;
            k++;
        } else {
            aux[k] = a[j];
            j++;
            k++;
        }
    }

    while (i <= mid) {
        aux[k] = a[i];
        i++;
        k++;
    }

    while (j <= right) {
        aux[k] = a[j];
        j++;
        k++;
    }

    for (int k = left; k <= right; k++) {
        a[k] = aux[k - left];
    }
}

void mergesort(int a[], int left, int right) {
    if (left >= right) {
        return;
    }

    int mid = (left + right) / 2;

    mergesort(a, left, mid);
    mergesort(a, mid + 1, right);

    merge(a, left, mid, right);
}

void swap(int &x, int &y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int partition(int a[], int left, int right) {
    int pivot = a[left];
    int lastSmaller = left;

    for (int unknown = left + 1; unknown <= right; unknown++) {
        if (a[unknown] < pivot) {
            lastSmaller++;
            swap(a[unknown], a[lastSmaller]);
        }
    }
    swap(a[left], a[lastSmaller]);
    return lastSmaller;
}

void quicksort(int a[], int left, int right) {
    if (left >= right) {
        return;
    }

    int mid = partition(a, left, right);

    quicksort(a, left, mid - 1);
    quicksort(a, mid + 1, right);
}

int main() {
    int N;
    int a1[maxN];
    int a2[maxN];
    int a3[maxN];

    cout << "Zadaj pocet nahodnych cisel v poli: ";
    cin >> N;

    srand(time(NULL));
    for (int i = 0; i <= N - 1; i++) {
        a1[i] = rand() % 1000;
        a3[i] = a2[i] = a1[i];
    }

    clock_t start1, end1, start2, end2, start3, end3;

    start1 = clock();
    insertionsort(a1, N);
    end1 = clock();

    start2 = clock();
    mergesort(a2, 0, N - 1);
    end2 = clock();

    start3 = clock();
    quicksort(a3, 0, N - 1);
    end3 = clock();

    cout << "Insertion sort: " 
	 << (end1 - start1) * 1.0 / CLOCKS_PER_SEC << " CPU sekund" << endl;
    cout << "Merge sort: " 
	 << (end2 - start2) * 1.0 / CLOCKS_PER_SEC << " CPU sekund" << endl;
    cout << "Quick sort: " 
	 << (end3 - start3) * 1.0 / CLOCKS_PER_SEC << " CPU sekund" << endl;

}

Pre N = 100000 môžeme dostať napríklad nasledujúci výstup (líši sa od počítača k počítaču a od volania k volaniu):

Insertion sort: 7.474 CPU sekund
Merge sort: 0.076 CPU sekund
Quick sort: 0.032 CPU sekund

Problém batoha (Knapsack problem)

Dnes sme videli ako použiť rekurziu v rýchlych algoritmoch, teraz sa však vráťme k prehľadávaniu s návratom z minulej prednášky, čo je pomalá metóda pre prípady, keď nepoznáme lepší algoritmus.

Metódu prehľadávania s návratom využijeme na riešenie problému batoha. Ide o dôležitý problém, s ktorým sa ešte počas štúdia stretnete. Predstaviť si ho môžeme napríklad takto:

  • Zlodej sa vlúpal do obchodu, v ktorom našiel niekoľko predmetov.
  • Pozná cenu aj hmotnosť predmetov.
  • Z obchodu dokáže odniesť iba lup nepresahujúci nosnosť svojho batoha.
  • Ktoré predmety má zlodej odcudziť, aby ich celková hmotnosť nepresahovala nosnosť batoha a aby odišiel s čo najcennejším lupom?

Vstup nášho programu bude vyzerať napríklad nejako takto:

Zadaj pocet predmetov v obchode: 3
Zadaj hmotnost a cenu predmetu 1: 5 9
Zadaj hmotnost a cenu predmetu 2: 4 6
Zadaj hmotnost a cenu predmetu 3: 4 4
Zadaj nosnost batoha: 8

Výstup programu na horeuvedenom vstupe potom bude takýto:

Zober nasledujuce predmety: 2 3
Celkova hodnota lupu: 10

Pri reálnom použití nosnosť batoha môže reprezentovať dostupné zdroje, napr. výpočtový čas na serveri, dostupných pracovníkov, veľkosť rozpočtu a pod, a predmety sú potenciálne úlohy, z ktorých si chceme vybrať podmnožinu, ktorú by sme s danými zdrojmi vedeli vykonať a dosiahnuť čo najvyšší zisk alebo iný ukazovateľ.

Prvé riešenie: preskúmanie všetkých podmnožín

  • Preskúmame všetky podmnožiny množiny predmetov v obchode, čiže všetky potenciálne lupy.
  • Na to upravíme program generujúci všetky podmnožiny danej množiny.
  • Pre každú podmnožinu namiesto výpisu spravíme nasledovné:
    • Spočítame celkovú hmotnosť a cenu nájdeného potenciálneho lupu.
    • Ak hmotnosť tohto lupu nepresahuje nosnosť batoha, porovnáme jeho cenu s najlepším doposiaľ nájdeným lupom.
    • Ak je cennejší, ako doposiaľ najlepší lup, ide o nového kandidáta na optimálny lup a zapamätáme si ho.


  • Pre jednoduchosť použijeme v programe globálne premenné, lebo potrebujeme veľa údajov
    • Globálne premenné spôsobujú problémy vo väčších programoch: mená premenných sa môžu "biť", môžeme si omylom prepísať číslo dôležité v inej časti programu
    • Mohli by sme si tiež spraviť struct obsahujúci všetky premenné potrebné v rekurzii a odovzdávať si ten


Podmnožiny budeme reprezentovať poľom typu bool, v ktorom si pre každý predmet pamätáme, či do danej podmnožiny patrí.

#include <iostream>
using namespace std;

const int maxN = 100;

/* Struktura reprezentujuca jeden predmet */
struct predmet {
    int hmotnost;
    int cena;
}; 

/* Globalne premenne pouzivane v rekurzii: */

// pocet predmetov v obchode
int N;                         
// pole s udajmi o jednotlivych predmetoch
predmet a[maxN];                 
// nosnost batoha
int nosnost;                     

// najcennejsi doposial najdeny lup
bool najlepsiLup[maxN];          
// jeho cena (kazdy lup bude urcite cennejsi ako -1)
int cenaNajlepsiehoLupu = -1;    

int spocitajHmotnostLupu(bool lup[]) {
    int hmotnost = 0;
    for (int i = 0; i < N; i++) {
        if (lup[i]) {
            hmotnost += a[i].hmotnost;
        }
    }
    return hmotnost;
}

int spocitajCenuLupu(bool lup[]) {
    int cena = 0;
    for (int i = 0; i < N; i++) {
        if (lup[i]) {
            cena += a[i].cena;
        }
    }
    return cena;
}

void vypisLup(bool lup[]) {
    cout << "Zober nasledujuce predmety:";
    for (int i = 0; i < N; i++) {
        if (lup[i]) {
             cout << " " << i + 1;
        }            
    }
    cout << endl;
}

/* Generovanie vsetkych moznych lupov (podmnozin predmetov) */
void generujLupy(bool lup[], int index) {
    /* V poli lup[] dlzky N postupne generujeme podmnoziny.
       O hodnotach lup[0],...,lup[index-1] uz je rozhodnute.
       Postupne vygenerujeme vsetky moznosti 
       pre lup[index],...,lup[N-1].
       Kazdy lup porovname s doposial najlepsim 
       a v pripade potreby optimum aktualizujeme.    
    */
    if (index == N) {                                
        // Lup je vygenerovany; zisti, ci ho batoh unesie.                               
        if (spocitajHmotnostLupu(lup) <= nosnost) {  
            // Ak ano, porovnaj cenu s doposial najlepsim.
            int cenaLupu = spocitajCenuLupu(lup);
            if (cenaLupu > cenaNajlepsiehoLupu) {    
                // Ak je najdeny lup drahsi, uloz ho
                cenaNajlepsiehoLupu = cenaLupu;
                for (int i = 0; i < N; i++) {
                    najlepsiLup[i] = lup[i];
                } 
            }
        }
    } else {                                         
        // Lup este nie je vygenerovany,
        // skus obe moznosti pre lup[index]. 
        lup[index] = false;
        generujLupy(lup, index+1);
        lup[index] = true;
        generujLupy(lup, index+1);
    }
} 

int main() {
    cout << "Zadaj pocet predmetov v obchode: ";
    cin >> N;
    for (int i = 0; i < N; i++) {
        cout << "Zadaj hmotnost a cenu predmetu " 
             << (i+1) << ": ";
        cin >> a[i].hmotnost >> a[i].cena;
    }
    cout << "Zadaj nosnost batoha: ";
    cin >> nosnost;
    
    bool lup[maxN];
    generujLupy(lup, 0);
    
    vypisLup(najlepsiLup);
    cout << "Celkova hodnota lupu: " 
         << cenaNajlepsiehoLupu << endl;
}

Cvičenie: Čo bude program robiť, keď každý predmet má hmotnosť väčšiu ako nosnosť batoha?

Optimalizácia č. 1: ukončenie prehľadávania vždy, keď je prekročená nosnosť

Keď je už po vygenerovaní nejakej podmnožiny (čiže prvých niekoľko hodnôt poľa lup) jasné, že hmotnosť lupu bude presahovať nosnosť batoha, možno túto vetvu prehľadávania ukončiť.

Okrem samotnej funkcie generujLupy je potrebné prispôsobiť aj funkciu spocitajHmotnostLupu tak, aby ju bolo možné aplikovať aj na neúplne vygenerované podmnožiny.

/* Potrebujeme vediet spocitat hmotnost len pre cast predmetov: */

int spocitajHmotnostLupu(bool lup[], int pokial) {
    int hmotnost = 0;
    for (int i = 0; i <= pokial; i++) {
        if (lup[i]) {
            hmotnost += a[i].hmotnost;
        }
    }
    return hmotnost;
}

void generujLupy(bool lup[], int index) {
    if (spocitajHmotnostLupu(lup, index-1) > nosnost) {
        // Ak dosial vygenerovana cast lupu presahuje nosnost batoha, 
        // mozno prehladavanie ukoncit
        return;  
    }
    if (index == N) {
        int cenaLupu = spocitajCenuLupu(lup);
        if (cenaLupu > cenaNajlepsiehoLupu) {
            cenaNajlepsiehoLupu = cenaLupu;
            for (int i = 0; i < N; i++) {
                najlepsiLup[i] = lup[i];
            } 
        }
    } else {
        lup[index] = false;
        generujLupy(lup, index+1);
        lup[index] = true;
        generujLupy(lup, index+1);
    }
}

Cvičenie: Čo bude program robiť, keď každý predmet má hmotnosť väčšiu ako nosnosť batoha?

Optimalizácia č. 2: hmotnosť a cenu lupu netreba zakaždým počítať odznova

Predchádzajúci program vždy znovu a znovu prepočítava hmotnosť a cenu lupu, aj keď sa zoznam vybraných predmetov zmení iba trochu. Namiesto toho môžeme cenu a hmotnosť doposiaľ vygenerovanej časti lupu predávať funkcii generuj ako parameter.

#include <iostream>
using namespace std;

const int maxN = 100;

struct predmet {
    int hmotnost;
    int cena;
}; 

int N;
predmet a[maxN];
int nosnost;

bool najlepsiLup[maxN];
int cenaNajlepsiehoLupu = -1;

void vypisLup(bool lup[]) {
    cout << "Zober nasledujuce predmety:";
    for (int i = 0; i < N; i++) {
        if (lup[i]) {
            cout << " " << i + 1;
        }    
    }
    cout << endl;
}

void generujLupy(bool lup[], int index, int hmotnostLupu, int cenaLupu) {
    if (hmotnostLupu > nosnost) {
        return;
    }
    if (index == N) {
        if (cenaLupu > cenaNajlepsiehoLupu) {
            cenaNajlepsiehoLupu = cenaLupu;
            for (int i = 0; i < N; i++) {
                najlepsiLup[i] = lup[i];
            } 
        }
    } else {
        lup[index] = false;
        generujLupy(lup, index+1, hmotnostLupu, cenaLupu);
        lup[index] = true;
        generujLupy(lup, index+1, 
                    hmotnostLupu + a[index].hmotnost, 
                    cenaLupu + a[index].cena);
    }
} 

int main() {
    cout << "Zadaj pocet predmetov v obchode: ";
    cin >> N;
    for (int i = 0; i < N; i++) {
        cout << "Zadaj hmotnost a cenu predmetu " 
             << (i+1) << ": ";
        cin >> a[i].hmotnost >> a[i].cena;
    }
    cout << "Zadaj nosnost batoha: ";
    cin >> nosnost;
    
    bool lup[maxN];
    // Doposial nie je nic vygenerovane; 
    // hmotnost aj cena lupu su zatial nulove
    generujLupy(lup, 0, 0, 0); 
    
    cout << endl;
    vypisLup(najlepsiLup);
    cout << "Celkova hodnota lupu: " 
         << cenaNajlepsiehoLupu << endl;
}