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 18: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
(Vytvorená stránka „==Rad pomocou spájaného zoznamu== Pri implementácii radu pomocou spájaného zoznamu rozšírime spájané zoznamy zo 14. prednášky o smerník…“)
 
 
(48 medziľahlých úprav od 3 ďalších používateľov nie je zobrazených)
Riadok 1: Riadok 1:
==Rad pomocou spájaného zoznamu==
+
== Oznamy ==
  
Pri implementácii radu pomocou spájaného zoznamu rozšírime spájané zoznamy zo [[#Prednáška 14|14. prednášky]] o smerník <tt>last</tt> na posledný prvok zoznamu. Bude tak možné jednoducho vkladať prvky na koniec zoznamu, ako aj odoberať prvky zo začiatku zoznamu.
+
Budúci utorok bude na cvičeniach rozcvička na papieri.
 +
* Bude pokrývať učivo po prednášku 17.
 +
* Dôležitý tréning na semestrálny test.
  
Výhodou oproti implementácii radu pomocou poľa bude, podobne ako pri zásobníkoch, eliminácia obmedzenia na maximálny počet prvkov v rade.
+
Blíži sa '''semestrálny test'''.
 +
* Bude sa konať v stredu '''11.12. o 18:10''' v posluchárňach F1 a F2 v trvaní 90 minút.
 +
* Viac informácií na stránke [[Zimný semester, semestrálny test]].
 +
 
 +
Od pondelka 2.12. 19:00 sa môžete hlásiť na termín skúšky pri počítači.
 +
* Ak vidíte konflikt niektorého termínu s hromadnou skúškou alebo písomkou z iného predmetu, dajte nám vedieť čím skôr.
 +
* Kapacita termínov bude obmedzená, prihláste sa teda radšej skôr, neskôr to môžete zmeniť.
 +
* Prihlásiť a odhlásiť sa dá najviac deň vopred.
 +
* Skúšku 20.12. môžete robiť iba ak spravíte test 11.12. aspoň na 50% bodov.
 +
* Decembrový termín odporúčame hlavne študentom, ktorým programovanie nerobí problémy.
 +
* Viac informácií o skúške na prednáške v stredu 11.12.
 +
 
 +
== Opakovanie: zásobník a rad ==
 +
* '''Zásobník''' (angl. '''stack''') a '''rad''' alebo '''front''' (angl. '''queue''') sú abstraktné dátové typy, ktoré udržiavajú postupnosť nejakých prvkov.
 +
* Obidva typy podporujú vloženie prvku a výber prvky
 +
* Zo zásobníka sa vyberá prvok, ktorý v ňom pobudol najkratšie, z radu prvok, ktorý v ňom bol najdlhšie
 +
* Zásobník tak pripomína stĺpec čistých tanierov v reštaurácii, rad pripomína rad pri pokladni
 +
 
 +
Konkrétne funkcie hlavičky funkcií
 +
<pre>
 +
void init(stack &s);
 +
bool isEmpty(stack &s);
 +
void push(stack &s, dataType item); // vlozenie prvku
 +
dataType pop(stack &s);    // vyber prvku
 +
dataType peek(stack &s);
 +
void destroy(stack &s);
 +
 
 +
void init(queue &q);
 +
bool isEmpty(queue &q);
 +
void enqueue(queue &q, dataType item); // vlozenie prvku
 +
dataType dequeue(queue &q);  // vyber prvku
 +
dataType peek(queue &q);
 +
void destroy(queue &q);
 +
</pre>
 +
 
 +
* Pre obidva typy sme videli implementáciu v poli aj v spájanom zozname
 +
* Hlavné funkcie vkladania a vyberania sú v obidvoch implementáciách rýchle a jednoduché
 +
 
 +
Videli sme tiež, že obidve štruktúry sa dajú použiť na ukladanie dát alebo úloh, ktoré ešte treba vyriešiť
 +
* Dnes uvidíme ďalšie príklady
 +
 
 +
 
 +
== Použitie zásobníka a radu: nerekurzívny Quick Sort ==
 +
 
 +
Pripomeňme si triedenie Quick Sort z [[Prednáška 11#Quick_Sort|11. prednášky]]:
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
#include <cassert>
+
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() {
 +
  // ...
 +
  quicksort(a, 0, N-1);
 +
  // ...
 +
}
 +
</syntaxhighlight>
  
// ...
+
Namiesto rekurzie môžeme použiť aj zásobník úsekov, ktoré ešte treba dotriediť.
  
struct node {
+
<syntaxhighlight lang="C++">
     dataType data;
+
struct usek {
     node *next;
+
     int left;
 +
     int right;
 
};
 
};
  
struct queue {
+
typedef usek dataType;
    node *first; // Smernik na prvy prvok radu (spajaneho zoznamu). Ak je rad prazdny, ma hodnotu NULL.
+
 
    node *last;  // Smernik na posledny prvok radu (spajaneho zoznamu). Ak je rad prazdny, ma hodnotu NULL.
+
/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */
};
+
 
 +
/* Sem pridu funkcie swap a partition rovnake ako vyssie. */
  
/* Inicializuje prazdny rad */
+
void quicksort(int a[], int n) {
void init(queue &q) {
+
    stack s;
    q.first = NULL;
+
    init(s);
    q.last = NULL;
+
   
 +
    usek u;
 +
    u.left = 0;
 +
    u.right = n-1;
 +
    push(s,u);
 +
   
 +
    while (!isEmpty(s)) {
 +
        u = pop(s);
 +
        // vynechame useky dlzky 0 a 1
 +
        if (u.left >= u.right) {
 +
            continue;
 +
        }
 +
       
 +
        int middle = partition(a, u.left, u.right);
 +
       
 +
        usek u1;
 +
        u1.left = u.left;
 +
        u1.right = middle - 1;
 +
        usek u2;
 +
        u2.left = middle + 1;
 +
        u2.right = u.right;
 +
        push(s,u2);
 +
        push(s,u1);
 +
    }
 +
   
 +
    destroy(s);
 
}
 
}
  
/* Zisti, ci je rad prazdny */
+
int main() {
bool isEmpty(queue &q) {
+
  // ...
    return q.first == NULL;
+
  quicksort(a, N);
 +
  // ...
 
}
 
}
 +
</syntaxhighlight>
  
/* Prida prvok item na koniec radu */
+
* Tento program triedi úseky v rovnakom poradí, ako rekurzívny Quick Sort, lebo po rozdelení poľa na dve časti dá na vrch zásobníka úsek zodpovedajúci jeho ľavej časti. Až keď sa táto ľavá časť a všetky podúlohy, ktoré z nej vzniknú, spracuje, dôjde na spracovanie pravej časti poľa.
void enqueue(queue &q, dataType item) {
+
* Pri triedení Quick Sort však na tomto poradí nezáleží, takže by sme mohli jednotlivé úseky vkladať na zásobník aj v opačnom poradí.
     node *tmp = new node;
+
* Alebo by sme namiesto zásobníka mohli použiť rad. Potom by najskôr rozdelil ľavú aj pravú časť na ďalšie podčasti a potom by delil každú z týchto podčastí atď.
     tmp->data = item;
+
 
     tmp->next = NULL;
+
'''Na zamyslenie:''' ako by mohla vyzerať nerekurzívna verzia triedenia [[Prednáška_11#Triedenie_zlu.C4.8Dovan.C3.ADm_.28Merge_Sort.29|Merge Sort]]? Prečo sa nedá použiť rovnaký prístup ako pri triedení Quick Sort?
     if (isEmpty(q)) {
+
 
         q.first = tmp;
+
==Vyfarbovanie súvislých oblastí==
         q.last = tmp;
+
 
     } else {
+
* Uvažujme obrázok pozostávajúci z <tt>m</tt> krát <tt>n</tt> štvorčekov (pixelov).
         q.last->next = tmp;
+
* Farbu každého pixelu zapíšeme do jedného políčka dvojrozmerného poľa s <tt>m</tt> riadkami a <tt>n</tt> stĺpcami.
         q.last = tmp;
+
* V našom jednoduchom príklade budeme pracovať iba s piatimi farbami, ktoré budeme reprezentovať číslami <tt>0,...,4</tt> podľa nasledujúceho poľa (napríklad číslo <tt>0</tt> teda reprezentuje bielu farbu):
 +
<syntaxhighlight lang="C++">
 +
const char *farby[5] = {"white", "blue", "black", "yellow", "red"};
 +
</syntaxhighlight>
 +
 
 +
Napríklad obrázok
 +
 
 +
[[Súbor:Matica1.png|400px]]
 +
 
 +
tak môže byť reprezentovaný nasledujúcim textovým súborom obsahujúcim najprv rozmery matice (čísla <tt>m</tt> a <tt>n</tt>) a za nimi samotné prvky matice:
 +
<pre>
 +
11 17
 +
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 +
0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0
 +
0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0
 +
0 0 0 1 0 0 0 0 0 2 2 1 2 2 2 2 0
 +
0 0 0 1 0 0 0 0 0 2 0 1 0 0 0 2 0
 +
0 2 2 2 2 2 2 2 0 2 2 1 2 2 2 2 0
 +
0 2 0 1 0 0 0 2 0 0 0 1 0 0 0 0 0
 +
0 2 0 1 0 0 0 2 0 0 0 1 0 1 1 1 1
 +
0 2 0 1 1 1 1 2 1 1 1 1 0 1 0 0 1
 +
0 2 0 0 0 0 0 2 0 0 0 0 0 1 1 1 1
 +
0 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0
 +
</pre>
 +
 
 +
Zameriame sa teraz na nasledujúci problém: používateľ zvolí (zadá na konzolu) súradnice niektorého štvorčeka a cieľom je ofarbiť nejakou farbou (napríklad červenou) celú súvislú oblasť rovnakej farby obsahujúcu daný štvorček. Napríklad pre obrazec vyššie a vstupné súradnice <tt>(2,1)</tt> (teda štvorček v treťom riadku a druhom stĺpci, keďže matica sa bude indexovať od nuly) by mal byť výstupom nasledujúci obrazec: 
 +
 
 +
[[Súbor:Matica2.png|400px]]
 +
 
 +
Podobný problém je napríklad často potrebné riešiť v rôznych nástrojoch na prácu s grafikou a podobne.
 +
 
 +
* Jednoduché útvary, napr. obdĺžnik, by sa dali vyfarbiť jednoduchými cyklami, naša súvislá oblasť ale môže mať veľmi zložitý tvar, s mnohými zákrutami, vetveniami a dierami a podobne.
 +
* Pre každé políčko, ktoré prefarbíme, nesmieme zabudnúť skontrolovať všetkých jeho susedov a ak majú rovnakú farbu ako malo pôvodné políčko, tak prefarbiť aj ich, ich susedov, susedov ich susedov atď.
 +
* Toto vieme ľahko zapísať rekurzívne: zafarbíme jedno políčko a potom rekurzívne zafarbujeme celú dosiahnuteľnú oblasť pre každého suseda rovnakej farby, akú malo pôvodne prefarbené políčko.
 +
* Táto podúloha je menšia, lebo aspoň jedno políčko z nej ubudlo prvým prefarbením.
 +
 
 +
===Vyfarbovanie pomocou rekurzívnej funkcie===
 +
 
 +
Nasledujúca rekurzívna funkcia <tt>vyfarbi</tt> prefarbí políčko so súradnicami <tt>(riadok, stlpec)</tt> na cieľovú farbu <tt>farba</tt> a následne sa rekurzívne zavolá pre všetkých susedov tohto políčka, ktoré sú zafarbené pôvodnou farbou prefarbovanej oblasti.
 +
 
 +
<syntaxhighlight lang="C++">
 +
/* Prefarbi súvislú jednofarebnú oblasť obsahujúcu
 +
* pozíciu (riadok,stlpec) na farbu s číslom farba. */
 +
void vyfarbi(int **a, int m, int n,
 +
            int riadok, int stlpec, int farba) {
 +
     int staraFarba = a[riadok][stlpec];
 +
     if (staraFarba == farba) {
 +
        return;
 +
     }
 +
    a[riadok][stlpec] = farba;
 +
     if (riadok - 1 >= 0
 +
        && a[riadok - 1][stlpec] == staraFarba) {
 +
         vyfarbi(a, m, n, riadok - 1, stlpec, farba);
 +
    }
 +
    if (riadok + 1 < m
 +
         && a[riadok + 1][stlpec] == staraFarba) {
 +
        vyfarbi(a, m, n, riadok + 1, stlpec, farba);
 +
     }
 +
    if (stlpec - 1 >= 0
 +
        && a[riadok][stlpec - 1] == staraFarba) {
 +
         vyfarbi(a, m, n, riadok, stlpec - 1, farba);
 +
    }
 +
    if (stlpec + 1 < n
 +
         && a[riadok][stlpec + 1] == staraFarba) {
 +
        vyfarbi(a, m, n, riadok, stlpec + 1, farba);
 
     }
 
     }
 
}  
 
}  
 +
</syntaxhighlight>
 +
 +
Proces vyfarbovania môžeme animovať pomocou našej knižnice SVGdraw
 +
* [[:Media:Animacia1.svg|Výsledná animácia]]
 +
 +
V animácii okrem zmeny farby štvorčeka meníme aj farbu jeho rámčeka: pri zavolaní rekurzie sa zmení na hnedú a po skončení spracovania štvorčeka aj jeho susedov sa zmení na sivú. Hnedé sú teda vždy rámčeky štvorčekov uložené na zásobníku volaní.
 +
 +
===Program s animáciou===
 +
 +
* Program na vyfarbovanie súvislých oblastí obsahuje funkcie na inicializáciu matice, jej načítanie zo súboru, vykresľovanie jednotlivých štvorčekov a celej matice, ako aj uvoľnenie pamäte. Všetky tieto funkcie pracujú podobne ako pri príklade s výškovou mapou z [[Prednáška 13|prednášky 13]].
 +
* Funkcia <tt>main</tt> načíta maticu zo súboru <tt>vstup.txt</tt> a animáciu uloží do súboru <tt>matica.svg</tt>.
 +
* Do rekurzívnej funkcie pridáme volania funkcie <tt>vykresliStvorcek</tt>, ktoré aktuálny štvorček v obrázku prekreslia novou farbou a menia aj farbu rámčeka. Za ňou vždy zavoláme funkciu <tt>drawing.wait</tt>, ktorá animáciu pozdrží, aby sme zmeny stihli na obrázku sledovať.
 +
 +
<syntaxhighlight lang="C++">
 +
#include "SVGdraw.h"
 +
#include <cstdio>
 +
#include <cassert>
 +
 +
const char *farby[5] = {"white", "blue", "black", "yellow", "red"};
 +
 +
const int stvorcek = 40;    // velkost stvorceka v pixeloch
 +
const int hrubkaCiary = 2;  // hrubka ciary v pixeloch
 +
const double pauza = 0.3;  // pauza po kazdom kroku vyfarbovania v sekundach
 +
 +
/* Vytvori maticu s n riadkami a m stlpcami. */
 +
int **vytvorMaticu(int m, int n) {
 +
    int **a;
 +
    a = new int *[m];
 +
    for (int i = 0; i < m; i++) {
 +
        a[i] = new int[n];
 +
    }
 +
    return a;
 +
}
  
/* Odoberie prvok zo zaciatku radu a vrati jeho hodnotu */
+
/* Uvolni pamat matice a s n riadkami a m stlpcami. */
dataType dequeue(queue &q) {
+
void zmazMaticu(int **a, int m) {
     assert(!isEmpty(q));
+
     for (int i = 0; i < m; i++) {
    dataType result = q.first->data;
+
         delete[] a[i];
    node *tmp = q.first->next;
+
     }
    delete q.first;
+
     delete[] a;
    if (tmp == NULL) {
 
         q.first = NULL;
 
        q.last = NULL;
 
     } else {
 
        q.first = tmp;
 
     }
 
    return result;
 
 
}
 
}
  
/* Vrati prvok zo zaciatku radu, ale necha ho v rade */
+
/* Vykresli stvorcek v riadku i a stlpci j
dataType peek(queue &q) {
+
  s farbou vyplne farba a farbou ciary farbaCiary. */
     assert(!isEmpty(q));
+
void vykresliStvorcek(int i, int j,
     return q.first->data;
+
                      const char * farba,
}        
+
                      const char * farbaCiary,
 +
                      SVGdraw &drawing) {
 +
     drawing.setLineColor(farbaCiary);
 +
    drawing.setLineWidth(hrubkaCiary);
 +
    drawing.setFillColor(farba);
 +
     drawing.drawRectangle(j * stvorcek, i * stvorcek,
 +
                          stvorcek, stvorcek);
 +
}
  
/* Uvolni pamat */
+
/* Vykresli maticu a s n riadkami a m stlpcami. */
void destroy(queue &q) {
+
void vykresliMaticu(int **a, int m, int n, SVGdraw &drawing) {
     while (!isEmpty(q)) {
+
    for (int i = 0; i < m; i++) {
        dequeue(q);
+
        for (int j = 0; j < n; j++) {
 +
            vykresliStvorcek(i, j, farby[a[i][j]], "lightgray", drawing);
 +
        }
 +
    }
 +
}
 +
 
 +
/* Nacita z textoveho suboru, na ktory ukazuje fr,
 +
  prvky matice a s n riadkami a m stlpcami. */
 +
void nacitajMaticu(FILE *fr, int **a, int m, int n) {
 +
     assert(fr != NULL);
 +
    for (int i = 0; i < m; i++) {
 +
        for (int j = 0; j < n; j++) {
 +
            fscanf(fr, "%d", &a[i][j]);
 +
        }
 
     }
 
     }
 
}
 
}
</syntaxhighlight>
 
  
== Použitie zásobníka a radu ==
+
/* Prefarbi suvislu jednofarebnu oblast obsahujucu
Zásobník aj rad často uchovávajú dáta určené na spracovanie, zoznamy úloh, atď. Rad sa zvyčajne používa v prípadoch, keď je žiadúce zachovať ich poradie. Typicky môže ísť o situácie, keď jeden proces generuje úlohy spracúvané iným procesom, napríklad:
+
* poziciu (riadok,stlpec) na farbu s cislom farba. */
* Textový procesor pripravuje strany na tlač a vkladá ich do radu, z ktorého ich tlačiareň (resp. jej ovládač) postupne vyberá.
+
void vyfarbi(int **a, int m, int n,
* Sekvenčne vykonávané výpočtové úlohy čakajú v rade na spustenie.
+
            int riadok, int stlpec, int farba,
* Zákazníci čakajú na zákazníckej linke na voľného operátora.
+
            SVGdraw &drawing) {
* Pasažieri na standby čakajú na voľné miesto v lietadle.
+
    int staraFarba = a[riadok][stlpec];
 +
    if (staraFarba == farba) {
 +
        return;
 +
    }
 +
    a[riadok][stlpec] = farba;
 +
    vykresliStvorcek(riadok, stlpec, farby[farba],
 +
                    "brown", drawing);
 +
    drawing.wait(pauza);
 +
    if (riadok - 1 >= 0 && a[riadok - 1][stlpec] == staraFarba) {
 +
        vyfarbi(a, m, n, riadok - 1, stlpec, farba, drawing);
 +
    }
 +
    if (riadok + 1 < m && a[riadok + 1][stlpec] == staraFarba) {
 +
        vyfarbi(a, m, n, riadok + 1, stlpec, farba, drawing);
 +
    }
 +
    if (stlpec - 1 >= 0 && a[riadok][stlpec - 1] == staraFarba) {
 +
        vyfarbi(a, m, n, riadok, stlpec - 1, farba, drawing);
 +
    }
 +
    if (stlpec + 1 < n && a[riadok][stlpec + 1] == staraFarba) {
 +
        vyfarbi(a, m, n, riadok, stlpec + 1, farba, drawing);
 +
    }
 +
    vykresliStvorcek(riadok, stlpec, farby[farba],
 +
                    "lightgray", drawing);
 +
    drawing.wait(pauza);
 +
}
 +
 
 +
int main() {
 +
    FILE *fr = fopen("vstup.txt", "r");
 +
    assert(fr != NULL);
 +
 
 +
    // nacitanie rozmerov matice
 +
    int m, n;
 +
    fscanf(fr, "%d %d", &m, &n); 
 +
    // vytvorenie matice a nacitanie jej prvkov 
 +
    int **a = vytvorMaticu(m, n);
 +
    nacitajMaticu(fr, a, m, n);       
 +
    fclose(fr);
 +
       
 +
    SVGdraw drawing(n * stvorcek, m * stvorcek, "matica.svg");
 +
    vykresliMaticu(a, m, n, drawing);
 +
 
 +
    // nacitanie suradnic pociatocneho stvorceka
 +
    int riadok, stlpec;
 +
    scanf("%d", &riadok);
 +
    scanf("%d", &stlpec);
  
Zásobník sa, ako o niečo implementačne jednoduchší koncept, zvyčajne používa v situáciách, keď na poradí spracúvania nezáleží, alebo keď je žiadúce vstupné poradie obrátiť. Najvýznamnejší príklad situácie druhého typu je nasledujúci:
+
    vyfarbi(a, m, n, riadok, stlpec, 4, drawing);
* Operačný systém ukladá lokálne premenné volaných funkcií na tzv. ''zásobníku volaní'' (angl. ''call stack''), čo umožňuje používanie rekurzie.
 
* Rekurzívne programy sa dajú prepísať na nerekurzívne pomocou &bdquo;ručne vytvoreného&rdquo; zásobníka (na budúcej prednáške si ako príklad ukážeme nerekurzívnu verziu triedenia ''Quick Sort'').
 
  
=== Príklad: kontrola uzátvorkovania ===
+
    drawing.finish();
 +
    zmazMaticu(a, m);
 +
}
 +
</syntaxhighlight>
  
Ako jednoduchý príklad na použitie zásobníka uvažujme nasledujúcu situáciu: na vstupe je daný reťazec pozostávajúci (okrem prípadných ďalších znakov, ktoré možno ignorovať) zo zátvoriek <tt>(,),[,],{,}</tt>. Úlohou je zistiť, či je tento reťazec dobre uzátvorkovaný. To znamená, že:
+
==Počítanie ostrovov==
* Pre každú uzatváraciu zátvorku musí byť posledná dosiaľ neuzavretá otváracia zátvorka rovnakého typu, pričom musí existovať aspoň jedna dosiaľ neuzavretá zátvorka.
+
Obrázok, s ktorým sme pracovali vyššie, môže reprezentovať napríklad jednoduchú mapu súostrovia, kde more je znázornené modrou farbou a pevnina je znázornená žltou farbou. Úlohou môže byť zistiť počet ostrovov. Ten môžeme zistiť napríklad takto:
* Každá otváracia zátvorka musí byť niekedy neskôr uzavretá.  
+
* Prechádzame postupne všetky políčka mapy.
 +
* Ak narazíme na pevninu (t. j. žlté políčko), zvýšime doposiaľ nájdený počet ostrovov o 1 a ofarbíme celý ostrov (napríklad) na červeno.
 +
* Ak narazíme na ďalšie žlté políčko, opäť urobíme to isté.
 +
* Toto robíme, až kým prejdeme cez všetky políčka mapy.
  
Príklady očakávaného vstupu a výstupu:
+
Príklad mapy a jej zobrazenie pred začiatkom hľadania ostrovov, po nájdení prvých troch ostrovov a po nájdení všetkých ostrovov:
 
<pre>
 
<pre>
()
+
11 17
Retazec je dobre uzatvorkovany
+
1 1 1 1 1 1 3 1 1 1 1 1 1 1 1 1 1
 +
1 1 1 3 3 3 3 1 1 3 1 1 1 1 3 3 1
 +
1 1 1 3 3 1 1 1 1 1 1 1 1 1 3 3 1
 +
1 1 3 3 1 1 1 1 1 3 3 1 3 3 3 3 1
 +
1 1 1 3 3 1 1 1 1 3 1 1 3 3 3 3 1
 +
1 3 3 3 3 1 3 3 1 3 3 1 3 3 3 3 1
 +
1 1 3 3 1 1 1 3 1 1 1 1 1 1 1 1 1
 +
1 1 1 1 1 1 1 3 3 3 1 1 3 3 3 1 1
 +
1 1 1 1 1 1 1 3 3 3 1 1 3 1 3 1 1
 +
1 1 3 3 3 3 1 3 1 1 1 1 3 3 3 1 1
 +
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 +
</pre>
 +
<gallery heights=165px widths=255px>
 +
Image:Mapa1.png 
 +
Image:Mapa2.png
 +
Image:Mapa3.png 
 +
</gallery>
 +
 
 +
Do programu vyššie teda dorobíme funkciu
 +
<syntaxhighlight lang="C++">
 +
int najdiOstrovy(int **a, int m, int n, SVGdraw &drawing) {
 +
    int ostrovov = 0;
 +
    for (int i = 0; i < m; i++) {
 +
        for (int j = 0; j < n; j++) {
 +
            if (a[i][j] == 3) {
 +
                ostrovov++;
 +
                vyfarbi(a, m, n, i, j, 4, drawing);
 +
            }
 +
        }
 +
    }
 +
    return ostrovov;
 +
}
 +
</syntaxhighlight>
 +
 
 +
a funkciu <tt>main</tt> môžeme zmeniť napríklad takto:
 +
<syntaxhighlight lang="C++">
 +
int main() {
 +
    FILE *fr = fopen("ostrovy.txt", "r");
 +
    assert(fr != NULL);
 +
    int m, n;
 +
    fscanf(fr, "%d %d", &m, &n);
 +
    int **a = vytvorMaticu(m, n);
 +
    nacitajMaticu(fr, a, m, n);
 +
    fclose(fr);
 +
       
 +
    SVGdraw drawing(n * stvorcek, m * stvorcek, "mapa.svg");
 +
    vykresliMaticu(a, m, n, drawing);
  
nejaky text bez zatvoriek
+
    int pocetOstrovov = najdiOstrovy(a, m, n, drawing);
Retazec je dobre uzatvorkovany
+
    printf("Pocet ostrovov je %d.\n", pocetOstrovov);
  
[((({}[])[]))]()
+
    drawing.finish();
Retazec je dobre uzatvorkovany
+
    zmazMaticu(a, m);
 +
}
 +
</syntaxhighlight>
  
[[#))
+
* [[:Media:Animacia2.svg|Výsledná animácia]]
Retazec nie je dobre uzatvorkovany
 
  
())(
+
''Cvičenie:'' upravte program tak, aby ešte navyše zistil, či má niektorý z ostrovov jazero.
Retazec nie je dobre uzatvorkovany
 
  
((
+
==Nerekurzívne vyfarbovanie==
Retazec nie je dobre uzatvorkovany
 
  
((cokolvek
+
===Vyfarbovanie s použitím zásobníka===
Retazec nie je dobre uzatvorkovany
 
</pre>
 
  
Túto úlohu rieši nasledujúci program, ktorý postupne prechádza cez vstupný reťazec, pričom pre každú otváraciu zátvorku si na zásobník pridá uzatváraciu zátvorku rovnakého typu. Ak narazí na uzatváraciu zátvorku, výraz môže byť dobre uzátvorkovaný len v prípade, že je na zásobníku aspoň jedna zátvorka, pričom zátvorka na vrchu zásobníka sa zhoduje so zátvorkou na vstupe. V prípade úspešného prechodu cez celý vstup je reťazec dobre uzátvorkovaný práve vtedy, keď na zásobníku nezostala žiadna zátvorka.
+
S použitím niektorej implementácie zásobníka z [[Prednáška 17|minulej prednášky]] môžeme napísať aj nerekurzívnu verziu funkcie <tt>vyfarbi</tt>. Tá zakaždým vyberie zo zásobníka niektoré políčko, skontroluje všetkých jeho susedov a ak majú pôvodnú farbu, ofarbí ich a vloží ich na zásobník, aby sme ďalej skontrolovali aj ich susedov.  
  
 +
Súradnice jednotlivých susedov budeme počítať s použitím cyklu <tt>for</tt> a polí <tt>deltaStlpec</tt> a <tt>deltaRiadok</tt>, ktoré pre <tt>i = 0,1,2,3</tt> obsahujú ''posuny'' jednotlivých súradníc <tt>i</tt>-teho suseda oproti práve spracúvanému políčku (dalo by sa použiť aj v rekurzívnej verzii).
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
#include <iostream>
+
struct policko {
#include <cassert>
+
    int riadok, stlpec;
using namespace std;
+
};
 +
 
 +
typedef policko dataType;
 +
 
 +
 
 +
/* Sem pride definicia struktury pre zasobnik
 +
* a funkcii poskytovanych zasobnikom. */
  
typedef char dataType;
 
  
/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */
+
const int deltaRiadok[4] = {0, 0, 1, -1};
 +
const int deltaStlpec[4] = {1, -1, 0, 0};
  
int main(void) {
+
/* Prefarbi suvislu jednofarebnu oblast obsahujucu
     char vyraz[100];
+
* poziciu (riadok,stlpec) na farbu s cislom farba. */ 
     cin.getline(vyraz, 100);
+
void vyfarbi(int **a, int m, int n,
      
+
            int riadok, int stlpec, int farba,
 +
            SVGdraw &drawing) {
 +
     int staraFarba = a[riadok][stlpec];
 +
     if (staraFarba == farba) {
 +
        return;
 +
     }
 +
    a[riadok][stlpec] = farba;
 +
    vykresliStvorcek(riadok, stlpec, farby[farba],
 +
    "lightgrey", drawing);
 
     stack s;
 
     stack s;
 
     init(s);
 
     init(s);
 
      
 
      
     bool dobre = true;
+
     policko p;
 +
    p.riadok = riadok;
 +
    p.stlpec = stlpec;
 +
    push(s, p);
 
      
 
      
     for (int i = 0; vyraz[i] != 0; i++) {
+
     while (!isEmpty(s)) {
        switch (vyraz[i]) {
+
        p = pop(s);
             case '(':
+
        for (int i = 0; i <= 3; i++) {
                 push(s, ')');
+
            policko sused;
                 break;
+
            sused.riadok = p.riadok + deltaRiadok[i];
            case '[':
+
            sused.stlpec = p.stlpec + deltaStlpec[i];
                push(s, ']');
+
             if (sused.riadok >= 0 && sused.riadok < m
                 break;
+
                 && sused.stlpec >= 0 && sused.stlpec < n
            case '{':
+
                 && a[sused.riadok][sused.stlpec] == staraFarba) {
                 push(s, '}');
+
                 a[sused.riadok][sused.stlpec] = farba;
                 break;
+
                 vykresliStvorcek(p.riadok, p.stlpec, farby[farba],
            case ')':
+
                                "lightgrey", drawing);
            case ']':
+
                 drawing.wait(pauza);
            case '}':
+
                 push(s, sused);  
                if (isEmpty(s)) {
+
            }      
                    dobre = false;
 
                 } else {
 
                    char c = pop(s);
 
                    if (c != vyraz[i]) {
 
                        dobre = false;
 
                    }
 
                }
 
                break;
 
 
         }
 
         }
 
     }
 
     }
 +
    destroy(s);
 +
}
 +
</syntaxhighlight>
 +
 +
* [[:Media:Animacia3.svg|Výsledná animácia]]
 +
* Poradie sa trochu zmenilo oproti rekurzívnej verzii. Prečo?
 +
 +
===Vyfarbovanie s použitím radu===
 +
 +
Namiesto zásobníka môžeme použiť aj rad. Obrazec sa potom bude vyfarbovať v poradí podľa vzdialenosti od počiatočného políčka. Tento algoritmus sa volá ''prehľadávanie do šírky'', kým rekurzívna verzia zodpovedá ''prehľadávaniu do hĺbky''.
 +
 +
<syntaxhighlight lang="C++">
 +
struct policko {
 +
    int riadok, stlpec;
 +
};
 +
 +
typedef policko dataType;
 +
 +
 +
/* Sem pride definicia struktury pre rad
 +
* a funkcii poskytovanych radom. */
 +
 +
 +
const int deltaRiadok[4] = {0, 0, 1, -1};
 +
const int deltaStlpec[4] = {1, -1, 0, 0};
 +
 +
/* Prefarbi suvislu jednofarebnu oblast obsahujucu
 +
* poziciu (riadok,stlpec) na farbu s cislom farba. */ 
 +
void vyfarbi(int **a, int m, int n,
 +
            int riadok, int stlpec, int farba,
 +
            SVGdraw &drawing) {
 +
    int staraFarba = a[riadok][stlpec];
 +
    if (staraFarba == farba) {
 +
        return;
 +
    }
 +
    a[riadok][stlpec] = farba;
 +
    vykresliStvorcek(riadok, stlpec, farby[farba],
 +
                    "lightgrey", drawing);
 
      
 
      
     dobre = dobre && isEmpty(s);
+
     queue q;
       
+
    init(q);
     destroy(s);
+
   
 +
    policko p;
 +
    p.riadok = riadok;
 +
    p.stlpec = stlpec;
 +
     enqueue(q, p);
 
      
 
      
     if (dobre) {
+
     while (!isEmpty(q)) {
         cout << "Retazec je dobre uzatvorkovany." << endl;
+
         p = dequeue(q);
    } else {
+
        for (int i = 0; i <= 3; i++) {
        cout << "Retazec nie je dobre uzatvorkovany." << endl;
+
            policko sused;
 +
            sused.riadok = p.riadok + deltaRiadok[i];
 +
            sused.stlpec = p.stlpec + deltaStlpec[i];
 +
            if (sused.riadok >= 0 && sused.riadok < m
 +
                && sused.stlpec >= 0 && sused.stlpec < n
 +
                && a[sused.riadok][sused.stlpec] == staraFarba) {
 +
                a[sused.riadok][sused.stlpec] = farba;
 +
                vykresliStvorcek(sused.riadok, sused.stlpec, farby[farba],
 +
                                "lightgrey", drawing);
 +
                drawing.wait(pauza);
 +
                enqueue(q, sused);   
 +
            }       
 +
        }
 +
    }
 +
    destroy(q);
 +
}
 +
</syntaxhighlight>
 +
 
 +
* [[:Media:Animacia4.svg|Výsledná animácia]]
 +
 
 +
Program potom môžeme upraviť aj tak, aby do každého ofarbeného políčka vypísal jeho vzdialenosť od počiatočného políčka:
 +
<syntaxhighlight lang="C++">
 +
struct policko {
 +
    int riadok, stlpec, vzd;
 +
};
 +
 
 +
typedef policko dataType;
 +
 
 +
 
 +
/* Sem pride definicia struktury pre rad
 +
* a funkcii poskytovanych radom. */
 +
 
 +
void vypisVzdialenost(policko p, SVGdraw &drawing) {
 +
    drawing.setLineColor("white");
 +
    drawing.setFontSize(20);
 +
    char text[15];
 +
    sprintf(text, "%d", p.vzd);
 +
    drawing.drawText((p.stlpec + 0.5) * stvorcek,
 +
    (p.riadok + 0.5) * stvorcek, text);
 +
}
 +
 
 +
const int deltaRiadok[4] = {0, 0, 1, -1};
 +
const int deltaStlpec[4] = {1, -1, 0, 0};
 +
 
 +
/* Prefarbi suvislu jednofarebnu oblast obsahujucu
 +
* poziciu (riadok,stlpec) na farbu s cislom farba. */ 
 +
void vyfarbi(int **a, int m, int n,
 +
            int riadok, int stlpec, int farba,
 +
            SVGdraw &drawing) {
 +
    int staraFarba = a[riadok][stlpec];
 +
    if (staraFarba == farba) {
 +
        return;
 
     }
 
     }
 
      
 
      
     return 0;
+
     queue q;
}
+
    init(q);
 +
   
 +
    policko p;
 +
    p.riadok = riadok;
 +
    p.stlpec = stlpec;
 +
    p.vzd = 0;
 +
    enqueue(q, p);
 +
    a[riadok][stlpec] = farba;
 +
    vykresliStvorcek(riadok, stlpec, farby[farba],
 +
                    "lightgrey", drawing);
 +
    vypisVzdialenost(p, drawing);
 +
   
 +
    while (!isEmpty(q)) {
 +
        p = dequeue(q);
 +
        for (int i = 0; i <= 3; i++) {
 +
            policko sused;
 +
            sused.riadok = p.riadok + deltaRiadok[i];
 +
            sused.stlpec = p.stlpec + deltaStlpec[i];
 +
            if (sused.riadok >= 0 && sused.riadok < m
 +
                && sused.stlpec >= 0 && sused.stlpec < n
 +
                && a[sused.riadok][sused.stlpec] == staraFarba) {
 +
        sused.vzd = p.vzd + 1;
 +
                a[sused.riadok][sused.stlpec] = farba;
 +
                vykresliStvorcek(sused.riadok, sused.stlpec, farby[farba],
 +
                                "lightgrey", drawing);
 +
                vypisVzdialenost(sused, drawing);
 +
                drawing.wait(pauza);
 +
                enqueue(q, sused);   
 +
            }       
 +
        }
 +
    }
 +
    destroy(q);
 +
}  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
''Cvičenie:'' Prepíšte program na kontrolu zátvoriek do rekurzívnej podoby. Použite pritom iba premenné typu <tt>char</tt>; špeciálne nepoužívajte žiadne polia. Reťazec načítavajte pomocou funkcií <tt>getc</tt> a <tt>ungetc</tt>. Môžete predpokladať, že je ukončený koncom riadku.
+
* [[:Media:Animacia5.svg|Výsledná animácia]]
 +
 
 +
==Zhrnutie==
 +
* Vyfarbovanie súvislých oblastí v matici sa dá využiť v počítačovej grafike ale na iné úlohy, napríklad počítanie ostrovov na mape.
 +
* Dá sa jednoducho napísať rekurzívne.
 +
* Rekurziu vieme odstrániť použitím zásobníka alebo radu na ukladanie políčok, ktoré ešte treba spracovať.
 +
* Pri využití radu vieme spočítať aj vzdialenosť od počiatočného bodu
 +
* Budúci semester uvidíte na Programovaní (2) prehľadávanie grafov, ktoré funguje veľmi podobne, ale je trochu všeobecnejšie.

Aktuálna revízia z 09:13, 27. november 2024

Oznamy

Budúci utorok bude na cvičeniach rozcvička na papieri.

  • Bude pokrývať učivo po prednášku 17.
  • Dôležitý tréning na semestrálny test.

Blíži sa semestrálny test.

Od pondelka 2.12. 19:00 sa môžete hlásiť na termín skúšky pri počítači.

  • Ak vidíte konflikt niektorého termínu s hromadnou skúškou alebo písomkou z iného predmetu, dajte nám vedieť čím skôr.
  • Kapacita termínov bude obmedzená, prihláste sa teda radšej skôr, neskôr to môžete zmeniť.
  • Prihlásiť a odhlásiť sa dá najviac deň vopred.
  • Skúšku 20.12. môžete robiť iba ak spravíte test 11.12. aspoň na 50% bodov.
  • Decembrový termín odporúčame hlavne študentom, ktorým programovanie nerobí problémy.
  • Viac informácií o skúške na prednáške v stredu 11.12.

Opakovanie: zásobník a rad

  • Zásobník (angl. stack) a rad alebo front (angl. queue) sú abstraktné dátové typy, ktoré udržiavajú postupnosť nejakých prvkov.
  • Obidva typy podporujú vloženie prvku a výber prvky
  • Zo zásobníka sa vyberá prvok, ktorý v ňom pobudol najkratšie, z radu prvok, ktorý v ňom bol najdlhšie
  • Zásobník tak pripomína stĺpec čistých tanierov v reštaurácii, rad pripomína rad pri pokladni

Konkrétne funkcie hlavičky funkcií

void init(stack &s);
bool isEmpty(stack &s);
void push(stack &s, dataType item); // vlozenie prvku
dataType pop(stack &s);    // vyber prvku
dataType peek(stack &s);
void destroy(stack &s);

void init(queue &q);
bool isEmpty(queue &q);
void enqueue(queue &q, dataType item); // vlozenie prvku
dataType dequeue(queue &q);  // vyber prvku
dataType peek(queue &q);
void destroy(queue &q);
  • Pre obidva typy sme videli implementáciu v poli aj v spájanom zozname
  • Hlavné funkcie vkladania a vyberania sú v obidvoch implementáciách rýchle a jednoduché

Videli sme tiež, že obidve štruktúry sa dajú použiť na ukladanie dát alebo úloh, ktoré ešte treba vyriešiť

  • Dnes uvidíme ďalšie príklady


Použitie zásobníka a radu: nerekurzívny Quick Sort

Pripomeňme si triedenie Quick Sort z 11. prednášky:

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() {
  // ...
  quicksort(a, 0, N-1);
  // ...
}

Namiesto rekurzie môžeme použiť aj zásobník úsekov, ktoré ešte treba dotriediť.

struct usek {
    int left;
    int right;
};

typedef usek dataType;

/* Sem pride definicia struktury stack a vsetkych potrebnych funkcii. */

/* Sem pridu funkcie swap a partition rovnake ako vyssie. */

void quicksort(int a[], int n) {
    stack s;
    init(s);
    
    usek u;
    u.left = 0;
    u.right = n-1;
    push(s,u);
    
    while (!isEmpty(s)) {
        u = pop(s);
        // vynechame useky dlzky 0 a 1
        if (u.left >= u.right) {
            continue;
        }
        
        int middle = partition(a, u.left, u.right);
        
        usek u1;
        u1.left = u.left;
        u1.right = middle - 1;
        usek u2;
        u2.left = middle + 1;
        u2.right = u.right;
        push(s,u2);
        push(s,u1);
    }
    
    destroy(s);
}

int main() {
  // ...
  quicksort(a, N);
  // ...
}
  • Tento program triedi úseky v rovnakom poradí, ako rekurzívny Quick Sort, lebo po rozdelení poľa na dve časti dá na vrch zásobníka úsek zodpovedajúci jeho ľavej časti. Až keď sa táto ľavá časť a všetky podúlohy, ktoré z nej vzniknú, spracuje, dôjde na spracovanie pravej časti poľa.
  • Pri triedení Quick Sort však na tomto poradí nezáleží, takže by sme mohli jednotlivé úseky vkladať na zásobník aj v opačnom poradí.
  • Alebo by sme namiesto zásobníka mohli použiť rad. Potom by najskôr rozdelil ľavú aj pravú časť na ďalšie podčasti a potom by delil každú z týchto podčastí atď.

Na zamyslenie: ako by mohla vyzerať nerekurzívna verzia triedenia Merge Sort? Prečo sa nedá použiť rovnaký prístup ako pri triedení Quick Sort?

Vyfarbovanie súvislých oblastí

  • Uvažujme obrázok pozostávajúci z m krát n štvorčekov (pixelov).
  • Farbu každého pixelu zapíšeme do jedného políčka dvojrozmerného poľa s m riadkami a n stĺpcami.
  • V našom jednoduchom príklade budeme pracovať iba s piatimi farbami, ktoré budeme reprezentovať číslami 0,...,4 podľa nasledujúceho poľa (napríklad číslo 0 teda reprezentuje bielu farbu):
const char *farby[5] = {"white", "blue", "black", "yellow", "red"};

Napríklad obrázok

Matica1.png

tak môže byť reprezentovaný nasledujúcim textovým súborom obsahujúcim najprv rozmery matice (čísla m a n) a za nimi samotné prvky matice:

11 17
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0
 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0
 0 0 0 1 0 0 0 0 0 2 2 1 2 2 2 2 0
 0 0 0 1 0 0 0 0 0 2 0 1 0 0 0 2 0
 0 2 2 2 2 2 2 2 0 2 2 1 2 2 2 2 0
 0 2 0 1 0 0 0 2 0 0 0 1 0 0 0 0 0
 0 2 0 1 0 0 0 2 0 0 0 1 0 1 1 1 1
 0 2 0 1 1 1 1 2 1 1 1 1 0 1 0 0 1
 0 2 0 0 0 0 0 2 0 0 0 0 0 1 1 1 1
 0 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0

Zameriame sa teraz na nasledujúci problém: používateľ zvolí (zadá na konzolu) súradnice niektorého štvorčeka a cieľom je ofarbiť nejakou farbou (napríklad červenou) celú súvislú oblasť rovnakej farby obsahujúcu daný štvorček. Napríklad pre obrazec vyššie a vstupné súradnice (2,1) (teda štvorček v treťom riadku a druhom stĺpci, keďže matica sa bude indexovať od nuly) by mal byť výstupom nasledujúci obrazec:

Matica2.png

Podobný problém je napríklad často potrebné riešiť v rôznych nástrojoch na prácu s grafikou a podobne.

  • Jednoduché útvary, napr. obdĺžnik, by sa dali vyfarbiť jednoduchými cyklami, naša súvislá oblasť ale môže mať veľmi zložitý tvar, s mnohými zákrutami, vetveniami a dierami a podobne.
  • Pre každé políčko, ktoré prefarbíme, nesmieme zabudnúť skontrolovať všetkých jeho susedov a ak majú rovnakú farbu ako malo pôvodné políčko, tak prefarbiť aj ich, ich susedov, susedov ich susedov atď.
  • Toto vieme ľahko zapísať rekurzívne: zafarbíme jedno políčko a potom rekurzívne zafarbujeme celú dosiahnuteľnú oblasť pre každého suseda rovnakej farby, akú malo pôvodne prefarbené políčko.
  • Táto podúloha je menšia, lebo aspoň jedno políčko z nej ubudlo prvým prefarbením.

Vyfarbovanie pomocou rekurzívnej funkcie

Nasledujúca rekurzívna funkcia vyfarbi prefarbí políčko so súradnicami (riadok, stlpec) na cieľovú farbu farba a následne sa rekurzívne zavolá pre všetkých susedov tohto políčka, ktoré sú zafarbené pôvodnou farbou prefarbovanej oblasti.

/* Prefarbi súvislú jednofarebnú oblasť obsahujúcu 
 * pozíciu (riadok,stlpec) na farbu s číslom farba. */
void vyfarbi(int **a, int m, int n, 
             int riadok, int stlpec, int farba) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    a[riadok][stlpec] = farba;
    if (riadok - 1 >= 0 
        && a[riadok - 1][stlpec] == staraFarba) {
        vyfarbi(a, m, n, riadok - 1, stlpec, farba);
    }
    if (riadok + 1 < m 
        && a[riadok + 1][stlpec] == staraFarba) {
        vyfarbi(a, m, n, riadok + 1, stlpec, farba);
    }
    if (stlpec - 1 >= 0 
        && a[riadok][stlpec - 1] == staraFarba) {
        vyfarbi(a, m, n, riadok, stlpec - 1, farba);
    }
    if (stlpec + 1 < n 
        && a[riadok][stlpec + 1] == staraFarba) {
        vyfarbi(a, m, n, riadok, stlpec + 1, farba);
    }
}

Proces vyfarbovania môžeme animovať pomocou našej knižnice SVGdraw

V animácii okrem zmeny farby štvorčeka meníme aj farbu jeho rámčeka: pri zavolaní rekurzie sa zmení na hnedú a po skončení spracovania štvorčeka aj jeho susedov sa zmení na sivú. Hnedé sú teda vždy rámčeky štvorčekov uložené na zásobníku volaní.

Program s animáciou

  • Program na vyfarbovanie súvislých oblastí obsahuje funkcie na inicializáciu matice, jej načítanie zo súboru, vykresľovanie jednotlivých štvorčekov a celej matice, ako aj uvoľnenie pamäte. Všetky tieto funkcie pracujú podobne ako pri príklade s výškovou mapou z prednášky 13.
  • Funkcia main načíta maticu zo súboru vstup.txt a animáciu uloží do súboru matica.svg.
  • Do rekurzívnej funkcie pridáme volania funkcie vykresliStvorcek, ktoré aktuálny štvorček v obrázku prekreslia novou farbou a menia aj farbu rámčeka. Za ňou vždy zavoláme funkciu drawing.wait, ktorá animáciu pozdrží, aby sme zmeny stihli na obrázku sledovať.
#include "SVGdraw.h"
#include <cstdio>
#include <cassert>

const char *farby[5] = {"white", "blue", "black", "yellow", "red"}; 

const int stvorcek = 40;    // velkost stvorceka v pixeloch
const int hrubkaCiary = 2;  // hrubka ciary v pixeloch
const double pauza = 0.3;   // pauza po kazdom kroku vyfarbovania v sekundach

/* Vytvori maticu s n riadkami a m stlpcami. */
int **vytvorMaticu(int m, int n) {
    int **a;
    a = new int *[m];
    for (int i = 0; i < m; i++) {
        a[i] = new int[n];
    }
    return a;
}

/* Uvolni pamat matice a s n riadkami a m stlpcami. */
void zmazMaticu(int **a, int m) {
    for (int i = 0; i < m; i++) {
        delete[] a[i];
    }
    delete[] a;
}

/* Vykresli stvorcek v riadku i a stlpci j 
   s farbou vyplne farba a farbou ciary farbaCiary. */
void vykresliStvorcek(int i, int j, 
                      const char * farba, 
                      const char * farbaCiary, 
                      SVGdraw &drawing) {
    drawing.setLineColor(farbaCiary);
    drawing.setLineWidth(hrubkaCiary);
    drawing.setFillColor(farba);
    drawing.drawRectangle(j * stvorcek, i * stvorcek, 
                          stvorcek, stvorcek);
}

/* Vykresli maticu a s n riadkami a m stlpcami. */
void vykresliMaticu(int **a, int m, int n, SVGdraw &drawing) {
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            vykresliStvorcek(i, j, farby[a[i][j]], "lightgray", drawing);
        }
    }
}

/* Nacita z textoveho suboru, na ktory ukazuje fr, 
   prvky matice a s n riadkami a m stlpcami. */
void nacitajMaticu(FILE *fr, int **a, int m, int n) {
    assert(fr != NULL);
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            fscanf(fr, "%d", &a[i][j]);
        }
    }
}

/* Prefarbi suvislu jednofarebnu oblast obsahujucu 
 * poziciu (riadok,stlpec) na farbu s cislom farba. */
void vyfarbi(int **a, int m, int n, 
             int riadok, int stlpec, int farba,
             SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    a[riadok][stlpec] = farba;
    vykresliStvorcek(riadok, stlpec, farby[farba], 
                     "brown", drawing);
    drawing.wait(pauza);
    if (riadok - 1 >= 0 && a[riadok - 1][stlpec] == staraFarba) {
        vyfarbi(a, m, n, riadok - 1, stlpec, farba, drawing);
    }
    if (riadok + 1 < m && a[riadok + 1][stlpec] == staraFarba) {
        vyfarbi(a, m, n, riadok + 1, stlpec, farba, drawing);
    }
    if (stlpec - 1 >= 0 && a[riadok][stlpec - 1] == staraFarba) {
        vyfarbi(a, m, n, riadok, stlpec - 1, farba, drawing);
    }
    if (stlpec + 1 < n && a[riadok][stlpec + 1] == staraFarba) {
        vyfarbi(a, m, n, riadok, stlpec + 1, farba, drawing);
    }
    vykresliStvorcek(riadok, stlpec, farby[farba], 
                     "lightgray", drawing);
    drawing.wait(pauza);
} 

int main() {
    FILE *fr = fopen("vstup.txt", "r");
    assert(fr != NULL);

    // nacitanie rozmerov matice
    int m, n;
    fscanf(fr, "%d %d", &m, &n);  
    // vytvorenie matice a nacitanie jej prvkov  
    int **a = vytvorMaticu(m, n);
    nacitajMaticu(fr, a, m, n);         
    fclose(fr);
        
    SVGdraw drawing(n * stvorcek, m * stvorcek, "matica.svg");
    vykresliMaticu(a, m, n, drawing);

    // nacitanie suradnic pociatocneho stvorceka 
    int riadok, stlpec;
    scanf("%d", &riadok);
    scanf("%d", &stlpec);

    vyfarbi(a, m, n, riadok, stlpec, 4, drawing);

    drawing.finish();
    zmazMaticu(a, m);
}

Počítanie ostrovov

Obrázok, s ktorým sme pracovali vyššie, môže reprezentovať napríklad jednoduchú mapu súostrovia, kde more je znázornené modrou farbou a pevnina je znázornená žltou farbou. Úlohou môže byť zistiť počet ostrovov. Ten môžeme zistiť napríklad takto:

  • Prechádzame postupne všetky políčka mapy.
  • Ak narazíme na pevninu (t. j. žlté políčko), zvýšime doposiaľ nájdený počet ostrovov o 1 a ofarbíme celý ostrov (napríklad) na červeno.
  • Ak narazíme na ďalšie žlté políčko, opäť urobíme to isté.
  • Toto robíme, až kým prejdeme cez všetky políčka mapy.

Príklad mapy a jej zobrazenie pred začiatkom hľadania ostrovov, po nájdení prvých troch ostrovov a po nájdení všetkých ostrovov:

11 17
 1 1 1 1 1 1 3 1 1 1 1 1 1 1 1 1 1
 1 1 1 3 3 3 3 1 1 3 1 1 1 1 3 3 1
 1 1 1 3 3 1 1 1 1 1 1 1 1 1 3 3 1
 1 1 3 3 1 1 1 1 1 3 3 1 3 3 3 3 1
 1 1 1 3 3 1 1 1 1 3 1 1 3 3 3 3 1
 1 3 3 3 3 1 3 3 1 3 3 1 3 3 3 3 1
 1 1 3 3 1 1 1 3 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 3 3 3 1 1 3 3 3 1 1
 1 1 1 1 1 1 1 3 3 3 1 1 3 1 3 1 1
 1 1 3 3 3 3 1 3 1 1 1 1 3 3 3 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Do programu vyššie teda dorobíme funkciu

int najdiOstrovy(int **a, int m, int n, SVGdraw &drawing) {
    int ostrovov = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (a[i][j] == 3) {
                ostrovov++;
                vyfarbi(a, m, n, i, j, 4, drawing);
            }
        }
    }
    return ostrovov;
}

a funkciu main môžeme zmeniť napríklad takto:

int main() {
    FILE *fr = fopen("ostrovy.txt", "r");
    assert(fr != NULL);
    int m, n;
    fscanf(fr, "%d %d", &m, &n);
    int **a = vytvorMaticu(m, n);
    nacitajMaticu(fr, a, m, n);
    fclose(fr);
        
    SVGdraw drawing(n * stvorcek, m * stvorcek, "mapa.svg");
    vykresliMaticu(a, m, n, drawing);

    int pocetOstrovov = najdiOstrovy(a, m, n, drawing);
    printf("Pocet ostrovov je %d.\n", pocetOstrovov);

    drawing.finish();
    zmazMaticu(a, m);
}

Cvičenie: upravte program tak, aby ešte navyše zistil, či má niektorý z ostrovov jazero.

Nerekurzívne vyfarbovanie

Vyfarbovanie s použitím zásobníka

S použitím niektorej implementácie zásobníka z minulej prednášky môžeme napísať aj nerekurzívnu verziu funkcie vyfarbi. Tá zakaždým vyberie zo zásobníka niektoré políčko, skontroluje všetkých jeho susedov a ak majú pôvodnú farbu, ofarbí ich a vloží ich na zásobník, aby sme ďalej skontrolovali aj ich susedov.

Súradnice jednotlivých susedov budeme počítať s použitím cyklu for a polí deltaStlpec a deltaRiadok, ktoré pre i = 0,1,2,3 obsahujú posuny jednotlivých súradníc i-teho suseda oproti práve spracúvanému políčku (dalo by sa použiť aj v rekurzívnej verzii).

struct policko {
    int riadok, stlpec;
};

typedef policko dataType;


/* Sem pride definicia struktury pre zasobnik 
 * a funkcii poskytovanych zasobnikom. */


const int deltaRiadok[4] = {0, 0, 1, -1};
const int deltaStlpec[4] = {1, -1, 0, 0};

/* Prefarbi suvislu jednofarebnu oblast obsahujucu 
 * poziciu (riadok,stlpec) na farbu s cislom farba. */  
void vyfarbi(int **a, int m, int n, 
             int riadok, int stlpec, int farba,
             SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    a[riadok][stlpec] = farba;
    vykresliStvorcek(riadok, stlpec, farby[farba], 
		     "lightgrey", drawing);
    stack s;
    init(s);
    
    policko p;
    p.riadok = riadok;
    p.stlpec = stlpec;
    push(s, p);
    
    while (!isEmpty(s)) {
        p = pop(s);
        for (int i = 0; i <= 3; i++) {
            policko sused;
            sused.riadok = p.riadok + deltaRiadok[i];
            sused.stlpec = p.stlpec + deltaStlpec[i];
            if (sused.riadok >= 0 && sused.riadok < m
                && sused.stlpec >= 0 && sused.stlpec < n
                && a[sused.riadok][sused.stlpec] == staraFarba) {
                a[sused.riadok][sused.stlpec] = farba;
                vykresliStvorcek(p.riadok, p.stlpec, farby[farba], 
                                "lightgrey", drawing);
                drawing.wait(pauza);
                push(s, sused);    
            }        
        }
    }
    destroy(s);
}

Vyfarbovanie s použitím radu

Namiesto zásobníka môžeme použiť aj rad. Obrazec sa potom bude vyfarbovať v poradí podľa vzdialenosti od počiatočného políčka. Tento algoritmus sa volá prehľadávanie do šírky, kým rekurzívna verzia zodpovedá prehľadávaniu do hĺbky.

struct policko {
    int riadok, stlpec;
};

typedef policko dataType;


/* Sem pride definicia struktury pre rad 
 * a funkcii poskytovanych radom. */


const int deltaRiadok[4] = {0, 0, 1, -1};
const int deltaStlpec[4] = {1, -1, 0, 0};

/* Prefarbi suvislu jednofarebnu oblast obsahujucu
 * poziciu (riadok,stlpec) na farbu s cislom farba. */  
void vyfarbi(int **a, int m, int n, 
             int riadok, int stlpec, int farba, 
             SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    a[riadok][stlpec] = farba;
    vykresliStvorcek(riadok, stlpec, farby[farba],
                     "lightgrey", drawing);
    
    queue q;
    init(q);
    
    policko p;
    p.riadok = riadok;
    p.stlpec = stlpec;
    enqueue(q, p);
    
    while (!isEmpty(q)) {
        p = dequeue(q);
        for (int i = 0; i <= 3; i++) {
            policko sused;
            sused.riadok = p.riadok + deltaRiadok[i];
            sused.stlpec = p.stlpec + deltaStlpec[i];
            if (sused.riadok >= 0 && sused.riadok < m
                && sused.stlpec >= 0 && sused.stlpec < n
                && a[sused.riadok][sused.stlpec] == staraFarba) {
                a[sused.riadok][sused.stlpec] = farba;
                vykresliStvorcek(sused.riadok, sused.stlpec, farby[farba],
                                 "lightgrey", drawing);
                drawing.wait(pauza);
                enqueue(q, sused);    
            }        
        }
    }
    destroy(q);
}

Program potom môžeme upraviť aj tak, aby do každého ofarbeného políčka vypísal jeho vzdialenosť od počiatočného políčka:

struct policko {
    int riadok, stlpec, vzd;
};

typedef policko dataType;


/* Sem pride definicia struktury pre rad 
 * a funkcii poskytovanych radom. */

void vypisVzdialenost(policko p, SVGdraw &drawing) {
    drawing.setLineColor("white");
    drawing.setFontSize(20);
    char text[15];
    sprintf(text, "%d", p.vzd);
    drawing.drawText((p.stlpec + 0.5) * stvorcek,
		     (p.riadok + 0.5) * stvorcek, text);
}

const int deltaRiadok[4] = {0, 0, 1, -1};
const int deltaStlpec[4] = {1, -1, 0, 0};

/* Prefarbi suvislu jednofarebnu oblast obsahujucu 
 * poziciu (riadok,stlpec) na farbu s cislom farba. */  
void vyfarbi(int **a, int m, int n, 
             int riadok, int stlpec, int farba, 
             SVGdraw &drawing) {
    int staraFarba = a[riadok][stlpec];
    if (staraFarba == farba) {
        return;
    }
    
    queue q;
    init(q);
    
    policko p;
    p.riadok = riadok;
    p.stlpec = stlpec;
    p.vzd = 0;
    enqueue(q, p);
    a[riadok][stlpec] = farba;
    vykresliStvorcek(riadok, stlpec, farby[farba],
                     "lightgrey", drawing);
    vypisVzdialenost(p, drawing);
    
    while (!isEmpty(q)) {
        p = dequeue(q);
        for (int i = 0; i <= 3; i++) {
            policko sused;
            sused.riadok = p.riadok + deltaRiadok[i];
            sused.stlpec = p.stlpec + deltaStlpec[i];
            if (sused.riadok >= 0 && sused.riadok < m
                && sused.stlpec >= 0 && sused.stlpec < n
                && a[sused.riadok][sused.stlpec] == staraFarba) {
	        sused.vzd = p.vzd + 1;
                a[sused.riadok][sused.stlpec] = farba;
                vykresliStvorcek(sused.riadok, sused.stlpec, farby[farba],
                                 "lightgrey", drawing);
                vypisVzdialenost(sused, drawing);
                drawing.wait(pauza);
                enqueue(q, sused);    
            }        
        }
    }
    destroy(q);
}

Zhrnutie

  • Vyfarbovanie súvislých oblastí v matici sa dá využiť v počítačovej grafike ale na iné úlohy, napríklad počítanie ostrovov na mape.
  • Dá sa jednoducho napísať rekurzívne.
  • Rekurziu vieme odstrániť použitím zásobníka alebo radu na ukladanie políčok, ktoré ešte treba spracovať.
  • Pri využití radu vieme spočítať aj vzdialenosť od počiatočného bodu
  • Budúci semester uvidíte na Programovaní (2) prehľadávanie grafov, ktoré funguje veľmi podobne, ale je trochu všeobecnejšie.