Programovanie (2) v Jave
1-INF-166, letný semester 2023/24
2012/13 Programovanie (2) v Jave: Rozdiel medzi revíziami
(2 medziľahlé úpravy od rovnakého používateľa nie sú zobrazené.) | |||
Riadok 3 633: | Riadok 3 633: | ||
// vysledok je v premennej a | // vysledok je v premennej a | ||
</pre> | </pre> | ||
− | |||
=Prednáška 31= | =Prednáška 31= | ||
== Opakovanie OOP == | == Opakovanie OOP == | ||
Riadok 3 971: | Riadok 3 970: | ||
** Vytvorte aplikáciu, ktorá vytvorí v okne veľkosti 500x500 gombík veľkosti 20x20 v strede. Pomocou šípok meňte polohu gombíka. | ** Vytvorte aplikáciu, ktorá vytvorí v okne veľkosti 500x500 gombík veľkosti 20x20 v strede. Pomocou šípok meňte polohu gombíka. | ||
** Vytvorte text, ktorý sa bude sám hýbať po obrazovke v istých časových intervaloch. Premyslite si čím text zastavíte resp. ako aplikáciu ukončiť. | ** Vytvorte text, ktorý sa bude sám hýbať po obrazovke v istých časových intervaloch. Premyslite si čím text zastavíte resp. ako aplikáciu ukončiť. | ||
− | |||
=Prednáška 32= | =Prednáška 32= | ||
== Pozície a layouty == | == Pozície a layouty == | ||
Riadok 4 506: | Riadok 4 504: | ||
* Na vami vytvorenom grafickom paneli vytvorte MouseListener, ktorý celý čas pohybu myšou po paneli vypisuje (do textového popisku - JLabel) súradnice myši. | * Na vami vytvorenom grafickom paneli vytvorte MouseListener, ktorý celý čas pohybu myšou po paneli vypisuje (do textového popisku - JLabel) súradnice myši. | ||
− | |||
=Prednáška 33= | =Prednáška 33= | ||
== Kreslenie so SWINGom == | == Kreslenie so SWINGom == |
Aktuálna revízia z 12:44, 30. marec 2014
Táto stránka obsahuje archívnu kópiu materiálov zo školského roku 2012/13. Niektoré odkazy nemusia byť funkčné. V ďalších školských rokoch sa obsah predmetu môže mierne alebo výraznejšie meniť.
- #Letný semester, úvodné informácie
- #Letný semester, pravidlá
- #Letný semester, test a skúška
- #GraphGUI (knižnica ku skúške)
- #Letný semester, projekt
Týždeň 11.-17.2. |
Úvod do Javy |
#Prednáška 26 · #Cvičenia 14 · #DÚ11 |
Týždeň 18.-24.2. |
Úvod do objektovo-orientovaného programovania, JavaDoc |
#Prednáška 27 · #Cvičenia 15 · #DÚ12 |
Týždeň 25.2.-3.3. |
Dedenie, polymorfizmus, modifikátory, interface |
#Prednáška 28 · #Cvičenia 16 |
Týždeň 4.-10.3. |
Výnimky, generické programovanie |
#Prednáška 29 · #Cvičenia 17 · #DÚ13 |
Týždeň 11.-17.3. |
Collections, testovanie |
#Prednáška 30 · #Cvičenia 18 |
Týždeň 18.-24.3. |
Opakovanie OOP, úvod k Swingu |
#Prednáška 31 · #Cvičenia 19 |
Týždeň 25.-31.3. |
Swing (layout, menu, dialog, panel) |
#Prednáška 32 · #Cvičenia 20 · #DÚ14 |
Týždeň 1.-7.4. |
#Cvičenia 21 |
Týždeň 8.-14.4. |
Swing, JavaFX |
#Prednáška 33 · #Cvičenia 22 · #DÚ15 |
Týždeň 15.-21.4. |
Reprezentácia grafov, prehľadávanie do hĺbky |
#Prednáška 34 · #Cvičenia 23 · #DÚ16 |
Týždeň 22.-28.4. |
#Cvičenia 24 |
Týždeň 29.4.-5.5. |
Prehľadávanie grafu do šírky, prehľadávanie s návratom (najdlhšia cesta) |
#Prednáška 35 |
Týždeň 6.-12.5. |
Najväčšia klika, orietované grafy, topologické triedenie, informácie ku skúške |
#Prednáška 36 · #Cvičenia 25 |
Týždeň 13.-19.5. |
OOP v C++ |
#Prednáška 37 · #Cvičenia 26 |
Letný semester, úvodné informácie
Rozvrh
- Prednášky: utorok 9:50-11:20 v F1-109
- Cvičenia:
- 1i1: Streda 9:50 v M-217 (cvičí Martin Králik)
- 1i2: Streda 14:00 v M-217 (cvičí Jaroslav Budiš)
- Streda 15:40 v M-217 nepovinné doplnkové cvičenia
Vyučujúce
- RNDr. Jana Katreniaková PhD., miestnosť M-263,
- Konzultácie po dohode e-mailom
- Mgr. Broňa Brejová, PhD., miestnosť M-163,
- Konzultácie po dohode e-mailom
Cvičiaci
Ciele predmetu
- Predmet 1-INF-166 Programovanie (2) v Jave nadväzuje na predmet 1-INF-127 Programovavanie (1) v C/C++. Oba sú určené študentom prvého ročníka bakalárskeho študijného programu Informatika a tvoria alternatívu k povinným predmetom 1-INF-126 Programovanie (1) a 1-INF-165 Programovanie (2).
- Každý študent sa môže rozhodnúť, či absolvuje 1-INF-127 a 1-INF-166 alebo 1-INF-126 a 1-INF-165. Nie je však možné absolvovať obe verzie programovania ani sa po prvom semestri presunúť z jednej verzie do druhej.
- Predmety 1-INF-126 a 1-INF-165 sa vyučujú v jazyku FreePascal a majú na našej fakulte už dlhú tradíciu. Predmety 1-INF-127 a 1-INF-166 budú vyučované v jazykoch C resp. C++ a Java. Výhodou je, že tieto jazyky využijete aj v ďalších nadväzujúcich predmetoch (Systémové programovanie, Programovanie (3) a podobne) a sú to aj jazyky využívané v praxi. Nevýhodu je, že sa po prvom semestri budete musieť preorientovať na iný jazyk.
- Ciele predmetu 1-INF-166:
- prehĺbiť a rozšíriť zručnosti v algoritmickom uvažovaní, písaní a ladení programov z predchádzajúceho semestra
- oboznámiť sa so základnými programovými a dátovými štruktúrami jazyka Java
- zvládnuť základy objektovo-orientovaného programovania a tvorby programov s grafickým užívateľským rozhraním
- oboznámiť sa s ďalšími základnými algoritmami a dátovými štruktúrami
Literatúra
- Predmet sa nebude striktne riadiť žiadnou učebnicou. Prehľad preberaných tém a stručné poznámky nájdete na stránke predmetu, doporučujeme Vám si na prednáškach a cvičeniach robiť vlastné poznámky.
- Pri štúdiu Vám môžu pomôcť knihy o jazyku Java, o programovaní všeobecne a o algoritmoch preberaných na prednáške. Tu je výber z vhodných titulov, ktoré sú k dispozícii na prezenčné štúdium vo fakultnej knižnici:
- Bruce Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Staršie 2. vydanie v českom preklade v knižnici so signatúrou D-INF-E-1b, 3. vydanie v angličtine online, doporučujeme však radšej 4. vydanie
- Michael T. Goodrich, Roberto Tamassia: Data Structures and Algorithms in Java. John Wiley & Sons, 2006. Signatúra D-INF-G-5
- Robert Sedgewick: Algorithms in Java, Parts 1-4, Fundamentals, Data Structures, Sorting, Searching, Addison-Wesley, 2003. Signatúra D-INF-S-1/I-IVa
- Robert Sedgewick: Algorithms in Java, Part 5, Graph Algorithms Addison-Wesley, 2004. Signatúra D-INF-S-1/V (I)
- Bruce Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Tutoriál k jazyku Java a referenčná príručka k štandardným knižniciam
Priebeh semestra
- Na prednáškach budeme preberať obsah predmetu. Prednášky budú dve vyučovacie hodiny do týždňa.
- Cvičenia budú dve vyučovacie hodiny do týždňa v počítačovej učebni a ich cieľom je aktívne si precvičiť učivo. Na začiatku cvičenia bude krátka diskusia o prípadných nejasnostiach ohľadom materiálu z minulého cvičenia. Potom nasleduje rozcvička (krátky test) písaný na papieri. Ďalšou časťou cvičenia je precvičovanie príkladov k predchádzajúcim prednáškam (spoločne alebo individuálne). Na konci cvičenia spravidla budete môcť začať pracovať na domácej úlohe, pričom cvičiaci Vám v prípade potreby odpovie na Vaše otázky.
- Domáce úlohy navrhujeme tak, aby Vám ich riešenie pomohlo osvojiť si a precvičiť si učivo, čím sa okrem iného pripravujete aj na záverečnú skúšku. Okrem tohto sú za domáce úlohy body do záverečného hodnotenia. Najviac sa naučíte, ak sa Vám domácu úlohu podarí samostatne vyriešiť, ale ak sa vám to napriek vášmu usilu nedarí, neváhajte sa spýtať o pomoc prednášajúcich alebo cvičiacich. Možno s malou radou od nás sa Vám podarí úlohu spraviť. Treba však na domácej úlohe začať pracovať v predstihu, aby ste nás v prípade problémov stihli kontaktovať.
- Cieľom vyučujúcich tohto predmetu je vás čo najviac naučiť, ale musíte aj vy byť aktívni partneri. Ak Vám na prednáške alebo cvičení nie je niečo jasné, spýtajte sa. Môžete nám klásť tiež otázky počas našich konzultačných hodín alebo emailom. Ak sa dostanete do väčších problémov s plnením študijných povinností, poraďte sa s vyučujúcimi alebo s tútorom, ako tieto problémy riešiť.
- 40% známky dostávate za prácu cez semester, preto netreba nechávať štúdium učebnej látky až na skúškové obdobie.
Letný semester, pravidlá
Známkovanie
- 20% známky je na základe rozcvičiek, ktoré sa píšu na (takmer) každom cvičení
- 20% známky je za domáce úlohy
- 30% známky je za záverečný písomný test
- 30% známky je za praktickú skúšku pri počítači
- 10% bonusových percent je za nepovinný projekt
Pozor, body získavané za jednotlivé príklady nezodpovedajú priamo percentám záverečnej známky. Body za každú formu známkovania sa preváhujú tak, aby maximálny získateľný počet zodpovedal váham uvedených vyššie. Úlohy označené ako bonusové sa nerátajú do maximálneho počtu získateľných bodov v danej aktivite.
Stupnica
- Na úspešné absolvovanie predmetu je potrebné splniť nasledovné tri podmienky:
- Získať aspoň 50% bodov v celkovom hodnotení
- Získať aspoň 50% zo záverečného písomného testu
- Získať aspoň 50% zo skúšky
- Ak niektorú z týchto troch podmienok nesplníte, dostávate známku Fx.
- V prípade úspešného absolvovania predmetu získate známku podľa percent v celkovom hodnotení takto:
- A: 90% a viac, B:80...89%, C: 70...79%, D: 60...69%, E: 50...59%
Rozcvičky
- Rozcvičky sú krátke testy (cca 15 minút), ktoré sa píšu na začiatku (takmer) každého cvičenia. Za každú rozcvičku môžete získať najviac 5 bodov.
- Pri rozcvičke môžete použiť ľubovoľné písomné materiály (poznámky, knihy,...), nie však počítače a iné elektronické pomôcky. Počas rozcvičky nie je možné zdieľať materiály so spolužiakmi.
- Ak bude počas semestra celkovo N rozcvičiek, do výslednej známky sa vám zaráta iba N-2 najlepších, t.j. dve rozcvičky, na ktorých ste získali najmenej bodov (alebo ste sa ich ani nezúčastnili) sa vám škrtajú.
Domáce úlohy
- Domáce úlohy budú vypisované v priemere raz za dva týždne. Maximálny počet bodov za domácu úlohu bude uvedený v zadaní a bude sa pohybovať spravidla v rozsahu 10-20 bodov podľa náročnosti úlohy.
- Domáce úlohy treba odovzdať elektronicky pomocou systému Moodle do termínu určeného v zadaní. Neskoršie odovzdané úlohy nebudú akceptované. Po odovzdaní si ešte raz skontrolujte pri príslušnej úlohe, že vaše súbory sa správne nahrali.
- Program, ktorý odovzdáte ako domácu úlohu by mal byť skompilovateľný a spustiteľný v prostredí používanom na cvičeniach. Budeme kontrolovať správnosť celkovej myšlienky, správnosť implementácie, ale aj štýl.
Nepovinný projekt
- Za nepovinný projekt môžete získať 10% bonus k vašej výslednej známke (musíte však stále splniť všetky tri podmienky ukočenia predmetu).
- Projekt sa bude odovzdávať v prvom týždni skúškového obdobia.
- Témy projektov a podrobnejšie pravidlá nájdete na zvláštnej stránke.
- Na získanie bodov za projekt musí byť vaša práca dostatočne rozsiahla a kvalitná, v opačnom prípade získate 0 bodov.
Záverečný písomný test
- Záverečný test bude trvať 90 minút a bude obsahovať úlohy podobné tým, ktoré sa riešili na cvičeniach.
- Riadny termín testu sa bude konať koncom semestra alebo začiatkom skúškového obdobia a cez skúškové obdobie bude aj opravný termín testu.
- Pri teste nemôžete používať žiadne pomocné materiály (písomné ani elektronické) okrem povoleného ťaháku v rozsahu jedného listu formátu A4 s ľubovoľným obsahom na oboch stranách.
Skúška
- Na skúške budete riešiť 2 úlohy pri počítači v celkovom trvaní 2-3 hodiny.
- Na skúške nemôžete používať žiadne pomocné materiály okrem povoleného ťaháku v rozsahu jedného listu formátu A4 s ľubovoľným obsahom na oboch stranách. Nebude k dispozícii ani internet. Budete používať rovnaké programátorské prostredie ako na cvičeniach.
- Po skončení skúšky sa koná krátky ústny pohovor s vyučujúcim, počas ktorého sa prediskutujú programy, ktoré ste odovzdali a uzavrie sa vaša známka.
Opravné termíny
- Máte nárok na dva opravné termíny (ale len v rámci termínov, ktoré sme určili).
- Toto sa týka písomky aj skúšky pri počítači.
- Účasť na opravnom termíne písomky teda tiež vedie k zápisu známky z predmetu v opravnom termíne.
- Druhý opravný termín písomky môžu písať len študenti, ktorí už majú absolvovanú skúšku na aspoň 50% bodov a majú šancu úspešne ukončiť predmet (termín určíme v prípade potreby).
- Ak sa zúčastníte opravného termínu, strácate body z predchádzajúceho termínu, aj keby ste na opravnom získali menej bodov.
- Ak po skúške pri počítači máte nárok na známu E alebo lepšiu, ale chceli by ste si známku ešte opraviť, musíte sa dohodnúť so skúšajúcimi pred zapísaním známky do indexu.
- Ak po skúške pri počítači ešte opravujete písomku, je potrebné prísť uzavrieť a zapísať známku v termíne určenom vyučujúcimi.
- Ak sa zo závažných dôvodov (napr. zdravotných, alebo konflikt s inou skúškou) nemôžete zúčastniť termínu skúšky alebo písomky, dajte o tom vyučujúcim vedieť čím skôr.
Opisovanie
- Máte povolené sa so spolužiakmi a ďalšími osobami rozprávať o domácich úlohách a stratégiách na ich riešenie. Kód, ktorý odovzdáte, musí však byť vaša samostatná práca. Je zakázané opisovať kód z literatúry alebo z internetu a ukazovať svoj kód spolužiakom. Domáce úlohy môžu byť kontrolované softvérom na detekciu plagiarizmu.
- Počas rozcvičiek, testov a skúšok môžete používať iba povolené pomôcky a nesmiete komunikovať s žiadnymi osobami okrem vyučujúcich.
- Ak nájdeme prípady opisovania alebo nepovolených pomôcok, všetci zúčastnení študenti získajú za príslušnú domácu úlohu alebo test nula bodov (t.j. aj tí, ktorí dali spolužiakom odpísať). Opakované alebo obzvlášť závažné prípady opisovania budú podstúpené na riešenie dekanovi fakulty.
Neprítomnosť
- Účasť na cvičeniach veľmi silne doporučujeme a v prípade neprítomnosti stratíte body za rocvičky.
- V prípade kratšieho ochorenia alebo iných problémov môžete využiť možnosť, že dve najhoršie rozcvičky sa škrtajú a stratené body za domáce úlohy je možné dohoniť riešením bonusových príkladov.
- V prípade dlhšieho ochorenia (aspoň dva týždne alebo opakovaná neprítomnosť) alebo iných závažných prekážok sa príďte poradiť s prednášajúcimi o možných riešeniach. Treba tak spraviť čím skôr, nie až spätne cez skúškové. Prineste si potvrdenku od lekára.
Test pre pokročilých
- V druhom týždni semestra sa bude konať nepovinný test pre pokročilých. Príklady na ňom budú podobné ako na záverečnom teste.
- Ak na test prídete a napíšete ho na menej ako 50%, nezískate žiadne výhody (ako keby ste na test ani neprišli).
- V opačnom prípade za každých celých získaných 10% získavate plný počet bodov z jednej rozcvičky. Napr. ak ste získali 59% z testu, dostanete plný počet bodov z prvých 5 rozcvičiek po opravení testu. Tieto body nie je možné presúvať na iné termíny rozcvičiek.
- Navyše si môžete body z testu pre pokročilých nechať uznať ako body zo záverečného testu. Máte však aj možnosť písať záverečný test so spolužiakmi.
Letný semester, test a skúška
Na túto stránku budeme postupne pridávať informácie týkajúce sa záverečného písomného testu a praktickej skúšky pri počítači v letnom semestri. Doporučujeme tiež si preštudovať pravidlá predmetu.
Organizácia skúšky
Písomný test
- Termín: pondelok 20.5.2013 o 10:00 v posluchárni B
- Opravný/náhradný termín: streda 29.5. o 14:00 v M-III
- Trvanie 90 minút
- Príklady zhruba ako na cvičeniach alebo rozcvičkách
- Dobre si rozvrhnite čas, niektoré úlohy sú ťažšie, iné ľahšie.
- Aby ste mali šancu úspešne ukončiť predmet, musíte získať aspoň polovicu bodov.
- Doneste si ISIC, písacie potreby, ťahák 1 list A4
- Zakázané sú ďalšie materiály, elektronické pomôcky, opisovanie
Skúška pri počítači
- Jeden príklad na Swing, jeden na grafy (väčšinou backtracking)
- Obidva v graphgui, pozrite si vopred pred skúškou
- Zapíšte sa na termín v AIS, prihlasovanie/odhlasovanie do 14:00 deň pred skúškou
- Doneste si ťahák 1 list A4, ISIC, index, písacie potreby, papier
- Príďte na 8:50 pred príslušnú učebňu, kde sa dozviete pokyny a rozsadenie do miestností
- Doobeda: 2 a 3/4 hodiny práca pri počítačoch.
- Poobede, príp. ďalší deň: vyhodnotenie na rovnakom prostredí u prednášajúcich
- Prostredie ako minulý semester, bude graphgui a dokumentácia k Jave
- Aby ste mali šancu úspešne ukončiť predmet, musíte získať aspoň polovicu bodov.
- Hodnotíme správnosť myšlienky, správnosť implementácie, štýl
- Nezáleží na rýchlosti programu
- Termíny: 22.5., 5.6., 21.6. (1. opravný, pozor zmena), 27.6. (2. opravný)
Skúškové prostredie
Písomný test
Na písomnom teste budú príklady na nasledujúce témy:
- Základné črty jazyka Java
- Objektovo-orientované programovanie (triedy, konštruktory, dedenie a polymorfizmus, interface,...)
- Výnimky
- Základy generického programovania, základy Collections (ArrayList, LinkedList, HashMap, Iterator)
- Testovacie vstupy (netreba JUnit, len tvorba vstupov ako takých)
- Grafy (reprezentácia, prehľadávanie grafu do hĺbky a šírky, topologické triedenie orientovaného grafu, úlohy na prehľadávanie s návratom)
- Všeobecné programátorské zručnosti, práca s poliami, zoznamami, stromami, rekurzia
Na písomnom teste nebudú príklady na Swing / JavaFX
Ukážkové príklady na písomný test
V texte nižšie je niekoľko príkladov, ktoré sa svojim charakterom a obtiažnosťou podobajú na príklady, aké budú na záverečnej písomke. Tieto ukážkové príklady sú prevažne vybrané z cvičení a prednášok, na skutočnej písomke však budú nové, zatiaľ nepoužité príklady.
- Príklad 1: Navrhnite triedu Polynomial, ktorá bude reprezentovať polynómy jednej premennej s celočíselnými koeficientami, ktorá bude mať nasledujúce metódy (napíšte kód celej triedy, vrátane premenných):
- Konštruktor bez parametrov, ktorý vytvorí nulový polynóm
- Konštruktor s dvoma celočíselnými parametrami a a i, ktorý vytvorí polynóm Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle ax^i}
- Konštruktor, ktorý dostane pole a vytvorí polynóm, ktorého koeficienty budú prvky tohto poľa
- Metódu getCoefficient(int i), ktorá vráti koeficient pri člene Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle x^i} .
- Metódu add(Polynomial p), ktorá vráti nový polynóm, ktorý bude súčtom tohto polynómu a polynómu p.
- Metódu evaluate(int value), ktorá vráti hodnotu polynómu, keď premenná bude mať hodnotu value.
- Príklad 2: Naprogramujte generickú triedu Matrix, ktorá reprezentuje obdĺžnikovú maticu prvkov nejakého neznámeho typu E.
- Napíšte konštruktor, ktorý vytvorí maticu zadaných rozmerov a vyplní ju zadaným prvkom typu E.
- Napíšte metódu get, ktorá vráti prvok matice nachádzajúci sa na zadanom mieste
- Napíšte metódu set, ktorá na zadané miesto v matici zapíše zadaný prvok typu E
- Ak metódam get alebo set užívateľ zadá súradnice mimo matice, hodia výnimku vašej vlastnej triedy MatrixIndexOutOfBoundsException (ktorú tiež naprogramujte)
- Výnimka tejto triedy by v metóde getMessage mala vrátiť reťazec obsahujúci obidve súradnice, ako aj obidva rozmery matice.
- Príklad 3: Napíšte generickú statickú metódu kazdyDruhy, ktorá dostane LinkedList a vyhodí z neho každý druhý prvok. K LinkedListu pristupujte iba cez iterátor a jeho metódu remove.
- Príklad 4: Uvažujme prehľadávanie grafu do hĺbky, ktoré do poľa whenVisited čísluje vrcholy v poradí, v akom boli navštívené. Odsimulujte algoritmus na grafe vpravo a zistite, v akom poradí budú vrcholy navštívené, ak začneme prehľadávať vo vrchole 0. Predpokladajte, že adjVertices vracia susedov v poradí podľa čísla vrcholu.
- Príklad 5: Odsimulujte prácu prehľadávania do šírky na grafe vyššie, ak začneme prehľadávať vo vrchole 0. Predpokladajte, že adjVertices vracia susedov v poradí podľa čísla vrcholu. Uveďte, v akom poradí boli vrcholy vložené do queue a vypíšte výsledné polia dist a prev.
- Príklad 6: Nájdite všetky topologické usporiadania orientovaného grafu na obrázku vpravo.
- Príklad 7: Napíšte funkciu static int countIsolated(Graph g), ktorá v grafe g spočíta počet izolovaných vrcholov, teda takých, ktoré nemajú žiadnych susedov. Graph je interface z prednášok s metódami getNumberOfVertices, getNumberOfEdges, addEdge, existsEdge, adjVertices.
- Príklad 8: Navrhnite 5 testov pre metódu s hlavičkou a špecifikáciou uvedenou nižšie. Pre každý test uveďte obsah vstupných parametrov a a x, ako aj aký výstup by metóda mala vrátiť a stručne slovne popíšte význam testu. Pokryte obvyklé aj okrajové prípady.
/** Z pola a vyhodi prvy vyskyt objektu rovneho x * pricom rovnost sa testuje metodou equals. * Vsetky dalsie prvky posunie o jedno dolava a na koniec * pola da null. * Vrati true, ak bolo pole modifikovane, inak false. * Ak a je null alebo x je null, vyhodi java.lang.NullPointerException */ public static boolean remove(Object[] a, Object x) {
- Príklad 9: Uveďte, čo vypíše nasledujúci program:
class E0 extends Exception {} public class Prog { static void f(int x) throws E0 { try { if(x==0) throw new E0(); System.out.print('A'); } catch (E0 e) { System.out.print('B'); throw new E0(); } } public static void main(String[] args) { for(int x=0; x<=1; x++) { try { System.out.print(x); f(x); System.out.print(x); } catch (E0 e) { System.out.print('C'); } } } }
- Príklad 10: Uveďte, čo vypíše nasledujúci program:
class A { private int x; public A() { x = 0; } public void print() { System.out.println(x); } public void inc() { x++; } public void work() { inc(); print(); } } class B extends A { private int y; public B() { super(); y=0; } @Override public void inc() { y++; } public static void main(String[] args) { A a = new B(); a.work(); a.work(); a = new A(); a.work(); a.work(); } }
- Príklad 11: Uveďte, čo vypíše nasledujúci program:
public class Prog { static void vypis(int[][] a) { if (a != null) { for (int i = 0; i < a.length; i++) { for (int j = 0; j < a[i].length; j++) { System.out.print(" " + a[i][j]); } System.out.println(); } } else { System.out.println("null"); } } static void vytvorMaticu(int[][] a) { if (a == null) { a = new int[2][]; } a[0] = new int[2]; a[1] = a[0]; a[0][0] = 1; // (*) } public static void main(String[] args) { int[][] a = new int[2][]; vytvorMaticu(a); int[][] b = null; vytvorMaticu(b); vypis(a); vypis(b); } }
Skúška pri počítači
Na skúške pri počítači dostanete k dispozícii program #GraphGUI
- Do programu graphgui budete pridávať riešenia dvoch úloh
- Tieto úlohy bude možné riešiť nezávisle na sebe a budú mať rovnakú váhu, nemusia však byť pre vás rovnako ťažké
- V úlohe A bude treba doprogramovať niečo do grafického prostredia
- Pozri napr. úlohu C na cvičeniach 24
- V úlohe B bude treba naprogramovať nejaký grafový algoritmus, väčšinou metódou prehľadávania s návratom
- Pozri napr. úlohu B na cvičeniach 25, úlohy C, D na cvičeniach cvičeniach 26
- Odovzdávate iba súbory Editor.java (úloha A) a GraphAlgorithm.java (úloha B), celé vaše riešenie by teda malo byť v týchto súboroch.
- Ak kvôli ladeniu meníte iné časti programu, ubezpečte sa, že odovzdané súbory pracujú aj s pôvodnou verziou programu graphgui.
- Súbor ExamSpecifics.java sa môže na každom termíne mierne líšiť, dôležité zmeny budú vysvetlené v zadaní.
Na skúške budete mať prístup aj k dokumentácii k Jave, ktorá je stiahnuteľná tu, resp. prezerateľná tu
- Nebude možné v dokumentácii vyhľadávať, naučte sa v nej preto pred skúškou orientovať navigáciou z farebnej tabuľky na úvodnej stránke
GraphGUI
Knižnica GraphGUI ku skúške
Na skúške pri počítači budete pracovať s knižnicou GraphGUI. Túto knižnicu aj jej dokumentáciou dostanete k dipozícii na skúške, doporučujeme Vám ale sa s ňou oboznámiť vopred.
Do programu graphgui budete pridávať riešenia dvoch úloh
- Tieto úlohy bude možné riešiť nezávisle na sebe a budú mať rovnakú váhu, nemusia však byť pre vás rovnako ťažké
- V úlohe A bude treba doprogramovať niečo do grafického prostredia
- V úlohe B bude treba naprogramovať nejaký grafový algoritmus, väčšinou metódou prehľadávania s návratom
- Odovzdávate iba súbory Editor.java (úloha A) a GraphAlgorithm.java (úloha B), celé vaše riešenie by teda malo byť v týchto súboroch.
- Ak kvôli ladeniu meníte iné časti programu, ubezpečte sa, že odovzdané súbory pracujú aj s pôvodnou verziou programu graphgui.
- Súbor ExamSpecifics.java sa môže na každom termíne mierne líšiť, dôležité zmeny budú vysvetlené v zadaní.
Používanie knižnice
Po spustení GraphGui sa objaví grafický panel na editovanie grafu
- Pri kliknutí vzniká nový vrchol alebo je vrchol označený resp. odznačený
- Označený vrchol má červený rámik, ostatné vrcholy majú čierny rámik.
- Pre označený vrchol program vypíše vpravo zoznam susedov. Kliknutím na suseda v zozname označíme hranu, ktorá ich spája.
- Pri klinutí so Shiftom pridávame hranu z označeného vrchola na kliknutý vrchol.
- Označený vrchol vieme zmazať tlačidlom Delete vertex
- Označenú hranu vieme zmazať tlačidlom Delete edge
- Po stlačení tlačidla Edit sa spustí vaše riešenie úlohy A, po stlačení tlačidla Action sa spustí vaše riešenie úlohy B
Dôležité súčasti knižnice
GraphInterface
- Interface pre prácu s neorientovaným grafom.
- Umožňuje pridávať aj mazať vrcholy aj hrany.
- Vrcholy a hrany sú objekty typu Vertex a Edge.
- Vrcholy aj hrany si pamätajú hodnotu (value) ako referenciu na hodnotu typu Object, takže je možné si vo vrchole resp. na hrane uložiť ľubovoľné číslo, reťazec a pod.
- Vrcholy sú číslované 0,...,N-1, kde N je počet vrcholov, a v algoritmoch je možné ku nim pristupovať buď cez tieto čísla (id) alebo cez samotné objekty typu Vertex. Pri pridávaní a mazaní vrcholov sa id vrcholov môžu meniť.
- Vrcholy majú uložené aj údaje o svojich súradniciach, farbe a veľkosti. Hrany majú uloženú tiež svoju farbu.
- Vo vašich častiach programu nevolajte žiadne metódy tried Vertex a Edge, pristupujte k týmto objektom len metódami GraphInterface.
public interface GraphInterface { public int getNumberOfVertices(); public Vertex getVertex(int index); public int getVertexId(Vertex vertex); public Iterable<Vertex> getVertices(); public Iterable<Vertex> adjVertices(Vertex vertex); public Iterable<Integer> adjVertexIds(Vertex vertex); public Iterable<Edge> adjEdges(Vertex vertex); public void setEdgeValue(Edge e, Object value); public Object getEdgeValue(Edge e); public void setVertexValue(Vertex vertex, Object value); public Object getVertexValue(Vertex vertex); public Color getVertexColor(Vertex vertex); public void setVertexColor(Vertex vertex, Color color); public Color getEdgeColor(Edge edge); public void setEdgeColor(Edge e, Color color); public Vertex addVertex(int x, int y); public Vertex addVertex(int x, int y, Object value); public Vertex addVertex(int x, int y, int size); public Vertex addVertex(int x, int y, int size, Object value); public void deleteVertex(Vertex vertex); public void deleteVertex(int index); Edge addEdge(Vertex n1, Vertex n2, Object value); Edge addEdge(int n1, int n2, Object value); Edge addEdge(Vertex n1, Vertex n2); Edge addEdge(int n1, int n2); void deleteEdge(Edge e); void deleteEdge(Vertex n1, Vertex n2); void deleteEdge(int n1, int n2); Edge findEdge(Vertex n1, Vertex n2); Edge findEdge(int n1, int n2); Vertex getEdgeOrigin(Edge e); Vertex getEdgeDestination(Edge e); void setVertexPosition(Vertex v, Point p); Point getVertexPosition(Vertex v); }
Graph, Vertex, Edge
Sú pomocné triedy reprezentujúce graf a jeho súčasti
- Trieda Graph implementuje interface GraphInterface. Používajte iba premenné typu GraphInterface a metódy, ktoré tento interface špecifikuje.
- Vertex a Edge reprezentujú vrcholy a hrany grafu. Aj keď budete používať premenné týchto typov, používajte ich len ako argumenty pre metódy z GraphInterface.
MyFrame, MyPanel
Sú rozšírenia grafických kompomentov SWINGu, upravených pre naše grafové prostredie
- MyFrame je obsiahnutý v GraphGUI.java a je to neverejná trieda reprezentujúca frame pre hlavné grafické okno
- MyPanel je grafický panel pre editovanie samotného grafu
- Okrem akcií priamo na ňom vie odovzdať aj práve vybraný vrchol alebo hranu
- Obsahuje funkciu na vykresľovanie vrcholov a hrán grafu
- Tiež má v sebe funkcie pre načítanie a výpis grafu
ExamSpecifics
ExamSpecifics je trieda, ktorá obsahuje niekoľko nastavení, ktoré sa môžu v závislosti od zadaní skúšky meniť. Patrí sem napríklad
- rozšírené načítanie grafu (načítanie aj hodnôt vrcholov a hrán, ktoré môžu byť rôznych typov - keďže sú všeobecne typu Object)
- podobne aj rozšírené vypísanie týchto hodnôt - ak by napríklad neboli primitívnych typov
- nastavenie vstupného a výstupného súboru
Obsahuje tiež volanie triedy Editor, ktorú budete implementovať úlohu A
GraphAlgorithm
Trieda, ktorá má obsahovať implementáciu požadovaného grafového algoritmu z úlohy B. V tejto triede používajte iba metódy GraphInterface.
Ukážkový príklad tejto triedy:
package graphgui; import java.awt.Color; /** Trieda, ktorá má obsahovať implementáciu * požadovaného grafového algoritmu z úlohy B. V tejto triede používajte * iba metódy GraphInterface, nevolajte priamo žiadne metódy triedy * Vertex ani z iných tried z balíčka graphgui. * Ak to úloha vyžaduje, graf môžete modifikovať metódami * z GraphInterface. * Nemeňte hlavičku konštruktora ani metód getMessage a performAlgorithm, môžete * však samozrejme zmeniť ich telo a pridávať do triedy vlastné * metódy a premenné. */ public class GraphAlgorithm { /** Samotný graf, na ktorom spúšťame algoritmus */ private GraphInterface graph; /** Vybraný vrchol, ktorý je parametrom algoritmu, * môže byť aj null. */ private Vertex selected; /** Vybraný vrchol, ktorý je parametrom algoritmu, * môže byť aj null. */ private Edge selectedEdge; /** Konštruktor triedy, ktorý dostane graf, vybraný vrchol a hranu * (tento vrchol a hrana môžu byť aj null). */ public GraphAlgorithm(GraphInterface graph, Vertex selected, Edge selectedEdge) { // uloz vstupne udaje this.graph = graph; this.selected = selected; this.selectedEdge = selectedEdge; } /** Metóda, ktorá vráti výstup algoritmu v textovom formáte * podľa špecifikácie v zadaní ulohy. */ public String getMessage() { // v tomto ukazkovom programe iba vypiseme, ci je graf prazdny if (graph.getNumberOfVertices() > 0) { return "Neprazdny"; } else { return "Prazdny"; } } /** Metóda, ktorá upraví graf podľa špecifikácie v zadaní úlohy */ public void performAlgorithm() { // ukazkove pouzitie grafu: prefarbi vsetky vrcholy na zlto for (Vertex n : graph.getVertices()) { graph.setVertexColor(n, Color.yellow); } // ak bol nejaky vrchol vybrany, premaluj ho na oranzovo if (selected != null) { graph.setVertexColor(selected, Color.orange); } } }
Editor
Trieda, ktorá má obsahovať implementáciu úlohy A na skúške.
Obsahuje
- odkaz na volajúci frame (pozor nie typu MyFrame, ale JFrame)
- graf (typu GraphInterface)
Vašou úlohou bude modifikovať metódy tejto triedy. Metódy triedy sú volané v prípade stlačenia gombíka Edit podľa toho, ako bude špecifikované v ExamSpecifics.java.
- V príklade nižšie, ak je vybratý vrchol, zavolá sa metóda editVertex a ak nie je vybratý vrchol, zavolá sa metóda edit
Odporúčame vyhadzovať aj nejaké výnimky, sú odchytávané a vypísané na System.err.
V tomto jednoduchom príklade sa v prípade, že je vybratý vrchol, zobrazí jeho číslo a ak nie je vybratý vrchol, zobrazí sa hláška Hello wold a vyhodí sa výnimka.
public class Editor { private final JFrame parent_frame; private final GraphInterface graph; public Editor(JFrame parent_frame, GraphInterface graph) { this.parent_frame = parent_frame; this.graph = graph; } /** * Akcia, ktorá sa má vykonať v prípade, že je vybratý nejaký vrchol * @param vertex vybraný vrchol */ public void editVertex(Vertex vertex) { JOptionPane.showMessageDialog(parent_frame,"vertex "+graph.getVertexId(vertex)); } /** * Akcia, ktorá sa má vykonať v prípade, že nie je vybratý žiadny vrchol */ public void edit() throws Exception{ JOptionPane.showMessageDialog(parent_frame, "Hello world!"); throw new Exception("hups, nic nevybrate, co mam editovat"); } }
Letný semester, projekt
- Súčasťou bodovania je aj nepovinný projekt, za ktorý môžete dostať 10% bonus.
- Je to príležitosť vyskúšať si písanie väčšieho programu, pričom v projekte máte väčšiu voľnosť ako pri domácich úlohách
- Projekty, ktoré nebudú spĺňať podmienky uvedené nižšie, budú hodnotené 0 bodmi, nemá teda význam odovzdávať nedokončené programy
Požiadavky na projekt
- Projekt musí byť na jednu z tém nižšie. Témy však nepopisujú presné požiadavky, poskytujú len námety, z ktorých si môžete vybrať a prípadne ich ďalej rozšíriť.
- Projekt musí byť napísaný v Jave a spustiteľný v Netbeans v učebniach na fakulte a napísaný prehľadne s dostatkom komentárov.
- Používať môžete len štandardné javovské knižnice (a prípadne JUnit). Okrem toho samozrejme môžete používať kód poskytnutý v rámci predmetu. Zvyšok programu by mal byť z väčšej časti napísaný Vami. Ak použijete nejaké úryvky kódu z internetu alebo iných zdrojov, v komentároch jasne uveďte zdroj.
- Program by mal poskytovať grafické užívateľské prostredie, mal by byť príjemne ovládateľný a mal by sa vyrovnať aj s neobvyklým správaním používateľa (nemal by padať na chyby, okrem naozaj závažných neobvyklých situácií ako nedostatok pamäte).
- Váš program by mal byť schopný načítavať spracovávané dáta zo súboru a po ich prípadnej zmene ich opäť do súboru uložiť. Formát súboru si môžete zvoliť, program by však nemal padať, ak dostane súbor v zlom formáte alebo s nezmyselnými dátami.
Projekt spĺňajúci tieto požiadavky získa aspoň 5% bonusových bodov, pričom je možné získať až 10% v závislosti od náročnosti a kvality vypracovania projektu.
Termíny a odovzdávanie
- V prípade, že chcete robiť projekt, treba si vybrať jednu z ponúkaných tém a ohlásiť to v Moodli do 7.5. [1]
- Doporučujeme však toto rozhodnutie spraviť čím skôr, aby ste na projekt mali dosť času, koncom semestra býva veľa práce na všetkých predmetoch.
- Samotný projekt je potrebné odovzdať do 22.5. 22:00 opäť v Moodli [2]
- V Moodli odovzdajte
- zip súbor obsahujúci zdrojový kód a ďalšie potrebné súbory, napr. bitmapy
- jar súbor so skompilovaným projektom
- zip súbor obsahujúci niekoľko príkladov vstupných súborov
- popis projektu vo formáte txt alebo pdf. V tomto popise stručne vymenujte, aké možnosti váš program používateľovi poskytuje a popíšte štruktúru programu (hlavné triedy, prípadne ich najdôležitejšie metódy). Úlohou popisu je zjedodušiť opravovateľovi orientáciu vo vašom projekte.
- Projekt je tiež potrebné prísť predviesť vyučujúcim v dopredu oznámených termínoch
- Na tieto termíny sa bude zapisovať v AISe
- Zverejníme odhadovaný časový rozvrh, aby ste vedeli, kedy približne prídete na radu
- Vyučujúcim predvediete používanie vášho programu a hlavné časti zdrojového kódu, podobne ako na poobednajšej časti skúšky
- Body oznámime prostredníctvom Moodlu až po prezretí všetkých projektov
Témy projektov
- Rozšírte #DÚ9 (ostrovy) o grafické rozhranie, ktoré zobrazí mapu ostrovov, umožňuje užívateľovi túto mapu editovať a prezerať si vlastnosti jednotlivých ostrovov (napr. dĺžku pobrežia, plochu atď), prípadne vyhľadávať ostrovy v mape podľa zadaného rozsahu určitej vlastnosti. Pri editovaní mapy sa pokúste ponúknuť paletu viacerých nástrojov na vytváranie väčších plôch pevniny (alebo mora) buď pevne daného tvaru alebo s náhodným prvkom. Môžete rozšíriť program aj o detekciu jazier -- políčok vody uzavretých v ostrove a prípadne aj ostrovov vo vnútri týchto jazier.
- Rozšírte #DÚ12 (diamanty) o grafické rozhranie umožňujúce užívateľovi vytvárať si hraciu plochu a modifikovať ju pomocou pridávania alebo úpravy kameňov. Tiež by ste mali umožniť režim pre hranie hry - umožnenie korektných ťahov a následnú likvidáciu vybuchnutých kameňov. Môžete tiež umožniť hru pre dvoch hráčov, ktorí sa budú striedať a aplikácia im bude počítať skóre.
- Rozšírte #DÚ13 (roboty) o grafické rozhranie, ktoré zobrazí hernú plochu a dovolí uživateľovi vytvoriť alebo editovať postupnosť robotov aj s ich nastaveniami (čas príchodu, typ) a potom simuluje roboty a vypisuje aktuálne štatistiky úspešnych a mŕtvych robotov. Môžete prípadne naprogramovať aj editor hernej plochy. Môžete tiež do hry pridať ďalšie typy robotov alebo políčok.
- Rozšírte #DÚ15 (polyline) o ďalšie možnosti editácie lomených čiar. Používateľ bude môcť okrem jednoduchého pridávania, posunu a mazania vrcholov aj vrcholy posúvať pomocou ťahania (napr. pomocou ľavého tlačidla myši). Tiež bude možnosť práce s celou lomenou čiarou (prípadne s jeho vybranou časťou), ktorú bude možné posúvať, škálovať a otáčať podobne, ako to robia grafické editory s obrázkom, pričom úpravu lomenej čiary prispôsobte jej ďalšiemu použitiu. Môžete napríklad naprogramovať tvorbu jednoduchých kriviek (bezierové krivky, interpolačné, b-spline atď.), prípadne editor vrcholov lomenej čiary - tvar, farby.
Prednáška 26
Ciele predmetu
- Prehĺbiť a rozšíriť zručnosti v algoritmickom uvažovaní, písaní a ladení programov z predchádzajúceho semestra
- Oboznámiť sa so základnými programovými a dátovými štruktúrami jazyka Java
- Zvládnuť základy objektovo-orientovaného programovania a tvorby programov s grafickým užívateľským rozhraním
- Oboznámiť sa s ďalšími základnými algoritmami a dátovými štruktúrami (najmä grafy a ich prehľadávanie)
Technické detaily
- Budeme používať verziu Java SE 6 (zopár rozdielov oproti Java 7)
- Budeme naďalej používať systém Netbeans, ale
- Pre Javu je lepšie prispôsobený ako pre C/C++
- Netbeans čisto pre Javu by mal byť jednoduchšie nainštalovateľný pod Windows
- Ak viete ako, môžete používať aj v iné prostredia, napr. Eclipse, prípadne textový editor, problémom môže byť editor grafických prostredí. Pozor, na skúške len štandardné Linuxové prostredie v učebniach.
Literatúra
- Knihy na prezenčné štúdium vo fakultnej knižnici:
- B. Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Staršie 2. vydanie v českom preklade v knižnici so signatúrou D-INF-E-1b, 3. vydanie v angličtine online, doporučujeme však radšej 4. vydanie
- M. T. Goodrich, R. Tamassia: Data Structures and Algorithms in Java. John Wiley & Sons, 2006. Signatúra D-INF-G-5
- R. Sedgewick: Algorithms in Java, Parts 1-4, Fundamentals, Data Structures, Sorting, Searching, Addison-Wesley, 2003. Signatúra D-INF-S-1/I-IVa
- R. Sedgewick: Algorithms in Java, Part 5, Graph Algorithms Addison-Wesley, 2004. Signatúra D-INF-S-1/V (I)
- B. Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Dokumentácia k jazyku Java od firmy Oracle (používame verziu Java SE 6):
- Tutoriál jazyka Java
- referenčná príručka k štandardným knižniciam
Pravidlá na tento semester
- Podobné ako minulý semester
- Pozrite si pravidlá na stránke predmetu http://compbio.fmph.uniba.sk/vyuka/prog/ a prihláste sa na tento predmet v systéme Moodle (s tým istým heslom)
- Zmeny oproti minulému semestru:
- Iba jedna prednáška do týždňa
- Menej väčších úloh, vyžadujú priebežnú prácu
- Nebudú bonusy na DÚ
- Bude nepovinný bonusový projekt za 10%. Témy na projekt a bližšie informácie oznámime neskôr.
- Na rozcvičkách môžu byť aj témy z minulého semestra (práca s poľami, zoznamami, stromami, rekurzia,...), ale v Jave
- Test pre pokročilých?
Odporúčania
- Doučte sa rekurziu
- Neopisujte
- Pracujte na DÚ priebežne, nie tesne pred termínom
- Ak niečomu nerozumiete alebo potrebujete poradiť s DÚ, pýtajte sa
- Skontrolujte si poriadne v Moodli, či je DÚ naozaj odovzdaná
- Reklamácie bodov riešte najskôr s tým, kto opravoval, ale v prípade problémov dajte vedieť niektorej vyučujúcej. Spravte tak čím skôr, nie až na skúške.
Začiatok semestra
- Prvá úloha zverejnená zajtra
- Druhá úloha zverejnená budúci týždeň
- Prvá rozcvička o týžden
- Cvičenia: 1i1 streda 14:00 M217, 1i2 streda 15:40 M217.
Hello world
V Netbeans
Vytvorenie projektu:
- V menu zvolíme New Project
- Na prvej obrazovke zvolíme Categories: Java a Projects: Java Application
- Na ďalšej obrazovke Project name: hello a Create Main Class: hello.Hello
- Do súboru Hello.java napíšeme text:
package hello; public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
- Potom spúšťame podobne ako program v jazyku C++
V Linuxe na príkazovom riadku
Ak chcete Javu skúsiť bez použitia Netbeans:
- Vytvoríme adresár hello, v ňom súbor Hello.java s rovnakým obsahom ako vyššie
- Kompilácia javac hello/Hello.java (vznikne súbor hello/Hello.class)
- Spustenie java hello.Hello
- Pozor, meno adresára musí sedieť s menom balíčka (hello), meno súboru s menom triedy (Hello)
- Ak vynecháme riadok package hello;, môžeme mať súbor Hello.java priamo v aktuálnom adresári.
Väčší program
- Ukážme si teraz väčší program, v ktorom bude aj načítanie vstupu, polia a rekurzia.
- Je to javová verzia C++ programu na generovanie variácií bez opakovania z minulého semestra.
- Jednotlivé jazykové konštrukty použité v programe rozoberieme nižšie v texte.
Najskôr v C++:
#include <iostream> using namespace std; void vypis(int a[], int k) { for (int i = 0; i < k; i++) { cout << a[i]; } cout << endl; } void generuj(int a[], bool bolo[], int i, int k, int n) { /* v poli a dlzky k mame prvych i cifier, * v poli bolo mame zaznamenane, ktore cifry su uz pouzite, * chceme vygenerovat vsetky moznosti * poslednych k-i cifier */ if (i == k) { vypis(a, k); } else { for (int x = 0; x < n; x++) { if (!bolo[x]) { a[i] = x; bolo[x] = true; generuj(a, bolo, i + 1, k, n); bolo[x] = false; } } } } int main(void) { int k, n; cin >> k >> n; int *a = new int[k]; bool *bolo = new bool[n]; for (int i = 0; i < n; i++) { bolo[i] = false; } generuj(a, bolo, 0, k, n); delete[] a; delete[] bolo; }
- A teraz v Jave:
package hello; import java.util.Scanner; public class Hello { static void vypis(int[] a) { for (int x : a) { System.out.print(" " + x); } System.out.println(); } static void generuj(int[] a, boolean[] bolo, int i, int n) { /* v poli a dlzky k mame prvych i cifier, * v poli bolo mame zaznamenane, ktore cifry su uz pouzite, * chceme vygenerovat vsetky moznosti * poslednych k-i cifier */ if (i == a.length) { vypis(a); } else { for (int x = 0; x < n; x++) { if (!bolo[x]) { a[i] = x; bolo[x] = true; generuj(a, bolo, i + 1, n); bolo[x] = false; } } } } public static void main(String[] args) { int k, n; Scanner s = new Scanner(System.in); k = s.nextInt(); n = s.nextInt(); int[] a = new int[k]; boolean[] bolo = new boolean[n]; for (int i = 0; i < n; i++) { bolo[i] = false; } generuj(a, bolo, 0, n); } }
Základy jazyka Java
Primitívne typy, polia a referencie
Primitívne typy (podobné na C/C++)
- int: 32-bitové číslo so znamienkom, hodnoty v rozsahu -2,147,483,648..2,147,483,647 (ďalšie celočíselné typy byte, short, long)
- double: 64-bitové desatinné číslo s pohyblivou desatinnou čiarkou (a 32-bitový float)
- boolean: hodnota true alebo false
- char: 16-bitový znak v kódovaní Unicode (podporuje teda napr. slovenskú diakritiku)
Lokálne premenné treba inicializovať, inak kompilátor vyhlási chybu:
int y; System.out.println(y); // variable y might not have been initialized
V poliach a v objektoch kompilátor inicializuje premenné na 0, null, resp. false.
Polia
- Polia v Jave vedia svoju dĺžku, nemusíme ju ukladať v dalšej premennej
- Pole musíme alokovať príkazom new:
double[] a; // deklarujeme premennu typu pole desatinnych cisel, zatial ma neinicializoavu hodnotu a = new double[3]; // alokujeme pole troch desatinnych cisel for (int i = 0; i < a.length; i++) { // do pola ulozime cisla 0..2 a[i] = i; }
- Alebo mu môžeme priradiť počiatočné hodnoty: double[] a = {0.0, 1.0, 2.0};
- Java kontroluje hranice polí, napr. System.out.println(a[3]); spôsobí chybu počas behu programu: Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
Referencie
- Každá premenná v Jave obsahuje buď hodnotu primitívneho typu alebo referenciu.
- Referencia, podobne ako smerník v C, predstavuje adresu v pamäti.
- Referencia môže ukazovať na pole alebo objekt, ale nie na primitívny typ.
- Nefunguje smerníková aritmetika.
- Referencie môžu mať hodnotu null, ak neukazujú na žiadnu pamäť.
- Na jedno pole alebo objekt môžeme mať viac referencií:
double[] a = {0.0, 1.0, 2.0}; double[] b = a; // skopiruje referenciu na to iste pole do b a[1]+=2; // zmenime pole, na ktore ukazuju a aj b System.out.println(b[1]); // vypise cislo 3.0 a = new double[2]; // a a b teraz ukazuju na rozne polia
- V Java nemusíme polia odalokovať, program to spraví sám, keď už na nich nie je žiadna referencia (garbage collection)
Operátory, cykly, podmienky
- Operátory podobne ako C/C++, napr. aritmetické +, -, *, /, %, priradenie =, +=,..., ++, --, logické !, &&, ||, porovnávanie ==, !=, >=,...
- Pozor pri referenciách operátor == testuje, či ukazujú na tú istú pamäť, nie či je v tej pamäti tá istá hodnota
- Podmienky if, if else, switch rovnako ako v C
- Cykly for, while, do .. while podobne ako v C, podobne break a continue
Navyše Java má cyklus for, ktorý ide cez všetky hodnoty v poli aj bez indexovej premennej
- Tu vidíme dva spôsoby ako vypísať obsah poľa
double[] a = {0.0, 1.0, 2.0}; for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } for (double x : a) { System.out.println(x); }
- Pozor, takýto cyklus sa nedá použiť na zmenenie hodnôt v poli:
for (double x : a) { x = 0; // nemeni pole, iba lokalnu premennu x }
Funkcie (statické metódy) a ich parametre
- Ak chceme písať menší program bez vlastných objektov, ako sme robili v C, použijeme statické metódy umiestnené v jednej triede
- Pred každé meno metódy okrem návratového typu píšeme slovo static
- Pred main píšeme aj slovo public, aby bola viditeľná aj mimo aktuálneho balíčku.
- Návratový typ funkcie main je void, argumenty sú v poli reťazcov (nie je tam meno programu ako v C)
- Parametre sa odovzdávajú hodnotou
- Ak ide o primitívny typ, funkcii sa skopíruje jeho hodnota
- Ak ide o referenciu na pole alebo objekt, funkcii sa skopíruje táto referencia, funkcia môže teda meniť tento objekt alebo pole
- Nedá sa teda napísať funkcia swap, ktorá vymení obsah dvoch premenných
- Tu je ilustratívny príklad:
static void pokus(int[] a, int x) { a[1] = 5; // zmeni v poli na ktore ukazuje a aj b a = new int[3]; // a ukazuje na nove pole, b na stare System.out.println(a[1]); // vypise 0 x = 6; // zmeni v x, y ostava ta ista } public static void main(String[] args) { int[] b = {1, 2, 3}; int y = 4; pokus(b, y); System.out.println(b[1]); // vypise 5 System.out.println(y); // vypise 4 }
- Návratový typ void, primitívny typ alebo referencia
- Príkaz return ako v C
Práca s maticami
- V poli môžeme mať aj referencie na iné polia, dostávame tak viacrozmerné matice, podobne ako v C-čku.
- Deklarácia 3-rozmerného poľa: int[][][] a;
- Ak sú všetky rozmery známe, môžeme ho jedným príkazom alokovať, napr. a=new int[2][3][4];
- Môžeme však spraviť napr. trojuholníkovú maticu, v ktorej má každý riadok inú dĺžku:
package hello; public class Hello { static void vypis(int[][] a) { /* vypiseme cisla v matici a na konzolu */ for (int[] riadok : a) { for (int x : riadok) { System.out.print(" " + x); } System.out.println(); } } public static void main(String[] args) { int[][] a = new int[3][]; for (int i = 0; i < a.length; i++) { a[i] = new int[i+1]; for (int j = 0; j < a[i].length; j++) { a[i][j] = i * j; } } vypis(a); } }
Výstup:
0 0 1 0 2 4
- Podobne 3-rozmerné pole s rôzne veľkými podmaticami a riadkami:
static void vypis(int[][][] a) { /* vypiseme cisla v 3D poli a na konzolu */ for (int[][] matica : a) { for (int[] riadok : matica) { System.out.print("["); for (int x : riadok) { System.out.print(" " + x); } System.out.print(" ] "); } System.out.println(); } } public static void main(String[] args) { int[][][] a = new int[3][][]; for (int i = 0; i < a.length; i++) { a[i] = new int[i + 1][]; for (int j = 0; j < a[i].length; j++) { a[i][j] = new int[j + 1]; for (int k = 0; k < a[i][j].length; k++) { a[i][j][k] = i * j * k; } } } vypis(a); }
Výstup:
[ 0 ] [ 0 ] [ 0 1 ] [ 0 ] [ 0 2 ] [ 0 4 8 ]
Reťazce
- Objekt triedy String, po vytvorení sa nedá meniť
- Text medzi úvodzovkami je považovaný za String
- Inicializácia konštantným reťazcom: String greeting = "Hello world!";
- Operátor + konkatenuje reťazce. Ak je jeden operand reťazec, iné typy konvertuje na String:
int x=1; String str = "Hodnota x: " + x;
Prístup k reťazcu:
- dĺžka sa počíta metódou length() a i-ty znak metódou charAt(i)
String str = "Ahoj!"; int len = str.length(); // dlzka retazca for (int i = 0; i < len; i++) { System.out.println(i + ". znak: " + str.charAt(i)); }
Výstup:
0. znak: A 1. znak: h 2. znak: o 3. znak: j 4. znak: !
- Porovnanie reťazcov na rovnosť metódou equals (Pozor, porovnanie == testuje, či ide o to isté miesto v pamäti)
String str1 = "abc"; // retazec abc String str2 = str1; // referencia na ten isty retazec String str3 = str1 + ""; // vznikne novy retazec abc if (str1 == str2) { // true, lebo to iste miesto System.out.println("str1==str2"); } if (str1 == str3) { // false, lebo rozne miesta System.out.println("str1==str3"); } if (str1.equals(str3)) { // true, lebo zhodne retazce System.out.println("str1.equals(str3)"); }
- Veľa ďalších metód, pozri dokumentáciu
- Ak potrebujete reťazec meniť, napr. k nemu postupne pridávať, môžete použiť StringBuilder
- Rýchlejšie ako stále vyrábať nové reťazce pomocou operátora + (pre spájanie malého počtu častí stačí String)
- Napr. dva spôsoby ako vytvoriť reťazec abeceda obsahujúci písmená a..z:
// Pomocou String, postupne vytvorí 27 rôznych String-ov String abeceda = ""; for (char c = 'a'; c <= 'z'; c++) { abeceda = abeceda + c; // vytvori novy String, naplni ho novym obsahom }
// Pomocou StringBuilder, vytvorí jeden StringBuilder a jeden String StringBuilder buffer = new StringBuilder(); for (char c = 'a'; c <= 'z'; c++) { buffer.append(c); // modifikuje objekt buffer } String abeceda = buffer.toString(); // vytvori novy String
Vstup, výstup, súbory
- Java má rozsiahle knižnice, uvádzame len návod na základnú prácu s textovými súbormi.
- Vo väčšine prípadov potrebujeme triedy z balíčku java.io, takže si ich môžeme naimportovať všetky: import java.io.*;
- Trieda Scanner je v balíčku java.util, použijeme teda import java.util.Scanner;
- V prípade, že pri práci so súbormi nastane nečakaná chyba, Java použije mechanizmus výnimiek (expection)
- O výnimkách sa budeme učiť neskôr, nateraz len do metódy main (a prípadne ďalších metód) pridáme upozornenie, že výnimka môže nastať:
- public static void main(String[] args) throws java.io.IOException { ... }
Písanie na konzolu
- System.out.print(retazec)
- System.out.println(retazec) - pridá koniec riadku
- Reťazec môžeme vyskladať z viacerých častí rôznych typov pomocou +
- Formátovanie podobné na printf c C-čku: System.out.format("%.1f%n", 3.15); vypíše číslo na jedno desatinné miesto, t.j. 3.2 a koniec riadku podľa operačného systému.
- Viac detailov v dokumentácii alebo v tutoriáli
Čítanie z konzoly
- Objekt System.in je typu FileInputStream a podporuje iba čítanie jednotlivých bajtov resp. polí bajtov
- Lepšie sa pracuje, ak ho použijeme ako súčasť objektu, ktorý vie tieto bajty spracovať do riadkov, čísel a pod.
- Trieda BufferedReader umožňuje čítať celý riadok naraz ale aj znak po znaku. Tu je príklad jej použitia:
package hello; import java.io.*; // potrebujeme nejake triedy z balicka java.io public class Hello { public static void main(String[] args) throws java.io.IOException { // musime pridat oznam, ze moze vzniknut vynimka - chyba pri citani BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while (true) { // nacitame riadok do retazca String line = in.readLine(); // skoncime, ked uzivatel zada prazdny riadok alebo ked prideme na koniec vstupu (null) if (line == null || line.equals("")) { break; } // vypiseme nacitany riadok System.out.println("Napisali ste riadok \"" + line + "\""); } System.out.println("Koncime..."); } }
Príklad behu programu:
Ahoj Napisali ste riadok "Ahoj" 1 2 3 Napisali ste riadok "1 2 3" Koncime...
- Metóda readLine() teda číta celý riadok (odstráni znak pre koniec riadku), metóda read() číta jeden znak (na konci súboru vráti -1)
- Trieda Scanner rozkladá vstup na slová oddelené bielymi znakmi (medzery, konce riadku a pod.) a prípadne ich premieňa na čísla.
- Príklad programu, ktorý vypisuje slová načítané od užívateľa, kým nezadá END alebo neskončí vstup
package hello; import java.util.Scanner; public class Hello { public static void main(String[] args) { Scanner s = new Scanner(System.in); // inicializujeme Scanner int num = 0; while (s.hasNext()) { // kym neskonci vstup String word = s.next(); // nacitame slovo if (word.equals("END")) { // skoncili sme ak najdeme END break; } System.out.println("Slovo " + num + ": " + word); // vypiseme slovo num++; } } }
Príklad behu programu:
Ahoj Slovo 0: Ahoj 1 2 3 END Slovo 1: 1 Slovo 2: 2 Slovo 3: 3
- Metóda nextInt() vráti ďalšie slovo konvertované na int (pozri program s rekurziou vyššie), overiť si, či nasleduje číslo, môžeme vopred metódou hasNextInt(). Podobne nextDouble().
Práca so súbormi
Čítanie zo súboru funguje podobne ako čítanie z konzoly, iba inak inicializujeme použitý objekt:
- Scanner vytvoríme príkazom Scanner s = new Scanner(new File("vstup.txt"));
- File reprezentuje súbor s určitým menom, potrebujeme pridať import java.io.File; alebo import java.io.*;
- BufferedReader vytvoríme príkazom BufferedReader in = new BufferedReader(new FileReader("vstup.txt"));
- Scanner aj BufferedReader umožňujú zavrieť súbor metódou close()
Písanie do súboru môžeme robiť napr. triedou PrintStream
- Otvorenie súboru: PrintStream out = new PrintStream("vystup.txt");
- Potom používame staré známe metódy print, println, format ako pri System.out (napr. out.println("Ahoj"))
- Na konci zavoláme out.close()
- Tento spôsob otvárania súborov existujúci obsah premaže
- Ak chceme pridávať na koniec súboru, použijeme PrintStream out = new PrintStream(new FileOutputStream("vystup.txt",true));
Matematika a pseudonáhodné čísla
- V triede Math nájdete rôzne matematické konštanty a funkcie
- Napr. Math.PI, Math.cos(x), Math.min(x,y) Math.pow(x,y),...
- Triedy na prácu s veľkými číslami a ďalšie matematické funkcie nájdete v balíčku java.math
Pseudonáhodné čísla
- Math.random() vygeneruje double z intervalu [0,1)
- Väčšie možnosti poskytuje trieda Random v balíčku java.util (generuje celé čísla, bity), umožňuje nastaviť počiatočnú hodnotu
Cvičenia 14
Spúšťanie programu Hello world
Najskôr si vyskúšame vytvoriť a spustiť jednoduchý program v Jave v prostredí Netbeans.
- V menu zvolíme New Project
- Na prvej obrazovke zvolíme Categories: Java a Projects: Java Application
- Na ďalšej obrazovke Project name: hello a Create Main Class: hello.Hello
- Do súboru Hello.java napíšeme text:
package hello; public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
- Potom spúšťame podobne ako program v jazyku C++
Referencie, polia a súbory v Jave
- Odsimulujte nasledovný program a nakreslite, ako budú vyzerať premenné v pamäti v oboch prípadoch, keď sa vykonal riadok označený (*). Zistite, čo program vypíše.
package prog; public class Prog { static void vypis(int[][] a) { if (a != null) { for (int i = 0; i < a.length; i++) { for (int j = 0; j < a[i].length; j++) { System.out.print(" " + a[i][j]); } System.out.println(); } } else { System.out.println("null"); } } static int[] vytvorRiadok() { return new int[2]; } static void vytvorMaticu(int[][] a) { if (a == null) { a = new int[2][]; } a[0] = vytvorRiadok(); a[1] = a[0]; a[0][0] = 1; // (*) } public static void main(String[] args) { int[][] a = new int[2][]; vytvorMaticu(a); int[][] b = null; vytvorMaticu(b); vypis(a); vypis(b); } }
- Napíšte funkciu static int[][] transpose(int[][] a), ktorá transponuje danú maticu a, teda vytvorí a vráti novú maticu, ktorá bude mať v stĺpcoch uložené riadky matice a. Môžete predpokladať, že vstupná matica má všetky riadky rovnako dlhé.
- Napíšte program, ktorý zo súboru matica.txt načíta najprv počet riadkov a stĺpcov matice a potom jej jednotlivé prvky (celé čísla). Do súboru matica2.txt vypíše transponovanú maticu k vstupnej matici v rovnakom formáte, teda najskôr počet riadkov a stĺpcov a potom jednotlivé riadky.
Náhodné čísla
- Pozrite si dokumentáciu k triede Random v balíčku java.util a napíšte program, ktorý odsimuluje 10 hodov kockou, teda vypíše 10 náhodných celých čísel od 1 po 6.
- Napíšte program, ktorý odsimuluje 10 hodov nevyváženou mincou, pri ktorej v každom hode s pravdepodobnosťou 80% padne hlava a s pravdepodobnosťou 20% prípadoch padne znak. Pomôcka: Ak sa chceme rozhodovať medzi dvoma vecami s určitou pravdepodobnosťou x, môžeme vygenerovať náhodné desatinné číslo z intervalu [0,1) a ak je toto náhodné číslo menej ako x, zvolíme jednu možnosť a ak viac ako x, zvolíme druhú.
DÚ11
Odovzdávanie DÚ11 max. 10 bodov, termín odovzdania streda 27.2. o 22:00
Cieľom tejto domácej úlohy je vyskúšať si prácu v Jave s poliami a súbormi.
Vstupný súbor bitmap.txt obsahuje binárnu maticu, ktorá je zadaná ako niekoľko rovnako dlhých riadkov núl a jedničiek, pričom jednotlivé znaky nie sú oddelené medzerami. Súbor teda môže vyzerať napríklad takto:
0000000100 0110010110 0010110100 1000111111 0100011100 0000011110
Matica reprezentuje obrázok vo forme bitmapy, pričom jednotky sú čierne políčka a nuly biele. Vašou úlohou je v matici nájsť najväčší útvar tvaru L pozostávajúci z čiernych políčok. Útvar tvaru L veľkosti k pozostáva zo zvislého pruhu k jedničiek a vodorovného pruhu k jedničiek, ktoré zdieľajú ľavý dolný roh. Na okolitých políčkach môžu byť jednotky aj nuly, L-ko teda môže byť súčasťou väčšieho čierneho útvaru.
Výstupný súbor vystup.txt obsahuje najprv kópiu vstupnej matice, ktorá je ale zmenená tak, že všetky jedničky v najväčšom L-ku sú prepísané písmenom L. Ak v matici neexistuje L-ko pre k>0, maticu nemeňte. Ak je viac možných riešení, vyznačte hociktoré z nich.
0000000100 0110010110 00101L0100 10001L1111 01000L1100 00000LLLL0
Algoritmus: Jednoduchý algoritmus by bolo začať z každého možného ľavého dolného rohu a v cykle spočítať, ako ďaleko sa vieme dostať po jednotkách vo vodorovnom aj zvislom smere. Ak máme na vstupe bitmapu Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle n\times n} , budeme takto skúmať Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle n^2} políčok a pre každé spustíme ešte cyklus na počítanie, ktorý spraví najviac n krokov, takže zložitosť by bola Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^3)} .
Vašou úlohou je ale implementovať efektívnejší algoritmus, ktorý pracuje pre bitmapu Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle n\times n} v čase Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} . Tento algoritmus si najskôr predpočíta dve pomocné matice. V prvej matici si pre každé políčko bitmapy pamätáme, aký veľký súvislý zvislý úsek jedničiek je nad ním. Ak je vo vstupnej bitmape na pozícii (i,j) nula, aj do pomocnej matice dáme nulu. Ak je na pozícii (i,j) jednotka, ale hneď nad ňou je nula, dáme tam jednotku atď. Príklad celej matice je uvedený nižšie. Na prvý pohľad aj výpočet tejto matice vyžaduje čas Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^3)} , ak by sme jej hodnoty rátali pre každé políčko zvlášť. Ak však poznáme hodnotu na pozícii (i,j), ľahko z nej aj bez ďalšieho cyklu spočítame hodnotu o riadok nižšie na pozícii (i+1,j). Druhá pomocná matica je podobná, ale pre každé políčko obsahuje veľkosť súvislého vodorovného úseku vedúceho doprava. Túto maticu doporučujeme počítať v každom riadku zprava doľava, pričom hodnotu pre (i,j) využijeme pri výpočte hodnoty vľavo od nej na pozícii (i,j-1).
Keď už máme obe pomocné matice, môžeme porovnať ich hodnoty na danom políčku (i,j) a zistiť, aké najväčšie L-ko má na tomto políčku bitmapy ľavý dolný roh.
Príklad pomocných matíc pre vstupnú bitmapu uvedenú vyššie (zvislé počty vľavo, vodorovné vpravo):
0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 1 0 2 1 0 0 2 1 0 0 1 0 2 1 0 0 0 2 0 1 2 0 3 0 0 0 0 1 0 2 1 0 1 0 0 1 0 0 0 2 3 1 4 1 1 1 0 0 0 6 5 4 3 2 1 0 1 0 0 0 4 2 5 0 0 0 1 0 0 0 3 2 1 0 0 0 0 0 0 0 5 3 6 1 0 0 0 0 0 0 4 3 2 1 0
Technické požiadavky:
- Odovzdávajte jeden súbor Bitmap.java, ktorý bude obsahovať balíček bitmap, v ňom jednu triedu Bitmap s funkciou main. Všetky vytvorené metódy by mali byť statické metódy v tejto triede.
- Dodržujte mená súborov a ich formát podľa príkladu v zadaní.
- Dbajte na úpravu a čitateľnosť vášho programu.
Pomôcky:
- Doporučujeme si v programe uložiť vstup ako dvojrozmerné pole char-ov. Ak riadok načítate ako String, môžete ho zmeniť na pole charov metódou toCharArray() triedy String.
- Nakoľko veľkosť bitmapy nie je vopred daná, môžete predpokladať, že jej každý rozmer je najviac 100. Konštantu zapíšeme ako static final int MAX = 100; v triede Bitmap.
Prednáška 27
Organizačné poznámky
- Cvičenia (od tohto týždňa): 1i1 streda 9:50 M217, 1i2 streda 14:00 M217
- Nepovinné cvičenia: streda o 15:40 sa v M217
- Test pre pokročilých sa bude konať v pondelok 25.2. 14:00-15:30 (stretnutie pred akvarkom I)
- Domáce úlohy
- DU11 odovzdávajte do stredy 27.2. 22:00
- DU12 bude zverejnená v stredu
Základné pojmy
Java je objektovo orientovaný jazyk a teda v podstate všetko, čo budeme používať sú objekty.
- Trieda - definícia objektu: aký má typ dát, operácie ...
class Pes { }
- Objekt je inštanciou triedy: napríklad Dunčo a Punťo sú inštancie psa
Pes dunco;
this je odkaz na konkrétny objekt, s ktorým práve pracujem. Z toho vyplýva niekoľko vecí
- this.premenna je vlastne to isté ako premenna
- this.funkcia(...) to isté ako funkcia(...)
Trieda a jej prvky môžu byť odniekiaľ prístupné a odinakiaľ nie. Na úpravu prístupových práv používame modifikátory. Pre triedu budeme najbežnejšie používať nasledovné:
- keď nič nedáme - je prístupná z package, kde sa nachádza
- public - je prístupná ľubovoľne
Premenné - dáta
Premenné sú lokálne dáta danej triedy.
- normálne (keď nič iné nepovieme) môžu k dátam pristupovať trieda a k nej spriatelené triedy
- ak by sme to nechceli poskytnúť nikomu (samozrejme okrem samotnému objektu danej triedy) použijeme modifikátor private
- ak by sme naopak chceli poskytnúť dáta ľubovoľne použijeme modifikátor public
Okrem toho ešte poznáme ďalšie modifikátory protected, static, final o ktorých sa budeme baviť neskôr
Prístup k premenným je pomocou . a keďže java nepozná smerníky iba referencie nemusíme rozlišovať . a ->
Metódy objektu
- funkcia alebo procedúra, ktorú vie objekt vykonávať nad svojimi dátami
- na rozdiel od statických funkcií, ktoré sme videli doteraz metódy patria k objektu
- prístup k funkciám pomocou '.'
Parametre:
- ako bolo spomínané, parametre sa odovzdávajú hodnotou - teda sa vytvorí lokálna kópia parametra a jej zmenou nedocielim zmenenie pôvodnej premennej
- zmenu premenných môžeme docieliť tým, že ako parameter použijem referenciu a tú síce meniť nemôžem ale obsah pamäte kam ukazujem meniť samozrejme môžem
- špeciálne pre základné premmné sa dá použiť tzv. wrapper class, teda trieda ktorá ich 'obalí' a umožní k nim pristupovať ako k objektom cez referenciu
class MyInteger { private int x; // data public MyInteger(int xIn) { x = xIn; } // konštruktor public int getValue() { return x; } // získanie hodnoty public void setValue(int xIn) { x = xIn;} // nastavenie hodnoty } static void swap(MyInteger rWrap, MyInteger sWrap) { // interchange values inside objects int t = rWrap.getValue(); rWrap.insertValue(sWrap.getValue()); sWrap.insertValue(t); }
Návratová hodnota funkcie:
- void, základný typ, referencia
- príkaz return vráti návratovú hodnotu - použitím príkazu return sa program ukončí
- ako vidno, vieme vrátiť iba jednu hodnotu (a navyše nevieme vrátiť hodnotu zmenou parametra!) - ak chceme vrátiť viac hodnôt, musíme si na to vytvoriť objekt a vrátiť referenciu naň
static class MyReturn { public int x; // data public char c; } static MyReturn funkcia(int x, char c){ MyReturn temp=new MyReturn(); temp.x=x; temp.c=c; return temp; } public static void main(String[] args) { MyReturn temp=new MyReturn(); temp=funkcia(7,'a'); System.out.println(temp.x+" "+temp.c); }
Modifikátory:
- bez modifikátora - je metóda prístupná vrámci package, v ktorej je definovaná jej trieda
- public - je prístupná odvšadiaľ
- private - je prístupná iba pre triedu
- protected,abstract, final - uvidíme neskôr
Pozor, môže byť viac funkcii s rovnakým názvom
- ktorú zavolá?
class Example { public int work(int data_){return data_;} public int work(int data_, int dat_){return data_+dat_;} public char work(char data_){return data_;} public String work(char c1,char c2){String temp=c1+""+c2; return temp;} } Example e=new Example(); System.out.println(e.work(7)); System.out.println(e.work('a','b')); System.out.println(e.work(7,1));
Konštruktor
- Špeciálna metóda objektu, ktorá ten objekt vytvorí
- Názov konštruktora je názov triedy. Teda konštruktor triedy Pes bude teda funkcia Pes().
- Rozdiel medzi metódou objektu a konštruktorom.
- Konštruktor nemá v hlavičke návratovú hodnotu. Návratovou hodnotou je tento objekt samotný, takže nie je potrebné túto návratovú hodnotu špeciálne definovať.
- V tele konštruktora nie je kľúčové slovo return.
- Prvý riadok konštruktora buď volá iný konštruktor tej istej triedy alebo triedy predchodcu (ak toto neurobíme, urobí to kompilátor za nás).
- Volanie iného konštruktora tej istej triedy (často napríklad s menším počtom parametrov) voláme pomocou this()
- Pozor! Tieto rozdiely sú niekedy nie úplne zrejmé pri pohľade do zdrojového kódu, preto sa v niektorých jazykoch používa kľúčové slovo "constructor", ale Java to nemá
- Defaultný konštruktor: V prípade, že nedefinujete pre triedu žiaden koštruktor, bude automaticky vygenerovaný konštruktor bez parametrov (s menom podľa názvu triedy), ktorý zavolá konštruktora predchodcu (o týždeň) a inicializuje premenné na defaultné hodnoty (nula pre číselné premenné, null pre referencie a false pre boolean).
- Defaultný konštruktor je vytvorený iba v prípade, že žiaden iný konštruktor neexistuje.
class Pes { String meno; Pes(){this("Dunco");} Pes(String meno_){meno=meno_;} Pes(String meno1, String meno2){this(meno1+" "+meno2);} }
Vznik objektov
Pes punto=new Pes("Punto");
- dynamicky alokuje pamäť pre objekt
- nastaví štandardné hodnoty pre premenné (null, 0, false)
- zavolá konštruktor objektu - defaultný alebo ten, ktorý sedí s počtom parametrov
- vráti referenciu - pamäťovú adresu objektu
- niekedy nechceme urobiť konštruktor prístupný ľubovoľne a preto si urobíme továreň na naše objekty - funkciu, ktorá nám objekty bude vytvárať (takto funguje konštruktor napríklad v Delphi)
public static Pes VytvorPsa(String meno) { return new Pes(meno); }//tovaren na psy Pes rex=Pes.VytvorPsa("Rex");
Veci z minulej prednáške
Na minulej prednáške sme sa objektom vyhýbali, ale oni tam aj tak sú - keďže v jave je všetko objekt
Základné typy - int, ..
- Aj základné typy majú svoje objektové varianty
- Jedná sa o wrapper typy - Integer, Double .. (viac pozri [3])
Polia
- Aj polia sú vlastne špeciálne objekty, ako vidno napríklad z premennej a.length
- Preto napríklad b=a nefunguje tak ako by sme čakali - je to naozaj iba priradenie referencie
Ukážka zásobníku + zapuzdrenie
- Ukážeme si teraz ako spraviť napríklad zásobník pomocou spájaného zoznamu.
- Vytvoríme si triedu Node, ktorá bude reprezentovať vrchol a prácu s ním
class Node { private int data; private Node next; public Node() {} public Node(int data_) {data=data_;} public Node(int data_, Node next_) {data=data_; next=next_;} public int getData() {return data;} public void setData(int data_) {data=data_;} public Node getNext() {return next;} public void setNext(Node next_) {next=next_;} }
- Samotný zásobník bude opäť trieda
class Stack { private Node front; public Stack() {} public void push(int data_){ Node p=new Node(data_); if (front==null) front=p; else { p.setNext(front); front=p; } } public int pop(){ if (front==null) return -1; int res=front.getData(); front=front.getNext(); return res; } public void printStack() { Node p=this.front; while (p!=null) { System.out.print(p.getData()+" "); p=p.getNext(); } System.out.println(); } }
- Teraz triedu niekde použijeme.
public static void main(String[] args) { Stack s=new Stack(); s.printStack(); s.push(7); s.push(12); s.printStack(); System.out.println(s.pop()); }
- A čo v prípade, že by sme sa teraz rozhodli to vytvoriť radšej pomocou poľa? Nič - proste by sme si to pomocou poľa naprogramovali a zvonku by sme si nič nevšimli.
- V ideálnom prípade by sme mali mať triedy zapuzdrené teda
- zvonku vidno veci, ktoré sú súčasťou API
- zvyšok je zvonku neprístupný
Binárny vyhľadávací strom
Podobne môžeme urobiť aj triedu vrchol stromu a binárny vyhľadávací strom.
class Node { private int data; private Node left, right; public Node() {} public Node(int data_) {data=data_;} public Node(int data_, Node left_, Node right_) {data=data_; left=left_; right=right_;} public int getData() {return data;} public void setData(int data_) {data=data_;} public Node getLeft() {return left;} public void setLeft(Node left_) {left=left_;} public Node getRight() {return right;} public void setRight(Node right_) {right=right_;} public Node getNext(int data_) { if (data_<this.data) return left; else return right; } public void setNext(Node next_){ if (next_.data<=this.data) this.setLeft(next_); else this.setRight(next_); } public void outPrefix(){ if (this!=null) { System.out.println(data); if (this.left!=null) left.outPrefix(); if (this.right!=null) right.outPrefix(); } } public void outInfix(){ if (this!=null) { if (this.left!=null) left.outInfix(); System.out.println(data); if (this.right!=null) right.outInfix(); } } } class Tree { private Node root; public Tree() {} public Tree(Node root_) {root=root_;} public void addNode(int data_){ Node p=new Node(data_); Node temp=this.root; if (temp==null) root=p; else { while (temp.getNext(data_)!=null) temp=temp.getNext(data_); temp.setNext(p); } } public void outPrefix(){if (this.root!=null) root.outPrefix();} public void outInfix(){if (this.root!=null) root.outInfix();} }
Javadoc
- Javadoc slúži na vytváranie dokumentácie k aplikácii - hlavne na API
- Javadoc komentár začína /** a končí ako klasický komentár */ pričom každý riadok začína *
- prvý riadok resp. odsek je slovný popis
- ďalšie špeciálne vlastnosti sa popisujú pomocou tagov [4]
Ako z toho potom vytvoriť dokumentáciu
- automaticky vytvára dokumentáciu iba k public veciam (keďže tie asi tvoria API)
- vo vlastnostiach aplikácie časť Documenting sa dá zmeniť
- samotná stránka sa vygeneruje v časti Run, Generate Javadoc
Komentár triedy
/** * Popis triedy. * * @author meno * @version 1.6 */ class Trieda { // telo triedy }
Komentár premennej
/** * Popis premennej. */ private int premenna = 0;
Komentár metódy
/** * Krátky popis. * * Dlhší popis, ktorý tu môže alebo aj nemusí byť. Môže byť dokonca * na viacerých riadkoch. * * @param premenna bla bla bla. * @return co to vrati. * @throws java.io.IOException popis. */ public int funkcia(int premenna) throws java.io.IOException { }
Cvičenia 15
- Navrhnite triedu Polynomial, ktorá bude reprezentovať polynómy jednej premennej s celočíselnými koeficientami.
- Implementujte vhodnú sadu konštruktorov, napr. konštruktor bez parametrov, ktorý vytvorí nulový polynóm, konštruktor s dvoma celočíselnými parametrami a a i, ktorý vytvorí polynóm Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle ax^i} a konštruktor, ktorý dostane pole a vytvorí polynóm, ktorého koeficienty budú prvky tohto poľa
- Implementujte metódu na vypísanie koeficientov polynómu
- Implementujte metódu getCoefficient(int i), ktorá vráti koeficient pri člene Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle x^i} .
- Implementujte metódu add(Polynomial p), ktorá vráti nový polynóm, ktorý bude súčtom tohto polynómu a polynómu p.
- Implementujte metódu evaluate(int value), ktorá vráti hodnotu polynómu, keď premenná bude mať hodnotu value.
- Na prednáške 27 je implementovaný binárny vyhľadávací strom pomocou tried Node a Tree.
- Implementujte do triedy Tree metódu searchNode(int data_), ktorá vyhľadáva v strome vrchol s hodnotou data_. Skúste využiť metódy triedy Node.
- Implementujte do triedy Tree metódu depth(), ktorá vráti hĺbku stromu. V prípade potreby urobte aj patričné úpravy v triede Node (premenné, metódy). Snažte sa čo najviac zachovať zapuzdrenie tried.
DÚ12
Odovzdávanie DÚ12 max. 20 bodov, termín odovzdania streda 13.3. o 22:00
Cieľom tejto úlohy je vyskúšať si prácu s triedami a systémom JavaDoc, ale aj zopakovať si z minulého semestra prehľadávanie s návratom (backtracking). Doporučujeme začať na tejto úlohe pracovať v dosť veľkom predstihu, nakoľko spája niekoľko potenciálne náročných podúloh:
- naštudovať časť kódu, ktorá je už hotová
- vhodne navrhnúť použité triedy a ich metódy
- implementovať metódy, pričom niektoré budú rekurzívne
- program odladiť a okomentovať systémom JavaDoc.
Hra diamanty
V hre Bejeweled [5] a jej obdobách sa vytvárajú z kociek (tu diamantov) žiadané útvary. Cieľom tejto hry je vymieňať dva susedné kamene a tým vytvoriť vodorovnú alebo zvislú reťaz aspoň 3 kameňov rovnakej farby. Pritom kamene, ktoré sú súčasťou nejakej reťaze, zmiznú a kamene padajúce zhora vyplnia vzniknuté medzery. Tým môžu vzniknúť ďalšie reťaze, ktoré podobne miznú. Kamene budeme mať 4 farieb - červené, zelené, modré a žlté. V tejto úlohe budeme uvažovať špeciálnu verziu hry, kde ďalšie kamene nepribúdajú. Cieľom bude zrušiť všetky kamene.
Vstup
Vstupný súbor obsahuje na prvom riadku číslo N udávajúce rozmery plochy, ktorá bude vždy štvorcová. Nasleduje N riadkov, kde každý obsahuje N znakov z množiny {'r','g','b','y','.'}, ktoré reprezentujú jednotlivé farby kameňov resp. prázdne políčko. Môžete predpokladať, že vstupný súbor existuje a má správny formát.
Ťah
Ťah v hre vyzerá nasledovne:
- Hráč si vyberie kameň na ploche a susediace políčko (nie nutne kameň), s ktorým ho chce vymeniť. Nekorektné výmeny, kde susediace políčko je mimo hracej plochy, hra nedovolí.
- Ťah je korektný, ak vďaka výmene vznikne aspoň jedna reťaz aspoň 3 kameňov rovnakej farby. Nekorektný ťah hra nedovolí.
- Všetky kamene, ktoré sú v nejakej reťazi dĺžky aspoň 3 zmiznú. Potom sú vzniknuté medzery zaplnené padajúcimi kameňmi z vrchnejších radov. Tým môžu opäť vzniknúť reťaze kameňov, ktoré opäť všetky miznú. Padanie kameňov a miznutie reťazí sa opakuje, kým dochádza na ploche k zmenám. Pozor, jeden kameň môže naraz patriť aj do dvoch reťazí (vodorovnej a zvislej).
Môžete predpokladať, že v hernej ploche zadanej vo vstupnom súbore neexistujú žiadne reťaze a žiaden kameň nemôže padnúť nižšie.
Príklad: Máme nasledovnú plochu
.y.. .y.. rrgr yybb
Keď vymeníme zelený kameň s červeným, ktorý je od neho napravo, bude plocha postupne vyzerať takto:
.y.. .y.. .... .... .y.. .y.. .y.. .... rrrg ...g .y.g ...g yybb yybb yybb y.bb
Tým ťah končí. Navyše žiaden ďalší ťah nie je možný.
Výstup
Na štandardný výstup vypíšte poradie ťahov (výmien kameňa a susedného políčka), ktoré spôsobia, že na ploche neostane žiaden kameň. Použite formát ako v príklade nižšie. Ak úloha nemá riešenie, podajte o tom správu. Vypisujte vždy iba jedno riešenie.
Popis programu
Program obsahuje jeden súbor s balíčkom diamonds. Obsahuje niekoľko pripravených tried, ktoré môžete používať a ďalšie, ktoré si musíte sami navrhnúť. Podrobnejší popis tried a ich metód nájdete v JavaDoc komentároch priamo v kostre.
- Trieda Diamonds obsahuje statickú metódu main, v ktorej zo súboru načíta plocha (inštancia triedy Board), následne sa pustí statická metóda solve, ktorá má vykonať samotné prehľadávanie. Metóda main tiež vypíše nájdené riešenie. Vašou úlohou je doprogramovať metódu solve.
- Trieda Position reprezentuje pozíciu kameňa na ploche. Obsahuje súkromné premenné i,j - súradnice kameňa, ku ktorým pristupujeme pomocou funkcií getI(),getJ(). Okrem konštruktora obsahuje metódu toString určenú na vypisovanie pozícií a metódu isCorrect, ktorá určí, či je pozícia vo vnútri plochy. V prípade potreby si doprogramujte aj ďalšie metódy.
- Triedu Exchange máte za úlohu navrhnúť vy. Reprezentuje výmenu medzi kameňom a susedným políčkom. Vymyslite, premenné akých typov potrebujete a navrhnite aj vhodné metódy, Môže sa zísť trieda Position. Pole prvkov triedy Exchange vracia metóda Diamonds.solve ako riešenie problému. Nezabudnite do triedy doprogramovať telo metódy toString, ktorá sa v metóde main používa pri výpise.
- Trieda Board reprezentuje hraciu plochu. V programe sú už hotové niektoré jej metódy, budete ale asi potrebovať navrhnút a implementovať aj ďalšie.
- Trieda má premenné N (veľkosť plochy) a samotnú plochu a reprezentovanú ako dvojrozmernú maticu znakov, pričom voľné miesto je rovnako ako vo vstupnom súbore označené bodkou a farby kameňov malými písmenami.
- Máte k dispozícii 2 konštruktory triedy Board - jeden vytvorí prázdnu plochu, druhý obsah plochy načíta zo súboru.
- Máte tiež funkciu clone, ktorá vytvorí kópiu plochy, funkcie na zisťovanie veľkosti plochy a polôh kameňov, metódu isPartOfGemLine, ktorá zistí, či je daná pozícia súčasťou nejakej reťaze a niekoľko ďalších potenciálne užitočných metód.
- Navrhujeme vytvoriť aj ďalšie metódy, ktoré pracujú s plochou a realizujú na nej zadaný ťah hry.
Algoritmus
Navrhujeme použiť nasledujúci jednoduchý algoritmus:
- Metóda solve nájde na ploche všetky kamene a keď tam žiadne nie sú, riešením je prázdna postupnosť ťahov. (Táto časť už je naprogramovaná)
- Skúste všetky možnosti prvého ťahu, t.j. každý kameň skúste presunúť všetkými smermi, pričom si zakaždým vytvorte novú kópiu plochy, na ktorej budete ťah realizovať. Pozor, nie každý ťah je platný.
- Pre túto novú plochu rekuzívne zavolajte funkciu solve.
- Ak sa podarilo novú plochu vyriešiť, stačí zobrať riešenie pre novú plochu a na jeho začiatok pridať ťah, ktorým sme ju získali (bude treba alokovať o 1 dlhšie pole ťahov). Týmto môžete prehľadávanie ukončiť.
- Ak rekurzívne volanie pre novú plochu nenašlo riešenie, pokračujte vyskúšaním inej možnosti pre prvý ťah.
Požiadavky
- Sústreďte sa predovšetkým na správnosť algoritmu. Rýchlosť programu nie je dôležitá (mal by však byť schopný vyriešiť napríklad vstupu uvedený nižšie).
- Veľmi dôležitý je štýl programu. Používajte správne techniky objektovo orientovaného programovania. Dbajte na zapuzdrenie tried a rozdelenie programu na menšie a logicky dobre definované metódy.
- Všetky nové metódy zdokumentujte systémom JavaDoc
- Pri generovaní JavaDoc dokumentácie v Netbeans si v nastaveniach projektu zapnite, aby generovalo dokumentáciu aj pre private triedy, inak väčšinu programu v JavaDocu neuvidíte.
- Odovzdajte súbor Diamonds.java, ktorý by mal obsahovať celú implementáciu.
Príklad vstupu a výstupu
8 ...bb... ...yy... ...rr... .b.yy.b. .y.yr.y. .r.ry.r. by.yr.yb rr.ry.rr
Vymena [7,3] a [7,2] Vymena [5,1] a [5,2] Vymena [6,1] a [6,2] Vymena [7,1] a [7,2] Vymena [7,4] a [7,5] Vymena [5,6] a [5,5] Vymena [6,6] a [6,5] Vymena [7,6] a [7,5] Vymena [7,3] a [7,2] Vymena [7,4] a [7,5]
Kostra programu
/** * Hra diamonds * */ package diamonds; import java.io.*; import java.util.Scanner; /** * Trieda pre pozíciu kameňa na ploche */ class Position { private int i; private int j; /** * Konštruktor, ktorý vytvorí pozíciu pre súradnicu riadku a stĺpca. * @param _i číslo riadku * @param _j číslo stĺpca */ public Position(int _i, int _j) { i = _i; j = _j; } /** * Vráti číslo riadku pre túto pozíciu. */ public int getI() { return i; } /** * Vráti číslo stĺpca pre túto pozíciu. */ public int getJ() { return j; } /** * Je pozícia korektná na ploche b? * @param b plocha (potrebné na získanie veľkosti plochy) * @return boolean, či sú obe súradnice v intervale 0..N-1 */ public boolean isCorrect(Board b) { return ((i >= 0) && (i < b.getN()) && (j >= 0) && (j < b.getN())); } /** * Vráti reťazec reprezentujúci pozíciu (vhodný na výpis) * @return String s vypísaním pozície */ public String toString() { return "[" + i + "," + j + "]"; } } /** * Trieda reprezentujúca nejakú výmenu dvoch susedných kameňov */ class Exchange { /** * Výpis výmeny do reťazca vo formáte podľa zadania, používa sa pri výpise nájdeného riešenia. * @return String obsahujúci vypísanú výmenu. */ public String toString() { return " a "; // PREPROGRAMUJTE } } /** * Trieda reprezentujúca aktuálnu situáciu hernej plochy. */ class Board { /** * Veľkosť plochy. */ private int N; /** * Znaky tvoriace plochu (ako na vstupe, t.j. bodka je voľné políčko). */ private char[][] a; /** * Konštruktor, ktorý vytvorí prázdnu plochu veľkosti _N * @param _N veľkosť plochy */ public Board(int _N) { N = _N; a = new char[N][N]; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { a[i][j] = '.'; } } } /** * Konštruktor, ktorý plochu načíta zo súboru. * @param filename názov súboru * @throws FileNotFoundException ak súbor neexistuje */ public Board(String filename) throws FileNotFoundException { Scanner in = new Scanner(new File(filename)); //otvorime subor N = in.nextInt(); // nacitame velkost plochy a = new char[N][N]; // alokujeme maticu for (int i = 0; i < N; i++) { String line = in.next(); // nacitame riadok for (int j = 0; j < N; j++) { // ulozime ho po znakoch do matice a[i][j] = line.charAt(j); } } } /** * Metóda, ktorá vytvorí kópiu plochy, ktorú je potom možné nezávisle meniť. * @return Kópia tejto plochy. */ public Board clone() { Board b = new Board(N); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { b.a[i][j] = a[i][j]; } } return b; } /** * Vráti veľkosť plochy (počet riadkov a stĺpcov) * @return Veľkosť plochy. */ public int getN() { return N; } /** * Zistí, či je v riadku x a stĺpci y kameň? */ private boolean isGem(int x, int y) { return (a[x][y] != '.'); } /** * Zistí, či je na pozícii p kameň. */ public boolean isGem(Position p) { return isGem(p.getI(), p.getJ()); } /** * Pomocná metóda, ktorá spočíta počet kameňov na ploche. */ private int getGemNumber() { int result = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { if (isGem(i, j)) { result++; } } } return result; } /** * Metóda, ktorá vráti zoznam pozícií kameňov na ploche * @return Pole pozícií kameňov. */ public Position[] getGemPositions() { int num = getGemNumber(); // zistime pocet kamenov a alokujeme pole Position[] result = new Position[num]; int pos = 0; // prejdeme maticou a ukladame pozicie for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { if (isGem(i, j)) { result[pos] = new Position(i, j); pos++; // posunieme sa na dalsie policku v poli result } } } return result; } /** * Výpis plochy na konzolu, môže sa hodiť pri ladení. */ public void printBoard() { for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { System.out.print(a[i][j]); } System.out.println(); } } /** * Zistí, či je pozícia p súčasťou nejakej reťaze. */ public boolean isPartOfGemLine(Position p) { int i = p.getI(); int j = p.getJ(); char col = a[i][j]; if (col == '.') { return false; } int kplus = 0; int kminus = 0; while ((i + (kplus + 1) < N) && (a[i + (kplus + 1)][j] == col) && (kplus + kminus + 1 < 3)) { kplus++; } while ((i - (kminus + 1) >= 0) && (a[i - (kminus + 1)][j] == col) && (kplus + kminus + 1 < 3)) { kminus++; } if (kplus + kminus + 1 >= 3) { return true; } kplus = 0; kminus = 0; while ((j + (kplus + 1) < N) && (a[i][j + (kplus + 1)] == col) && (kplus + kminus + 1 < 3)) { kplus++; } while ((j - (kminus + 1) >= 0) && (a[i][j - (kminus + 1)] == col) && (kplus + kminus + 1 < 3)) { kminus++; } if (kplus + kminus + 1 >= 3) { return true; } return false; } } /** * Trieda riešiaca hru Diamonds. Obsahuje statické metódy main a solve. */ public class Diamonds { /** * Statická metóda, ktorá dostane hraciu plochu a rekurzívne zistí, * či je možné ju vyriešiť. Ak áno, vráti jedno možné riešenie. * Ak nie, vráti null. * @param board Hracia plocha * @return Pole výmien kameňov, ktorá je treba vykonať na vyriešenie úlohy * v poradí, ako ich treba vykonať, alebo null, ak úloha nemá riešenie. */ static Exchange[] solve(Board board) { Position[] gems = board.getGemPositions(); // najdime na ploche kamene if (gems.length == 0) { // ak ziadne kamene uz nie su, riesenim je pole dlzky 0 return new Exchange[0]; } // VAS ALGORITMUS TU return null; } /** * Hlavná metóda programu, ktorá načíta vstup zo súboru, * spustí prehľadávanie a vypíše výstup. */ public static void main(String[] args) throws FileNotFoundException { Board b = new Board("vstup.txt"); // nacitame hraciu plochu Exchange[] exchanges = solve(b); // vyriesime hraciu plochu if (exchanges == null) { // nema riesenie System.out.println("Nema riesenie."); } else { // vypiseme zoznam tahov for (Exchange x : exchanges) { System.out.println("Vymena " + x.toString()); } } } }
Prednáška 28
Objekty a dedenie
- Trieda Pes
class Pes { String meno; Pes(){this("Dunco");} Pes(String meno_){meno=meno_;} void ozviSa() { System.out.println("Haf!"); } void strazDom() {} }
- Ak teraz chceme vytvoriť triedu Mačka vyzerá to jednoducho - veľa vecí má so psom podobných - spravíme Copy+Paste a vhodne upravíme
class Macka { String meno; Macka(){this("Mica");} Macka(String meno_){meno=meno_;} void ozviSa() { System.out.println("Mnau!"); } //macka zvacsa dom nestrazi }
- Problém nastane, keď si vytvoríme viacero takýchto "podobných" tried a budeme chcieť meniť niečo, čo majú spoločné. Aby sme sa tomuto problému vyhli tak pre spoločné premenné a metódy, ktoré majú všetky zvieratá si vytvoríme triedu Zviera.
class Zviera { String meno; Zviera(){} Zviera(String meno_){meno=meno_;} void ozviSa() { System.out.println("?"); } // zviera vlastne nevie že ako sa ozvať, ale vie, že to vie urobiť }
- Teraz sú Pes a Mačka potomkami triedy Zviera
- Čo sa v nich zmení?
Kľúčové slovo extends
- V jave sa dedí iba od jednej triedy (na rozdiel od napr. C++)
- Dedenie je však možné viacúrovňovo
class Pes extends Zviera { } class Civava extends Pes { }
Premenné v potomkoch a predchodcoch
- Potomok obsahuje všetky premenné predchodcu
- Vie pristupovať k premenným predchodcu okrem privátnych
class Zviera { protected String meno; } class Pes extends Zviera { // obsahuje premennú meno a vie k nej pristupovať String vratMeno() { return meno; } }
Konštruktor
- na začiatku konštruktora (pokiaľ to nie je napísané) sa zavolá super(), ktorý zavolá konštruktor nadradenej triedy
- podobne ako sme mohli v konštruktoroch používať this(..) môžeme aj my použiť super(..)
class Zviera { // ma iba defaultný konštruktor } class Pes extends Zviera { Pes(String meno_){ //super(); - automaticky urobi prekladac meno=meno_; } }
Metódy
- Polymorfizmus v programovaní (hlavne pri OOP) je schopnosť funkcií chovať sa rôzne
- S polymorfizmom sme sa už stretli pri jednoduchých triedach, kde funkcia mohla v závislosti od parameterov robiť rôzne veci (tzv. overloading)
- Pri dedení navyše sa môže funkcia chovať rôzne v rôznych triedach - aj napriek tomu, že sa volá rovnako a je dedená z jednej konkrétnej funkcie u nadradenej triedy (overriding)
- Ako to funguje keď zavolám funkciu f() v objekte typu T ( t.j. zavolám o.f() )
- V prvom rade zistím, či T vie vykonať f() ak áno, vykonám ju
- Ak ju T vykonať nevie zistíme, či T nemá nadtriedu a táto vie vykonať f().
- Takto pokračujeme...
- No dobre, v skutočnosti je tam tabuľka, ktorá rovno povie, ktorá sa vykoná
- Funkcie potomka prepíšu (nahradia) funkcie predchodcu
class Zviera { public void OzviSa() { System.out.println("He?"); } } class Pes extends Zviera { @Override public void OzviSa() { System.out.println("Haf!"); } }
Príklad
class Zviera { protected String meno; public void OzviSa() { System.out.println("He?"); } public void Spi() { System.out.println("Chrrr!"); } } class Pes extends Zviera { Pes(String meno_){ meno=meno_; } @Override public void OzviSa() { System.out.println("Haf!"); } } class Civava extends Pes { Civava(String meno_){ super("Maly/a "+meno_); } @Override public void OzviSa() { System.out.println("Hif!"); } } class Macka extends Zviera { Macka(String meno_) { meno=meno_; } @Override public void OzviSa() { System.out.println("Mnau!"); } @Override public void Spi() { /*macka nechrape!*/ } } ... static void test() { Zviera x=new Civava("Dunco"); Zviera y=new Macka("Mica"); Pes alik=new Pes("Alik"); Pes rex=new Pes("Rex"); if (x instanceof Pes) // x je Civava a teda je to aj Pes { Pes dunco=(Pes)x; // budeme sa na Zviera x pozerat ako na psa System.out.println("x je pes"); dunco.ozviSa() // "Hif!" aj ked je pes je to stale Civava } //Pes mnau=(Pes)y; - spadne za behu if (y instanceof Pes) // y nie je Pes (ani potomok psa) { Pes cica=(Pes)y; // toto sa nikdy nestane } alik.OzviSa(); // "Haf!" rex.OzviSa(); // "Haf!" x.OzviSa(); // "Hif!" (aj keď je to zviera nepovie "He?" ani "Haf" ako pes) y.OzviSa(); // "Mnau!" (aj keď je to zviera nepovie "He?") x.Spi(); // "Chrrr!" y.Spi(); // macky nechrapu! Zviera[] zvierata=new Zviera[2]; zvierata[0]=alik; zvierata[1]=y; for(Zviera zviera:zvierata) { zviera.OzviSa(); } }
Modifikátory tried, premenných a metód
- niektoré modifikátory súvisia s dedením
Abstract, final
abstract class Zviera { protected String meno; public abstract void OzviSa(); // nemusím písať zbytočne telo a stráži mi to, aby som nezabudla u potomka OzviSa() public abstract void Spi(); } class Pes extends Zviera { @Override public void OzviSa() { System.out.println("Haf!"); } @Override public final void Spi() { System.out.println("Chrrrrrr!"); } } final class Civava extends Pes { @Override public void OzviSa() { System.out.println("Hif!"); } // uz nemozem urobit Spi() - všetci psi chrápu }
Metódy dedené z Object
- Všetky triedy sú autoamticky potomkami triedy Object
- Object obsahuje virtuálne metódy (napr. toString()), ktoré môžeme preťažiť.
class Blabol /* extends Object */ { public void print() { System.out.println("Blablabla"); } @Override public String toString() { return "Blebleble"; } } ... Blabol b=new Blabol(); System.out.println(b.toString());
Aritmetický strom s využitím dedenia
abstract class Node { public abstract int Compute(); public abstract String Print(); } abstract class NularyNode extends Node { } abstract class UnaryNode extends Node { Node child; UnaryNode(Node child_){ child=child_; } } abstract class BinaryNode extends Node { Node left; Node right; BinaryNode(Node left_, Node right_) { left=left_; right=right_; } } class Constant extends NularyNode { int value; Constant(int value_) { value=value_;} @Override public int Compute() { return value;} @Override public String Print() { return new Integer(value).toString();} } class UnaryMinus extends UnaryNode { UnaryMinus(Node child_){ super(child_); } @Override public int Compute() { return -child.Compute();} @Override public String Print() { return "(-"+child.Print()+")";} } class Plus extends BinaryNode { Plus(Node left_, Node right_) { super(left_,right_); } @Override public int Compute() { return left.Compute()+right.Compute();} @Override public String Print() { return "("+left.Print()+"+"+right.Print()+")";} } ... public static void main(String[] args) { Node expr=new Plus(new UnaryMinus(new Constant(2)),new Constant(3)); System.out.println(expr.Print()); System.out.println(expr.Compute()); }
Interface
- Interface je podobné ako abstraktný predok, ale
- neobsahuje premenné a implementácie metód
- trieda môže implementovať viacero rozhraní - pripomína "viacnásobná dedičnosť"
package javaapplication5; interface IStack { void push(int data_); int pop(); } interface IPrintable { void print(); } class Stack implements IStack, IPrintable { static class Node { public int data; public Node next; public Node() {} public Node(int data_) {data=data_; } public Node(int data_, Node next_) {data=data_; next=next_;} } private Node front; public Stack() {} @Override public void push(int data_){ Node p=new Node(data_); if (front==null) front=p; else { p.next=front; front=p; } } @Override public int pop(){ if (front==null) return -1; int res=front.data; front=front.next; return res; } @Override public void print() { Node p=this.front; while (p!=null) { System.out.print(p.data+" "); p=p.next; } System.out.println(); } } class List implements IStack, IPrintable { static class Node2 { public int data; Node2(int data_){data=data_;} } private Node2[] a; private int n; List(){a=new Node2[100]; n=0;} List(int n){a=new Node2[n]; n=0;} @Override public void push(int data_){ a[n]=new Node2(data_); n++; } @Override public int pop(){ if (n<1) return -1; return a[n--].data; } @Override public void print(){ for (int i=0; i<n; i++) System.out.print(a[i].data+" "); System.out.println(); } } class Blabol implements IPrintable { @Override public void print() { System.out.println("Blablabla"); } } public class JavaApplication5 { static void fillStack(IStack stack) { stack.push(10); stack.push(20); } static void printTwice(IPrintable what) { what.print(); what.print(); } public static void main(String[] args) { Stack s1=new Stack(); IStack s2=new List(); Blabol b=new Blabol(); fillStack(s1); fillStack(s2); printTwice(s1); //printTwice(s2); - s2 je IStack a nevie, že sa vie vypísať printTwice((List)s2); printTwice(b); } }
Cvičenia 16
- Nižšie je uvedený kód abstraktnej triedy Progression, ktorá predstavuje celočíslenú postupnosť a dokáže jeden za druhým generovať jej členov, pričom si v premennej index pamätá index posledne vygenerovaného členu. Jediná public metóda tejto triedy je print, ktorá vypíše zadaných počet prvkov na obrazovku.
- Všimnite si, že premenná index je pre podtriedy tejto triedy "read-only", lebo jej hodnotu môžu zistiť pomocou metódy getIndex, ale nemôžu ju meniť.
- Napíšte triedu ArithmeticProgression, ktorá bude podtriedou Progression a bude reprezentovať aritmetickú postupnosť, ktorá je v konštruktore zadaná nultým prvkom a rozdielom medzi dvoma nasledujúcmi prvkami.
- Ak do main dáme Progression ap = new ArithmeticProgression(1, 3); ap.print(10);, program by mal vypísať 1 4 7 10 13 16 19 22 25 28
- Stačí implementovať konštruktor a currentValue()
- Napíšte triedu FibonacciProgression, ktorá bude reprezentovať Fibonacciho postupnosť, ktorá má pre účely tohto cvičenia nultý prvok 1, prvý prvok 1 a každý ďalší prvok je súčtom predchádzajúcich dvoch. Prvý prvok sa dá tiež reprezentovať ako súčet nultého a fiktívneho mínus prvého s hodnotou nula.
- Ak do main dáme Progression fp = new FibonacciProgression(); fp.print(10);, program by mal vypísať 1 1 2 3 5 8 13 21 34 55.
- Implementujte konštruktor, currentValue, firstvalue, nextValue
- Nižšie nájdete aj implementáciu triedy ProgressionSum, ktorá reprezentuje postupnosť, ktorá vznikla ako súčet dvoch postupností, ktoré dostane na vstupe.
- Ak do main dáme Progression ps = new ProgressionSum(fp, fp); ps.print(10);, chceli by sme dostať dvojnásobok Fibonacciho postupnosti, teda 2 2 4 6 10 16 26 42 68 110. Nie je to však tak. Prečo? Ako prebieha volanie nextValue() pre premennú triedy ProgressionSum? Aké všetky metódy sa volajú a v akom poradí?
- Zmeňte riadok s vytvorením postupnosti ps tak, aby program mal požadované správanie
package prog; /** Trieda reprezentujuca celociselnu postupnost. */ public abstract class Progression { /** Aktualny index prvku postupnosti. */ private int index; /** Konstruktor */ protected Progression() { index = 0; } /** Vrati aktualny index prvku postupnosti */ protected int getIndex() { return index; } /** Vrati hodnotu postupnosti pre aktualny index */ protected abstract int currentValue(); /** Restartuje index na 0 a vrati nulty prvok. */ protected int firstValue() { index = 0; return currentValue(); } /** Zvysi index o 1 a vrati aktualny prvok. */ protected int nextValue() { index++; return currentValue(); } /** Vypise prvych n prvkov postupnosti. */ public void print(int n) { System.out.print(firstValue()); for (int i = 1; i < n; i++) { System.out.print(" " + nextValue()); } System.out.println(); } public static void main(String[] args) { } }
class ProgressionSum extends Progression { Progression p1, p2; ProgressionSum(Progression p1_, Progression p2_) { p1 = p1_; p2 = p2_; } @Override protected int currentValue() { return p1.currentValue() + p2.currentValue(); } @Override protected int nextValue() { p1.nextValue(); p2.nextValue(); return super.nextValue(); } @Override protected int firstValue() { p1.firstValue(); p2.firstValue(); return super.firstValue(); } }
Prednáška 29
Výnimky
- Počas behu programu môže dôjsť k rôznym chybám a neobvyklým situáciám, napr.
- neexistujúci súbor, zlý formát súboru
- málo pamäte pri alokovaní polí, objektov
- adresovanie mimo hraníc poľa, delenie nulou,...
- Doteraz sme v našich cvičných programoch ignorovali chyby
- Programy určené pre užívateľov a kritické programy, ktorých zlyhanie by mohlo spôsobiť škody, by sa s takýmito situáciami mali vedieť rozumne vyrovnať
- Ošetrovanie chýb bez požitia výnimiek
- Do návratového kódu funkcie musíme okrem samotnej hodnoty zakomponovať aj ohlasovanie chýb
- Po každom príkaze, ktorý mohol spôsobiť chybu, musíme existenciu chyby otestovať a vyrovnať sa s tým
- Vedie to k neprehľadným programom
Malý príklad s načítaním poľa
Príklad: pseudokód funkcie, ktorá načíta zo súboru číslo n, naalokuje pole a načíta do poľa n čísel:
funkcia readArray { otvor subor vstup.txt if (nepodarilo sa otvorit) { return chybovy kod } nacitaj cislo n if (nepodarilo sa nacitat n) { zatvor subor return chybovy kod } alokuj pole a velkosti n if (nepodarilo sa alokovat pole) { zatvor subor return chybovy kod } for (int i=0; i<n; i++) { nacitaj cislo a uloz do a[i] if (nepodarilo sa nacitat) { zatvor subor odalokuj pole return chybovy kod } } zatvor subor return naalokovane pole, beh bez chyby
- Premiešané príkazy, ktoré niečo robia a ktoré ošetrujú chyby
- Ľahko môžeme zabudnúť odalokovať pamäť alebo zavrieť súbor
- Volajúca funkcia musí analyzovať chybový kód, môže potrebovať rozlišovať napr. problémy so súborom a s pamäťou
- Chyba môže nastať aj pri zatváraní súboru
Jednoduché použite výnimiek v Jave
- Prepíšme náš predchádzajúci príklad s výnimkami
static int[] readArray(String filename) { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } s.close(); return a; } catch (Exception e) { if (s != null) { s.close(); } e.printStackTrace(); return null; } }
- Využívame konštrukty try a catch.
- Do try bloku dáme príkazy, z ktorých niektorý môže zlyhať.
- Ak niektorý zlyhá a vyhodí výnimku, okamžite sa ukončí vykonávanie bloku try a pokračuje sa blokom catch. V bloku catch túto výnimku spracujeme, v našom prípade len debugovacím výpisom.
- Ak sa podarilo čísla načítať do poľa, metóda vráti pole, inak vráti null
Ako všelijako môže zlyhať
Rôzne príklady, ako môže táto metóda zlyhať:
- Príkazu na inicializáciu Scannera pošleme meno neexistujúceho súboru:
java.io.FileNotFoundException: vstup.txt (No such file or directory) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:137) at java.util.Scanner.<init>(Scanner.java:653) at prog.Prog.readArray(Prog.java:17) at prog.Prog.main(Prog.java:10)
- V súbore sú nečíselné údaje:
java.util.InputMismatchException at java.util.Scanner.throwFor(Scanner.java:857) at java.util.Scanner.next(Scanner.java:1478) at java.util.Scanner.nextInt(Scanner.java:2108) at java.util.Scanner.nextInt(Scanner.java:2067) at prog.Prog.readArray(Prog.java:18) at prog.Prog.main(Prog.java:10)
- Ak nie je dosť pamäte na pole a (toto ani nie je Exception, ale Error, takže náš catch to nezachytil, pozri ďalej)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at prog.Prog.readArray(Prog.java:19) at prog.Prog.main(Prog.java:10)
- Ak je číslo n v súbore záporné
java.lang.NegativeArraySizeException at prog.Prog.readArray(Prog.java:19) at prog.Prog.main(Prog.java:10)
- Súbor končí skôr ako sa načíta n čísel
java.util.NoSuchElementException at java.util.Scanner.throwFor(Scanner.java:855) at java.util.Scanner.next(Scanner.java:1478) at java.util.Scanner.nextInt(Scanner.java:2108) at java.util.Scanner.nextInt(Scanner.java:2067) at prog.Prog.readArray(Prog.java:21) at prog.Prog.main(Prog.java:10)
- A zrejme by sa dali vyrobiť aj ďalšie prípady (napr filename==null)
- V dokumententácii sa o každej metóde dočítame, aké výnimky produkuje za akých okolností
Rozpoznávanie typov výnimiek
- Možno by náš program mal rôzne reagovať na rôzne typy chýb, napr.:
- Chýbajúci súbor: vypýtať si od užívateľa nové meno súboru
- Zlý formát súboru: ukázať užívateľovi, kde nastala chyba, požiadať ho, aby ju opravil alebo zadal nové meno súboru
- Nedostatok pamäte: program vypíše, že operáciu nie je možné uskutočniť vzhľadom na málo pamäte
- Toto vieme spraviť, lebo výnimky patria do rôznych tried dedených z triedy Exception (prípadne z vyššej triedy Throwable)
- K jednému príkazu try môžeme mať viacero príkazov catch pre rôzne triedy výnimiek, každý chytá tú triedu a jej podtriedy
- Pri danej výnimke sa použije najvrchnejší catch, ktorý sa na ňu hodí
- Po blokoch try a catch môže nasledovať blok finally, ktorý sa vykoná vždy, bez ohľadu na to, či nastala výnimka a či sa nám ju podarilo odchytiť nejakým príkazom catch
- V tomto bloku môžeme napr. pozatvárať otvorené súbory a pod.
Jednoduchý príklad, ktorý vypíše rôzne hlášky pre rôzne typy chýb:
static int[] readArray(String filename) { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } return a; } catch (FileNotFoundException e) { System.err.println("Subor nebol najdeny"); return null; } catch(java.util.NoSuchElementException e) { System.err.println("Zly format suboru"); return null; } catch(OutOfMemoryError e) { System.err.println("Nedostatok pamate"); return null; } catch(Throwable e) { System.err.println("Neocakavana chyba pocas behu programu"); return null; } finally { if (s != null) { s.close(); } } }
- catch pre java.util.NoSuchElementException chytí aj InputMismatchException, ktorá je jej podtriedou, takže zahŕňa prípady keď súbor nečakane končí, aj keď v ňom nie sú číslené dáta
- do tejto kategórie by sme chceli zaradiť aj prípad, kedy je n záporné, ale ten skončí na všeobecnej Throwable
- to vyriešime tým, že hodíme vlastnú výnimku
Hádzanie výnimiek, vlastné triedy výnimiek
- Výnimku vyhodíme príkazom throw, pričom musíme vytvoriť objekt nejakej vhodnej triedy, ktorá podtriedou Throwable
- V našom príklade pre záporné n môžeme vyhodiť objekt triedy java.util.NoSuchElementException, ktorý sa spracuje rovnako ako iné chyby s formátom súboru
int n = s.nextInt(); if(n<0) { throw new java.util.NoSuchElementException(); }
- Nie je to však elegantné riešenie, lebo táto trieda reprezentuje iný typ udalosti
- Môžeme si vytvoriť aj vlastnú triedu, ktorá v premenných môže mať uložené podrobnejšie informácie o chybe, ktorá nastala.
- Väčšinou to bude podtrieda triedy Exception
static class WrongFormatException extends Exception { private String filename; public WrongFormatException(String filename_) { filename = filename_; } @Override public String getMessage() { return "Zly format suboru " + filename; } }
Propagácia a zreťazenie výnimiek
- Ak vznikne výnimka v príkaze, ktorý nie je vo vnútri try-catch bloku, alebo ak jej typ nie je zachytený žiadnym catch príkazom, hľadá sa ďalší try-catch blok, napr. vo volajúcej metóde
- Ak výnimku nikto nechytí, program skončí s chybovým výpisom zásobníka
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at prog.Prog.readArray(Prog.java:19) at prog.Prog.main(Prog.java:10)
- Pri spracovaní výnimky v bloku catch je možné hodiť novú výnimku (trebárs vhodnejšieho typu)
- Metóda musí deklarovať všetky výnimky, ktoré hádže, alebo ktoré v nej môžu vzniknúť a ich nechytá
- Neplatí pre výnimky triedy RuntimeException a jej podtried a pre Throwable, ktoré nie sú výnimka
Nasledujúci program si pýta meno súboru, až kým nenájde súbor, ktorý vie načítať
- V metóde readArray spracuje chyby týkajúce sa formátu súboru a hodí novú výnimku typu WrongFormatException.
- V metóde main spracuje WrongFormatException a FileNotFoundException tak, že sa znovu pýta meno súboru.
- Iné nečakané udalosti, napr. málo pamäte, koniec vstupu od užívateľa a pod. spôsobia ukončenie programu s chybovou hláškou.
public static void main(String[] args) { boolean fileRead = false; Scanner s = new Scanner(System.in); int[] a = null; while (!fileRead) { try { System.out.println("Zadaj meno suboru: "); String filename = s.next(); a = readArray(filename); fileRead = true; System.out.println("Dlzka pola je " + a.length); } catch (WrongFormatException e) { System.out.println(e.getMessage()); } catch (FileNotFoundException e) { System.out.println("Subor nebol najdeny."); } catch(Throwable e) { System.out.println("Neocakavana chyba."); System.exit(1); } } } static int[] readArray(String filename) throws WrongFormatException, FileNotFoundException { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); if (n < 0) { throw new WrongFormatException(filename); } a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } return a; } catch (java.util.NoSuchElementException e) { throw new WrongFormatException(filename); } finally { if (s != null) { s.close(); } } }
Zhrnutie
- Keď vznikne neočakávaná udalosť, môžeme ju signalizovať vyhodením výnimky pomocou príkazu throw.
- Výnimka je objekt triedy, ktorá je podtriedou Throwable
- Pri ošetrovaní sa nájde a vykoná najbližší vyhovujúci try ... catch blok obkolesujúci príkaz throw, v tej istej alebo niektorej volajúcej metóde, ďalej sa pokračuje za týmto blokom
- Blok finally sa vykoná vždy, keď je aj keď nie je výnimka a aj ak sa výnimku nepodarilo chytiť. Slúži na zatváranie súborov a iné upratovacie práce.
- Niektoré typy neodchytených výnimiek treba deklarovať v hlavičke funkcie.
Ďalšie informácie
- Dokumentácia k triedam Throwable, Exception
- Kapitola z tutoriálu
Generické programovanie
- V minulom semestri sme videli rôzne abstraktné dátové typy a dátové štruktúry, napr. zásobník, rad, slovník, spájaný zoznam,...
- V každej sme museli zadefinovať, akého typu dáta bude obsahovať
- Ak teda potrebujeme zásobník intov aj zásobník reťazcov, museli sme všetky funkcie písať dvakrát, čo prináša problémy
Zásobník dát typu Object
- Dedenie nám prináša jedno riešenie tohto problému - zadefinovať typ dát ako Object a nakoľko všetky triedy sú podtriedami tejto triedy, môžeme ich v zásobníku skladovať
class Node { private Object data; private Node next; public Node(Object data_, Node next_) { data = data_; next = next_; } public Object getData() { return data; } public Node getNext() { return next; } } class Stack { private Node front; public void push(Object data) { Node p = new Node(data, front); front = p; } public Object pop() { Object res = front.getData(); front = front.getNext(); return res; } }
Teraz môžeme do zásobníka dávať rôzne veci:
Stack s = new Stack(); s.push(null); s.push("Hello world!"); // String je potomok Object s.push(new int[4]); // pole sa tiez vie tvarit ako objekt int x = 4; s.push(x); // kompilator vytvori objekt typu Integer
Ale pozor, keď vyberáme zo zásobníka, majú typ Object, musíme ich teda pretypovať:
int y = (Integer)s.pop(); // ok int z = (Integer)s.pop(); // java.lang.ClassCastException
Pre pretypovaní teda môže dôjsť k chybe počas behu programu, radšej sme keď chybu objaví kompilátor.
Zásobník ako generická trieda
- Zadefinujeme parametrický typ class Stack <T>, kde T je parameter reprezentujúci typ objektov, ktoré do zásobníka budeme dávať.
- V definícii triedy namiesto konkrétneho typu (napr. Object), použijeme parameter T
- Keď vytvárame nový zásobník, špecifikujeme typ T: Stack<Integer> s = new Stack<Integer>();
- Potom do neho môžeme vkladať objekty triedy Integer a jej podtried
class Node <T> { private T data; private Node <T> next; public Node(T data_, Node<T> next_) { data = data_; next = next_; } public T getData() { return data; } public Node <T> getNext() { return next; } } class Stack <T> { private Node<T> front; public void push(T data) { Node<T> p = new Node<T>(data, front); front = p; } public T pop() { T res = front.getData(); front = front.getNext(); return res; } }
Použitie zásobníka:
Stack<Integer> s = new Stack<Integer>(); s.push(new Integer(4)); s.push(5); Integer y = s.pop(); int z = s.pop();
V tom istom programe môžeme vytvoriť zásobníky veľa rôznych typov.
Generické metódy
Aj jednotlivé metódy môžu mať typový parameter, ktorý sa píše pred návratový typ.
Statická metóda, ktorá dostane zásobník s prvkami typu T a vyprázdni ho.
static <T> void emptyStack(Stack<T> s) { while(!s.isEmpty()) { s.pop(); } }
Stack<String> s = new Stack<String>(); s.push("abc"); Prog.<String>emptyStack(s); // alebo len emptyStack(s);
Statická metóda, ktorá dostane pole s prvkami typu E a naplní ho referenciami na prvok e.
static <E> void fillArray(E[] a, E e) { for(int i=0; i<a.length; i++) { a[i] = e; } }
Integer[] a = new Integer[3]; fillArray(a, 4); int[] b = new int[3]; //fillArray(b, 4); // E musí byť objekt, nie primitívny typ
Generické triedenie, interface Comparable
Ukazovali sme si aj algoritmy na triedenie, aj tie pracovali s poľom konkrétneho typu.
- Triediacu funkciu môžeme spraviť generickú, potrebujeme však prvky porovnávať
- Operátory <, <= atď pracujú len s primitívnymi typmi
- Použijeme teda špeciálnu metódu compareTo špecifikovanú v interface Comparable
- x.compareTo(y) vráti zápornú hodnotu, ak x<y, nulu ak x=y a kladnú hodnotu ak x>y
- potrebujeme, aby prvky poľa boli z triedy, ktorá implementuje tento interface, čo zapíšeme ako <E extends Comparable>
Jednoduché generické triedenie vkladaním:
static <E extends Comparable> void sort(E[] a) { for (int i = 1; i < a.length; i++) { E prvok = a[i]; int kam = i; while (kam > 0 && prvok.compareTo(a[kam - 1]) < 0) { a[kam] = a[kam - 1]; kam--; } a[kam] = prvok; } } public static void main(String[] args) { Integer[] a = {3, 1, 2}; sort(a); for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } }
Cvičenia 17
Jednoduché výnimky
Nižšie nájdete program z prednášky, ktorý načítava zo súboru číslo n a n čísel do poľa, pričom neexistenciu súboru a jeho zlý formát rieši výnimkami. Od užívateľa opakovanie vypýta meno súboru, až kým sa mu nepodarí súbor prečítať.
- Po načítaní čísel do poľa v metóde readArray overte metódou hasNext() triedy Scanner, že sa v súbore už nenachádzajú ďalšie čísla alebo iné nebiele znaky. Ak sa nachádzajú, hoďte tiež WrongFormatException.
- Zmeňte program tak, aby WrongFormatException v konštruktore dostala aj podrobnejší popis chyby formátu, ktorá bude napríklad "Nepodarilo sa načítať počet prvkov n", alebo "Nepodarilo sa načítať prvok i", kde namiesto znaku i dosadíte príslušné poradové číslo prvku, kde nastala chyba. V metóde getMessage potom túto podrobnejšiu správu vráťte.
- Návod: premennú i v metóde readArray zadefinujte už pred príkazom try a inicializujte na -1. V časti catch potom podľa aktuálnej hodnoty i viete zistiť, či sa for cyklus vôbec nezačal alebo na ktorom prvku zhavaroval.
Generická trieda Matrix
Naprogramujte generickú triedu Matrix, ktorá reprezentuje obdĺžnikovú maticu prvkov nejakého neznámeho typu E.
- Prvky matice si v tejto triede uložte do premennej typu ArrayList<ArrayList<E>>, potrebujete si ešte udržiavať aj aktuálny počet riadkov a stĺcov.
- Napíšte konštruktor, ktorý vytvorí maticu zadaných rozmerov a vyplní ju zadaným prvkom typu E.
- Napíšte metódy, ktoré do matice pridajú nový stĺpec a nový riadok, vyplnený zadaným prvkom typu E.
- Napíšte metódu get, ktorá vráti prvok matice nachádzajúci sa na zadanom mieste
- Napíšte metódu set, ktorá na zadané miesto v matici zapíše zadaný prvok typu E
Výnimky v triede Matrix
- Čo sa stane, ak metódu get triedy Matrix zavoláte so súradnicami mimo rozsah matice?
Prepíšte metódy get a set tak, aby pri zlých súradniciach hádzali výnimku vašej vlastnej triedy MatrixIndexOutOfBoundsException.
- Výnimka tejto triedy by v metóde getMessage mala vrátiť reťazec obsahujúci obidve súradnice, ako aj obidva rozmery matice.
- Skúste dva spôsoby implementácie:
- v prvom odchytávajte vzniknuté výnimky a nahraďte ich svojou výnimkou
- v druhom otestujte vhodnosť indexov hneď na začiatku metódy a v prípade potreby vyhoďte vlastnú výnimku
Ešte Matrix, dedenie
Napíšte generickú triedu InfiniteMatrix, ktorá je podtriedou triedy Matrix a líši sa od nej v tom, že ak metóde get dáme súradnice mimo rozsah matice, vráti hodnotu null (a nevyhadzuje výnimku). Je to ako keby sme mali maticu nekonečnej veľkosti vyplnenú null-mi a v malom obdĺžniku s určitým počtom riadkov a stĺpcov máme nejaké uložené hodnoty, ktoré sa môžu líšiť od null.
Výnimky pre Scanner
Vytvorte triedu IntScanner, ktorá v konštruktore dostane meno súboru a okrem konštruktoru obsahuje public metódy hasNextInt, nextInt a close, ktoré robia zhruba to isté ako v triede Scanner. Avšak ak je v súbore nejaký reťazec, ktorý nie je možné interpretovať ako číslo, metóda nextInt vyhodí výnimku vašej novej triedy, ktorá v metóde getMessage vráti reťazec obsahujúci číslo riadku, na ktorom chyba nastala aj reťazec, ktorý nebolo možné ako číslo interpretovať. Trieda Scanner nám neumožňuje zistiť číslo riadku, preto budeme mať v tejto triede tri premenné:
- aktuálne číslo riadku
- premennú typu BufferedReader, v ktorej na začiatku otvoríme zadaný súbor a vždy keď treba, načítame z neho riadok, zvýšime počítadlo a vytvoríme novú inštanciu triedy Scanner, ktorá bude čítať čísla z toho riadku
- premennú triedy Scanner
Zdrojový kód pre prvý príklad
package prog; import java.io.*; import java.util.Scanner; class Prog { static class WrongFormatException extends Exception { private String filename; public WrongFormatException(String filename_) { filename = filename_; } @Override public String getMessage() { return "Zly format suboru " + filename; } } public static void main(String[] args) { boolean fileRead = false; Scanner s = new Scanner(System.in); int[] a = null; while (!fileRead) { try { System.out.print("Zadaj meno suboru: "); String filename = s.next(); a = readArray(filename); fileRead = true; System.out.println("Dlzka pola je " + a.length); } catch (WrongFormatException e) { System.out.println(e.getMessage()); } catch (FileNotFoundException e) { System.out.println("Subor nebol najdeny."); } catch (Throwable e) { System.out.println("Neocakavana chyba."); System.exit(1); } } } static int[] readArray(String filename) throws WrongFormatException, FileNotFoundException { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); if (n < 0) { throw new WrongFormatException(filename); } a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } return a; } catch (java.util.NoSuchElementException e) { throw new WrongFormatException(filename); } finally { if (s != null) { s.close(); } } } }
DÚ13
Odovzdávanie DÚ13 max. 20 bodov, termín odovzdania streda 3.4. o 22:00
Cieľom tejto domácej úlohy je vyskúšať si prácu s programom, ktorý má väčší počet rôznych tried a využíva dedenie a polymorfizmus. Dostanete program, ktorý simuluje pohyb robotov v jednoduchom svete a vašou úlohou bude tento svet rozšíriť o ďalšie typy robotov a políčok. Táto simulácia bola inšpirovaná počítačovou hrou Lemmings.
Doporučujeme Vám nasledujúci postup:
- Prečítajte si celé zadanie
- Stiahnite si program a skúste si ho spustiť pre rôzne vstupy, aby ste videli, ako sa správa. Aby sa Vám lepšie robilo, zväčšite si v Netbeans okienko s výstupom z konzoly, alebo si výstup presmerujte do súboru a ten si potom otvorte v editore.
- Pozrite sa, ako je správanie robotov implementované. Pomôžu Vám zdrojové súbory, ale aj JavaDoc dokumentácia.
- Znovu si prečítajte zadanie a pracujte na jednotlivých rozšíreniach. Pokúste sa dokončiť jedno rozšírenie skôr, ako začnete ďalšie.
- Pre každé rozšírenie naprogramujte potrebné triedy a metódy a rozšírenie otestujte na rôznych vstupoch. Nezabudnite ho tiež okomentovať a dbať na dobrý štýl.
Nenechávajte si úlohu na poslednú chvíľu. Ak máte k zadaniu otázky alebo nájdete chybu v programe, kontaktuje čím skôr B.Brejovú.
Súbory
Základná verzia simulácie
Svet pozostáva z matice štvorčekov s n riadkami a m stĺpcami. Každý štvorček je buď prázdny, alebo obsahuje stenu. Jeden prázdny štvorček slúži ako vchod, kde sa objavujú noví roboti. Niektoré prázdne štvorčeky môžu byť označené ako východ, kadiaľ roboti miznú von z nášho sveta. Pre jednoduchosť sú na celom okraji sveta steny, aby nám roboty nevypadávali mimo hracej plochy. V každom kroku hry sa zvolí jeden robot a povolí sa mu spraviť jeden ťah. Počas ťahu sa robot môže posunúť na iné políčko, prípadne meniť prostredie prostriedkami, ktoré má k dispozícii. Dôsledkom toho môže on alebo aj ďalší roboti spadnúť nižšie, umrieť, alebo dosiahnuť východ. Keď sa skončí ťah jedného robota, prichádza na radu ďalší robot. Robot základného typu, ktorý už je implementovaný, sa správa nasledovne:
- Pamätá si svoje natočenie (doľava alebo doprava) a pokúša sa najskôr ísť o jedno políčko týmto smerom. Na začiatku hry je otočený doprava.
- Ak je políčko pred ním obsadené (stenou alebo iným robotom), otočí sa do opačného smeru a v ďalšom ťahu sa teda bude pokúšať ísť opačným smerom (v aktuálnom ťahu už však nič iné nerobí).
- Ak už robot spravil určitý počet ťahov T, zvyšok hry bude stáť na mieste (pod vplyvom externých podmienok môže napr. umrieť alebo začať padať, v svojom ťahu však nerobí ďalšie kroky doľava alebo doprava)
Ďalšie udalosti, ktoré treba zobrať do úvahy:
- Ak sa výsledkom ťahu robot dostal na políčko, pod ktorým je prázdne políčko, začne padať a padá, až kým pod ním nie je neprázdne políčko. Ak padne naraz o viac ako určitý počet políčok, umrie. Rovnako padajú aj roboti, ktorí síce nie sú na ťahu, ale ktorým zmizla pevná zem pod nohami.
- Ak robot dopadne alebo stúpi na iného robota, spodný robot umiera.
- Ak robot dosiahne políčko východ, hoci aj počas pádu, mizne z hry
- Ak robot umrie, tiež mizne z hry. Ak na neho predtým dopadol alebo stúpil iný robot, tento padne na miesto mŕtveho robota. Ide však už o ďalší pád, ktorého výška sa nesčítava s prípadným pádom, ktorým pristál na umretom robotovi.
Každý robot má tiež dané, v ktorom ťahu vkročí do sveta, pričom každý robot musí vchádzať v inom ťahu. Jeden ťah simulácie pozostáva z toho, že najskôr každý žijúci robot vykoná svoj ťah a nakoniec sa pridá robot, ktorý má v tomto ťahu príchod, ak taký je. Takýto robot sa objaví vo vchodovom políčku a prípadne padne nižšie, nevykonáva sa však jeho ťah. Ak je vchodové políčko obsadené, robot sa vôbec vo svete neobjaví a hneď umiera. Žijúci roboti majú pevné poradie, pričom noví roboti sa pridávajú na koniec tohto poradia. V tomto pevnom poradí sa vykonávajú ich ťahy.
Celá simulácia končí, ak nastane jedna z podmienok:
- Žiaden robot nie je nažive na ploche a nepríde už žiaden ďalší robot
- Vykonal sa určitý špecifikovaný počet ťahov
Na konci sa vyhodnotí, koľkým robotom sa podarilo dosiahnuť úspešne východ a koľkí umreli. Celá simulácia sa dá chápať aj ako hlavolam, kde je úlohou pre daný svet navrhnúť príchody robotov tak, aby aspoň jeden, prípadne čo najviac z nich dosiahlo východ.
Implementácia základnej verzie
Aj keď správanie robotov v základnej verzii by sa dalo implementovať aj malým počtom funkcií operujúcich na jednoduchých dátových štruktúrach, v tejto domácej úlohe sme program rozdelili na väčší počet tried a jednoduchých metód. Takto navrhnutý program sa dá lepšie rozširovať o nové typy robotov a políčok a hlavne si na ňom lepšie precvičíte prácu s objektovo-orientovaným programovaním.
- Trieda Robots obsahuje metódu main a ďalšie statické metódy slúžiace na načítanie vstupných údajov.
- Stav sveta je uložený v triede World, ktorá obsahuje maticu štvorčekov a zoznam robotov a vie jednak zavolať ťahy jednotlivých robotov a jednak vypísať stav hry a rôzne štatistiky.
- Trieda Robot reprezentuje robota popísaného vyššie, pričom sa stará o jeho vnútorný stav a určuje, aké akcie chce počas ťahu vykonať. Robot však nevidí prostredie, preto tieto akcie, ako aj prípadné pády a podobne sú vyhodnocované samotným prostredím.
- Trieda Square a jej podtriedy predstavujú jednotlivé políčka sveta. Vyhodnocujú možnosť vykonania akcií robota a vykonávajú tieto akcie ako aj ich dôsledky. Základná trieda Square reprezentuje políčko typu stena, čo je najjednoduchší typ políčka, ktorý nič nevie robiť a nikdy sa nemení. Okrem toho máme triedy EmptySquare pre prázdne políčko (ktoré prípadne môže obsahovať robota), EntrySquare pre políčko so vstupom a ExitSquare pre políčko s výstupom.
- Interface RobotHolder reprezentuje typ políčka, na ktorom môže stáť robot. Trieda EmptySquare implementuje tento interface a všetky ostatné triedy pre políčka, na ktorých môže byť robot, by mali dediť z EmptySquare. Robot zo sveta vidí iba referenciu na jeden objekt typu RobotHolder a teda by mal so svetom komunikovať výlučne volaním metód zašpecifikovaných v tomto rozhraní.
- Jednoduchá trieda RobotException slúži na ohlasovanie výnimiek, ktoré nastali kvôli chybám v implementácii.
Správanie programu je ovplyvnené aj dvoma konštantami: Robots.MAXMOVES=1000 udáva maximálny počet ťahov hry a Robot.MAXHEIGHT=3 udáva maximálnu výšku, z ktorej môže robot padnúť a nezabiť sa. V odovzdanom program prosím nechajte týmto konštantám pôvodné hodnoty, váš program by mal však fungovať aj pre iné hodnoty týchto konštánt.
Samotný program načítava dva vstupné súbory. V súbore world.txt je hracia plocha, pričom každé políčko je dané jedným znakom. W znamená stenu (wall), bodka je voľné políčko, # je vstupné políčko (vyzerá ako dvere) a * je východ. Tu je príklad súboru:
WWWWWWWWW W.#.....W WWWWWWW.W W.*.....W WWWWWWWWW
V súbore robots.txt je zoznam robotov, každý na jednom riadku. Prvé číslo v riadku je ťah, v ktorom sa má robot objaviť vo vchode. Každý robot musí prísť v inom čase. Potom nasleduje písmeno (alebo slovo) označujúce typ robota, v našom prípade S, ako stopping robot. Potom nasleduje parameter T (po koľkých ťahoch robot zmení svoje správanie z chodenia na státie), v našom príklade je to 100. Posledné slovo na riadku je meno robota, ktoré je možné aj vynechať - program potom bude automaticky robotov číslovať v poradí ako sú na vstupe. Príklad súboru:
1 S 100 X01 2 S 100 X02
Program priebeh simulácie vypisuje na konzolu, pričom vždy vypíše, ktorý robot je na ťahu a upozorňuje na významné udalosti, ako pády, úmrtia a dosiahnutie východu. Po skončení ťahu jedného robota vypíše v textovom formáte stav celej plochy, pričom ak je na voľnom políčku robot, vypíše sa namiesto bodky tento robot v tvare < alebo > vyjadrujúcom jeho natočenie, prípadne ako !, ak už prekročil T ťahov a zastal.
Na vstupe vyššie najprv obaja roboti postupujú doprava do situácie
WWWWWWWWW W.#..>>.W WWWWWWW.W W.*.....W WWWWWWWWW
Potom prvý robot padne a v ďalšom ťahu sa otáča. Medzitým na neho padne robot X02 a zabije ho. Robot X02 však napokon úspešne dorazí do cieľa.
Ak by sme čas príchodu X02 zmenili z 2 na 3, obaja by úspešne dorazili do cieľa. Ak by sme potom ešte robotovi X01 znížili parameter T zo 100 na 10, zastaví tesne pred dosiahnutím cieľa a zablokuje tak cieľ aj pre X02, ktorý bude chodiť tam a späť v spodnej chodbe, až kým nedosiahne svoj počet ťahov T do zastavenia.
Úloha: tri rozšírenia
Vašou úlohou je do sveta doprogramovať tri rozšírenia popísané nižšie, ktoré pridávajú nové typy robotov a políčok. Vaša implementácia bude pozostávať najmä z pridania nových tried dediacich z existujúcich tried, niekedy však bude potrebné pridať do existujúcich tried nové metódy. Pokiaľ možno nemeňte existujúce metódy, okrem prípadov explicitne popísaných nižšie.
Čiastočné body dostanete aj v prípade, ak sa Vám podarí implemetovať iba niektoré rozšírenia. Všetky rozšírenia, ktoré implementujete, by však mali byť súčasťou jedného funkčného programu, takže ak nejaké rozšírenie nedokončíte, upravte program tak, aby aspoň ostatné rozšírenia správne fungovali.
Štýl a komentáre
- Okrem správnosti ide v tejto úlohe aj o návrh tried, ich metód a premenných a vhodné použitie dedičnosti a polymorfizmu.
- Dbajte o pekný programátorský štýl.
- Všetky triedy a ich metódy popíšte vo formáte JavaDoc. Ak meníte existujúce triedy, pridajte do horného JavaDoc komentáru k celej triede, aké zmeny ste v nej robili.
Odovzdávanie
- Odovzdávajte jeden .zip súbor obsahujúci adresár robots a v ňom všetky zdrojové súbory (*.java) vášho balíčka robots (vrátane tých, ktoré ste vôbec nemenili).
- Dodržiavajte mená tried a metód uvedené v zadaní.
- Vo výstupe na konzolu nemeňte časti, ktoré vypisuje existujúci program. Ak chcete, môžete vždy medzi "Start of move..." (resp. "Adding robot...") a "Current situation:" pridať aj oznamy o výbuchoch, teleportáciách a pod., udržujte však počet oznamov na rozumnej miere.
Hlina a kopajúci robot
Pridajte do hry políčko typu hlina a kopajúceho robota. Hlina (soil) sa správa podobne ako stena, nedá sa do nej vojsť a dá sa na nej stáť. Avšak kopajúci robot vie urobiť akciu kopanie (digging), ktorou vie políčko s hlinou, ktoré je pod ním, premeniť na prázdne políčko, do ktorého vzápätí spadne (ak je pod hlinou prázdne políčko, môže padať aj ďalej). Kopať sa dá iba cez hlinu, nie však cez steny a iné typy políčok. Kopajúci robot prvých T ťahov chodí rovnako ako základný robot. Od ťahu T+1 sa správa nasledovne: Ak sa dá kopať (stojí na políčku s hlinou), vykope toto políčko, inak chodí rovnako, ako by chodil základný robot v prvých T ťahoch. Tento robot teda nikdy neostane dobrovoľne stáť.
V súbore world.txt je hlina označená ako S (soil). V súbore robots.txt je kopajúci robot označený písmenom D (digging). Kým sa nespraví T ťahov, kopajúci robot sa zobrazuje na výstupe ako < alebo >, potom sa zobrazuje ako d.
- Vytvorte triedy SoilSquare a DiggingRobot, ktoré budú podtriedami vhodne zvolených tried a implementujte metódy, ktoré treba preťažiť.
- V triede EmptySquare implementujte metódu actionDigging, ktorá slúži na spracovanie akcie kopajúceho robota (táto metóda je už deklarovaná v interface RobotHolder).
- Do triedy Square pridajte metódu digging, ktorá slúži na samotné vykopanie políčka s hlinou. Políčka, ktoré nie sú hlina, budú na digging vracať false (podobne ako plné políčka vracajú false na receiveRobot). Políčka, ktoré sú hlina, budú na digging vracať true a uskutočnia samotnú zmenu políčka a jej následky (padanie robotov a pod.). Toto rozdielne správanie metódy v rôznych typoch políčok dosiahnete preťažením metódy digging v triede SoilSquare.
- Po vykopaní by sa typ políčka mal zmeniť zo SoilSquare na EmptySquare. To sa dá dosiahnuť tak, že vytvoríme nové políčko typu EmptySquare a zapojíme ho do plochy namiesto starého políčka. Na to však treba pomeniť referencie na toto políčko u jeho susedov aj v matici v triede World (na to ešte svet nemá metódu, treba pridať).
- Treba samozrejme aj do hlavného programu doprogramovať vytváranie nových typov štvorčekov a políčok pri čítaní vstupu (metódy createSquare a createRobot).
Skala a vybuchujúci robot
Pridajte do hry políčko typu skala a vybuchujúceho robota. Skala (rock) sa správa podobne ako stena, ale dá sa zničiť výbuchom. Vybuchujúci robot vie konať akciu výbuch (explosion), ktorý zasiahne políčko s robotom a všetky štyri políčka susediace s ním hranou. Všetci roboti a skaly nachádzajúce sa na týchto políčkach budú zničené. Výbuch nemá vplyv na steny a zeminu. Vybuchujúci robot sa tiež prvých T ťahov správa ako základný robot a v ťahu T+1 vybuchne, pričom samozrejme zahynie.
Celkový výsledok hry môže ovplyvniť aj to, v akom poradí spracovávame udalosti vyvolané výbuchom. Najskôr je zničené samotné políčko s robotom a potom jeho štyri susedné políčka počínajúc políčkom nad ním a postupujúc v smere hodinových ručičiek. Pri zničení obsahu políčka sa najskôr vyrovnáme so všetkými jeho dôsledkami a potom pokračujeme zničením ďalšieho políčka. Napríklad ak máme vybuchujúceho robota x, nad ktorým je skala a nad ňou ďalší robot y, najskôr umrie robot x, potom vybuchne skala nad ním, dôsledkom čoho robot y padne na políčko, kde bol pôvodne x. Ak by roboti umierali pri páde z výšky 2, rovno by aj umrel, inak žije ďalej. Potom vybuchuje políčko napravo a potom políčko dole. Ak políčko dole obsahovalo skalu a robot y ešte žije, začne zase padať. Vysporiadame sa so všetkými dôsledkami jeho pádu a nakoniec riešime výbuch políčka naľavo od pôvodnej pozície robota x.
V súbore world.txt je skala označená ako R (rock). V súbore robots.txt je vybuchujúci robot označený písmenom E (exploding). Kým sa nespraví T ťahov, vybuchujúci robot sa zobrazuje na výstupe ako < alebo >, potom sa zobrazuje ako e.
- Vytvorte triedy RockSquare a ExplodingRobot, ktoré budú podtriedami vhodne zvolených tried a implementujte metódy, ktoré treba preťažiť.
- V triede EmptySquare implementujte metódu actionExploding, ktorá slúži na spracovanie akcie vybuchujúceho robota.
- Do triedy Square pridajte metódu exploding, ktorá slúži na samotné vybuchnutie príslušného políčka. Túto metódu potom preťažte aj v ďalších typoch políčok, ktoré pri výbuchu majú reagovať inak ako stena.
- Po výbuchu skaly treba nahradiť políčko novým políčkom typu EmptySquare, podobne ako pri kopaní hliny.
Teleport
Pridajte do hry dva špeciálne typy políčok, ktoré predstavujú teleport. Teleport môže mať viacero vstupov a vždy práve jeden výstup. Políčko, ktoré je výstupom pre teleport, sa správa ako obyčajné prázdne políčko. Ak robot vôjde do vstupu pre teleport, alebo ak cez neho padá dole, teleport ho zachytí a presunie ho na výstup teleportu, odkiaľ prípadne robot opäť padá nižšie. Ak robot do vstupu teleportu padol, pád sa tým preruší, t.j. nesčítava výška pádu pred vstupom a po výstupe. Ak sa robot dostane na vstup teleportu, ktorý má výstup zaplnený iným robotom, robot na vstupe umiera.
V súbore world.txt sú vstupy do teleportu označené ako F (from) a výstup je označený T (to). Na výstupe sa teleport tiež označuje písmenami F a T, ale ak je na teleportovom políčku robot, vypíše sa namiesto toho ten.
- Po načítaní vstupu nájdite výstup teleportu a uložte referenciu na neho všetkým vstupom.
- Vytvorte triedy TeleportEntrySquare a TeleportExitSquare, ktoré budú podtriedami vhodne zvolených tried a implementujte metódy, ktoré treba preťažiť.
Prednáška 30
Opakovanie: generické programovanie
- V Jave môžeme definovať triedu alebo metódu, ktorá má špeciálny parameter určujúci typ dát, ktoré bude spracovávať, napr zásobník s prvkami typu T.
class Stack <T> { private Node<T> front; public void push(T data) { Node<T> p = new Node<T>(data, front); front = p; } public T pop() { T res = front.getData(); front = front.getNext(); return res; } }
Použitie zásobníka:
Stack<Integer> s = new Stack<Integer>(); s.push(new Integer(4)); s.push(5); Integer y = s.pop(); int z = s.pop();
V tom istom programe môžeme vytvoriť zásobníky veľa rôznych typov.
Java Collections
- Minulý semester programovali mnohé základné dátové štruktúry a algoritmy (zásobník, rad, slovník, triedenie...), vo väčšine moderných jazykov sú však tieto už k dispozícii v štandardných knižniciach
- Java Collections sú štandardné triedy na mnohé často používané dátové štruktúry, používajú generické programovanie
- Tutoriál
- Je dobré tieto triedy poznať a podľa potreby využívať
- Pochopením ich štruktúry si tiež môžeme precvičiť objektovo-orientované programovanie
- Na úvod dve ukážky
ArrayList
ArrayList sa podobá na vector z C++ (existuje aj trieda Vector)
- ide o štruktúru reprezentujúcu pole, ktoré rastie podľa potreby
- na koniec poľa pridávame metódou add(prvok), konkrétny prvok adresujeme metódou get(index), veľkosť poľa je size()
import java.util.ArrayList; ... ArrayList<Integer> a = new ArrayList<Integer>(); a.add(2); a.add(7); for (int i = 0; i < a.size(); i++) { System.out.println(a.get(i)); // vypiseme vsetky prvky pola a.set(i, -1); // a potom ich prepiseme na -1 }
LinkedList
LinkedList je obojsmerný spájaný zoznam, ktorý môžeme použiť napr. ako zásobník alebo rad
- vie teda efektívne pridávať a uberať prvky z oboch koncov a tiež prejsť cez všetky prvky zoznamu pomocou iterátora
- hľadanie prvku na pozícii i niekde v strede zoznamu je pomalé
LinkedList<Integer> a = new LinkedList<Integer>(); a.addFirst(2); // to iste ako push a.addLast(7); // to iste ako add for (ListIterator<Integer> it = a.listIterator(); it.hasNext(); ) { System.out.println(it.next()); } a.removeFirst(); // to iste ako pop a.removeLast();
Prehľad Collections
- Dátové štruktúry a algoritmy na základnú prácu so skupinami dát
- Generické typy - môžeme vytvárať dátové štruktúry pre dáta rôznych typov
- Definované pomocou interface-ov, jeden interface môže mať viacero implementácií
Vybrané triedy:
Interface | Význam | Implementácie |
Collection | skupina objektov | |
- Set | množina, skupina bez opakujúcich sa objektov | HashSet |
-- SortedSet | množina s definovaným usporiadaním prvkov | TreeSet |
- List | postupnosť objektov s určitým poradím | ArrayList, LinkedList |
Map | slovník, asociatívne pole, mapuje kľúče na hodnoty | HashMap |
- SortedMap | slovník s definovaným usporiadaním kľúčov | TreeMap |
V metódach je dobré argumenty definovať najvšeobecnejším vhodným interfacom alebo triedou.
- Napr. chceme spočítať súčet viacerých Integer-ov:
// tato metoda sa da pouzit iba na ArrayList public static Integer sum(ArrayList<Integer> a) { ... } // tato metoda sa da pouzit na hocijaku Collection (LinkedList, HashSet...) public static Integer sum(Collection<Integer> a) { ... }
Základné operácie pre Collection:
public interface Collection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object element); boolean add(E element); // optional boolean remove(Object element); // optional void clear(); // optional Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); // a dalsie... }
Metódy add a remove vracajú true, ak sa Collection zmenila a false ak sa nezmenila.
Prechádzanie cez prvky Collection
Použitie cyklu for-each:
public static Integer sum(Collection<Integer> a) { int sum = 0; for(Integer x : a) { sum += x; } return sum; }
Použitie iterátora:
public static Integer sum(Collection<Integer> a) { int sum = 0; for (Iterator<Integer> it = a.iterator(); it.hasNext();) { sum += it.next(); } return sum; }
- a.iterator() vráti objekt it implementujúci interface Iterator
- it.next() vráti ďalší prvok zo skupiny a, alebo hodí NoSuchElementException ak už ďalší nie je
- it.hasNext() vráti, či ešte je ďalší prvok
- it.remove() zmaže prvok, ktorý sme naposledy dostali pomocou next
- Poradie, v akom prvky navštívime, nie je definované
Pomocou iterátora môžeme napr. vymazať všetky nuly (môže byť pomalé):
static void removeZeroes(Collection<Integer> a) { for (Iterator<Integer> it = a.iterator(); it.hasNext();) { if (it.next().equals(0)) { // vynimka ak it.next() je null! it.remove(); } } }
Interface List definuje ListIterator
- umožňuje aj hýbať sa dozadu it.previous()
- pridávať a meniť prvky it.add(x), it.set(x)
- zisťovať polohu it.nextIndex(), it.previousIndex()
Iterátor pre SortedSet vracia prvky v utriedenom poradí od najmenšieho po najväčší.
Použitie Map
public interface Map<K,V> { V put(K key, V value); // klucu key prirad hodnotu value, vrati predch. hodnotu pre key V get(Object key); // hodnota pre kluc key alebo null V remove(Object key); // zmaz kluc key a jeho hodnotu boolean containsKey(Object key); // obsahuje kluc key? boolean containsValue(Object value); int size(); boolean isEmpty(); void putAll(Map<? extends K, ? extends V> m); void clear(); // Vrátia Set alebo Collection, cez ktorý môžeme iterovať public Set<K> keySet(); public Collection<V> values(); public Set<Map.Entry<K,V>> entrySet(); // Interface pre dvojice vo výsledku entrySet public interface Entry { K getKey(); V getValue(); V setValue(V value); } }
Príklad použitia Map:
- vstup z konzoly rozložíme Scannerom na slová (kým užívateľ nezadá END) a počítame počet výskytov každého slova
public static void main(String[] args) { Map<String, Integer> map = new HashMap<String, Integer>(); Scanner s = new Scanner(System.in); // inicializujeme Scanner while (s.hasNext()) { // kym neskonci vstup String word = s.next(); // nacitame slovo if (word.equals("END")) { // skoncili sme ak najdeme END break; } Integer freq = map.get(word); if(freq==null) { map.put(word, 1); } else { map.put(word, freq+1); } } System.out.println("Pocet roznych slov: " + map.size()); System.out.println(map);
Tu je príklad výstupu:
one two three one two two END Pocet roznych slov:3 {two=3, one=2, three=1}
HashMap vypisuje prvky v ľubovoľnom poradí. Ak typ zmeníme na TreeMap, dostaneme utriedené podľa kľúča:
{one=2, three=1, two=3}
Ak chceme vypísať zoznam slov a ich frekvencií v inom formáte, použijeme iterátor (alebo for)
for(Iterator<Map.Entry<String,Integer>> it=map.entrySet().iterator(); it.hasNext();) { Map.Entry<String,Integer> e = it.next(); System.out.println("Slovo " + e.getKey() + " sa vyskytuje " + e.getValue() + " krat"); }
Slovo two sa vyskytuje 3 krat Slovo one sa vyskytuje 2 krat Slovo three sa vyskytuje 1 krat
Porovnávanie objektov na rovnosť: equals
- Metódy z Collection contains(Object element), remove(Object element) a ďalšie potrebujú porovnávať objekty na rovnosť.
- Operátor == porovnáva referencie, t.j. či sú dva objekty na tej istej adrese v pamäti
- Collection používa namiesto toho metódu equals(Object obj) definovaná v triede Object
- Metóda equals() v triede Object tiež porovnáva len referencie, ostatné triedy ju môžu preťažiť
- Napr. v triedach ako String, Integer,... definovaná na porovnávanie reťazcov, čísel,...
- Rôzne triedy implementujúce Collection tiež väčšinou vedia porovnávať na rovnosť spúšťaním equals na jednotlivé prvky
- Metódy nevieme spúšťať na null, napr. contains(Object o) vracia true práve vtedy keď nejaký prvok e Collection spĺňa (o==null ? e==null : o.equals(e))
- Preťažená metóda equals by sa mala správať "rozumne", t.j. byť symetrická, tranzitívna a pod.
Porovnávanie objektov na nerovnosť: Comparable
- SortedMap a SortedSet potrebujú vedieť porovnávať prvky podľa veľkosti
- Používajú metódu compareTo definovanú v interface Comparable (videli sme na minulej prednáške)
- Ak naša trieda neimplementuje tento interface alebo chceme použiť iné usporiadanie, môžeme použiť vlastný komparátor, objekt implementujúci interface Comparator
Hashovacie funkcie: hashCode
- HashSet a HashMap potrebujú vedieť prekódovať ľubovoľný objekt do pozície v poli
- Používajú metódu int hashCode() definovanú v triede Object
- Object jednoducho použije svoju adresu v pamäti ako svoj hashCode
- Štandardné triedy preťažujú hashCode
- Ak preťažíte equals, treba preťažiť aj hashCode, lebo ak sa dva prvky rovnajú v equals, majú mať rovnaký hashCode
static class Name { String givenName; String lastName; @Override public int hashCode () { return givenName.hashCode() + 31*lastName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Name other = (Name) obj; return this.givenName.equals(other.givenName) && this.lastName.equals(other.lastName); } }
Algoritmy
- Triedy Collections a Arrays obsahujú statické metódy na prácu s Collections a poliami
- Napr. sort, shuffle (náhodne preusporadaj), reverse, fill, copy, swap, binarySearch, min, max,...
Zhrnutie
- Collections sú obrovská knižnica a veľmi užitočná
- Neváhajte ich používať v programoch, nájdite si v dokumentácii metódy, ktoré sa vám hodia
- Pri spracovaní väčších dát pozor na to, že niektoré metódy sú pomalé
- Mali by ste ovládať (s pomocou ťaháka) aspoň základy práce s ArrayList, LinkedList, HashMap a s iterátormi
- Pre prácu s Collections môže byť potrebné preťažiť niektoré metódy z Object (equals, hashCode)
- Ďalšie metódy z Object, ktoré sa často hodí preťažiť sú clone a toString
Testovanie programov
- Cieľom testovania je nájsť chyby v programe, teda preukázať, že program nefunguje podľa špecifikácie (aby bolo možné potom chyby lokalizovať a opraviť)
- Test pozostáva zo vstupu, správneho výstupu a popisu jeho významu
- Program sa spustí na vstupe a jeho výsledok sa porovná so správnou odpoveďou
- Tradičný prístup: najprv sa napíše kód, potom sa vytvárajú testy
- Test-driven development: najprv sa napíšu testy, potom sa programuje kód, ktorý ich dokáže splniť
Black-box testing
- Vytvorenie sady testov len na základe špecifikácie
- Snažíme sa zachytiť okrajové aj typické prípady
- Napr.:
/** Z pola a vyhodi prvy vyskyt objektu rovneho x * pricom rovnost sa testuje metodou equals. * Vsetky dalsie prvky posunie o jedno dolava a na koniec * pola da null. * Vrati true, ak bolo pole modifikovane, inak false. * Ak a je null alebo x je null, vyhodi java.lang.NullPointerException */ public static boolean remove(Object[] a, Object x) {
Príklady testov:
- prázdne pole a
- pole obsahujúce iba x
- pole obsahujúce x na začiatku
- pole obsahujúce x na konci
- pole obsahujúce x v strede
- pole obsahujúce viacero kópií x
- pole obsahujúce null prvky
- pole obsahujúce objekty rôznych typov
White-box testing
Testy vytvárame na základe kódu, snažíme sa preveriť všetky vetvy výpočtu
- V cykle vyskúšame 0 iterácií, 1 iteráciu, maximálny počet iterácií
- V podmienke vyskúšame vetvu true aj false
Tento kód nespĺňa úplne špecifikáciu:
/** Z pola a vyhodi prvy vyskyt objektu rovneho x * pricom rovnost sa testuje metodou equals. * Vsetky dalsie prvky posunie o jedno dolava a na koniec * pola da null. * Vrati true, ak bolo pole modifikovane, inak false. * Ak a je null alebo x je null, hodi java.lang.NullPointerException */ public static boolean remove(Object[] a, Object x) { int i; for (i = 0; i < a.length; i++) { if (a[i].equals(x)) { break; } } if (i == a.length) { return false; } while (i < a.length - 1) { a[i] = a[i + 1]; i++; } a[i] = null; return true; }
JUnit
- Systém JUnit umožňuje vytvárať špeciálne triedy obsahujúce testy iných tried
- Sadu testov môžeme ľahko automaticky spustiť a vyhodnotiť, vidíme všetky výsledky
- Môžeme program testovať po každej zmene
- Dobrá podpora v Netbeans
- Krátky návod: [6]
Príklad niekoľkých testov pre funkciu remove vyššie:
package prog; import org.junit.Test; import static org.junit.Assert.*; public class ProgTest { @Test public void testEmpty() { // hladame x v poli dlzky nula Object[] working = new Object[0]; // vstupne pole Object[] correct = new Object[0]; // spravna odpoved Object x = new Object(); boolean result = Prog.remove(working, x); // spustime testovanu metodu assertEquals(result, false); // testujeme navratovu hodnotu assertTrue(java.util.Arrays.equals(working,correct)); // testujeme pole } @Test public void testXOnly() { // hladame x v poli obsahujucom iba x Object[] working = {7}; Object[] correct = {null}; Object x = 7; boolean result = Prog.remove(working, x); assertEquals(result, true); assertTrue(java.util.Arrays.equals(working,correct)); } @Test(expected = java.lang.NullPointerException.class) public void testANull() { // Testujeme, ci hodi vynimku ked je pole null Object[] working = null; Object x = 7; boolean result = Prog.remove(working, x); } }
Tento príklad je možné rôzne vylepšovať
- Opakujúce sa časti kódu môžeme dať do pomocných metód
- Môžeme pridať výpisy výsledkov, aby sme v prípade chyby videli, čo sa stalo
- Môžeme pridať premenné triede ProgTest, konštruktor, ako aj špeciálne metódy, ktoré sa vykonajú pred každým testom, prípadne po každom teste.
Cvičenia 18
- Napíšte generickú statickú metódu prienik, ktorá dostane dve SortedSet (s tým istým typom prvkov E) a vráti SortedSet obsahujúcu ich prienik, t.j. prvky, ktoré sa nachádzajú v oboch. Použite algoritmus podobný na Merge, pričom sa posúvate dvoma iterátormi (iterátory pre SortedSet vracajú prvky v utriedenom poradí).
- Vyskúšajte si pre túto funkciu napísať niekoľko testovacích vstupov pomocou JUnit.
- Napíšte generickú statickú metódu kazdyDruhy, ktorá dostane List a vyhodí z neho každý druhý prvok. K Listu pristupujte iba cez iterátor a jeho metódu remove.
- Napíšte triedu Zlomok, ktorá bude implementovať zlomok s celočíselným čitateľom a menovateľom. Triede spravte konštruktor a preťažte equals tak, aby správne testovala rovnosť zlomkov a hashCode tak, aby bol konzistentný s equals. Vaša trieda by tiež mala implementovať interface Comparable s bežným porovnaním zlomkov podľa veľkosti. Skúšajte potom zlomky vkladať do TreeSet a HashSet a skontrolujte, že dostávate správne výsledky. Pozor na záporné čísla. Môže sa vám zísť Euklidov algoritmus na nájdenie najväčšieho spoločného deliteľa:
// ratame nsd(a,b) while(b != 0) { int x = a % b; a = b; b = x; } // vysledok je v premennej a
Prednáška 31
Opakovanie OOP
- Java je objektovo orientovaný jazyk - všetko sú objekty
- Program pozostáva z objektov, ktoré navzájom 'komunikujú' prostredníctvom svojich metód
Objekty, zapúzdrenie
- Trieda (class) definícia objektu
class MyClass { premenné konštruktory metódy }
- Konštruktor - vytváranie objektu
- používanie this(..) resp. super(..) na volanie iného konštruktora tej istej triedy, resp. nadtriedy
- Pes punto=new Pes("Punto");
- Úrovne prístupu - Modifikátory
- Zapuzdrenie - oddelenie rozhrania a implementácie
- Nemení sa to, čo je viditeľné zvonku
- Pozor na prístup k premenným
Dedenie objektov, polymorfizmus
- Trieda môže byť potomkom inej triedy - potom zdedí jej premenné a metódy
- Dediť môžeme iba od jednej triedy (ale môžeme viacúrovňovo)
- Triedy, ktoré nemajú predchodcu dedia všeobecnú triedu Object
- Aj základné typy premenných majú svoje objektové varianty
- Metódy sú virtuálne a je možné ich preťažiť - potomok tým prepíše chovanie metódy
- Potomka je vždy možné použiť na mieste predka, keďže má všetky jeho 'vlastnosti' - spolu s virtuálnymi metódami umožňuje polymorfizmus
Interface
- Abstraktná trieda, ktorá nemá žiadnu metódu implementovanú
- Trieda môže implementovať interface - musí implementovať všetky jej metódy
- Umožňuje "viacnásobnú dedičnosť" a zaručuje dobré zapuzdrenie
Generické programovanie
- Ako mať rôzne typy premenných vnútri nejakej štruktúry (napr. zásobník)
- Bude skladovať Object a keďže potomka môžeme použiť namiesto predka môžeme tam skladovať čokoľvek - musíme pretypovávať
- Generické programovanie: parametrický typ class Stack <T>, kde T je parameter reprezentujúci typ objektov, pri definícii povieme konkrétny typ
- Aj metódy môžu mať generické typy - typový parameter, ktorý sa píše pred návratový typ
static <E> void fillArray(E[] a, E e)
- Generické funkcie (napr. triedenie) s využitím interface
static <E extends Comparable> void sort(E[] a)
Swing
- Doteraz sme mali aplikácie, ktoré pracovali s textovým vstupom a výstupom - konzolové aplikácie
- Ukážeme si jednoduché aplikácie, ktoré majú grafické UI
- Grafické okno, ovládacie prvky, ...
- Udalosti, ktoré vedú k akciám
Program s jedným grafickým oknom
V klasickej java aplikácii (ako sme používali doteraz) môžeme jednoducho vytvoriť aj grafické okno.
- Potrebujeme importovať javax.swing.*
- V main následne vytvoríme frame - okno, ktoré je typu JFrame a s ním ďalej pracujeme
import javax.swing.*; public class JavaApplication { public static void main(String[] args) { JFrame frame = new JFrame("Hello Swing"); frame.setSize(300, 100); frame.setVisible(true); } }
- JFrame frame = new JFrame("Hello Swing"); - vytvorenie okna pomocou konštruktora
- nastavíme veľkosť a okno zobrazíme
- napriek tomu,že okno zavrieme program nám beží ďalej potrebujeme povedať, že uzavretie okna znamená zároveň ukončenie programu
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Text v okienku
- Do takto vytvoreného okna si môžeme vytvárať grafické (ovládacie) prvky label, button, ...
- pochopiteľne pre tieto prvky má Java vlastné triedy, ktorých inštancie vytvárame
- Pre jednoduchosť začneme textovým popiskom - label, ktorý je inštanciou JLabel
JLabel label = new JLabel("Label"); frame.add(label);
- Ďalej môžeme s týmto popiskom pracovať (napríklad mu zmeniť text)
- V našom prípade si počkáme napr. sekundu a potom text zmeníme
import javax.swing.*; import java.util.concurrent.*; public class HelloLabel { public static void main(String[] args) throws Exception { JFrame frame = new JFrame("Hello Swing"); JLabel label = new JLabel("Label"); frame.add(label); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300, 100); frame.setVisible(true); TimeUnit.SECONDS.sleep(1); label.setText("Changed Label"); } }
- Toto má už trochu problém. My totiž meníme text niečomu, čo môže práve pracovať a meníme to za behu.
- Grafické okno beží v samostatnom vlákne, kde spracováva všetko, čo sa deje v tomto okne nezávisle od toho, čo robí main.
- A čo sme my urobili je, že sme menili niečo v grafickom okne mimo vlákna, ktoré zodpovedá práci v grafickom okne.
- Aby sme pridali niečo do vlákna, ktoré pracuje (a v čase, kedy sa to môže vykonať) použijeme funkciu SwingUtilities.invokeLater();
- Parametrom funkcie je inštancia triedy, ktorá implementuje interface Runnable
- Interface Runnable má jedinú funkciu, ktorú je nutné implementovať - funkciu run, ktorá hovorí, čo sa to vlastne má vykonať
static class myClass implements Runnable { JLabel label; @Override public void run(){ label.setText("Changed Label"); } myClass(JLabel label_) { this.label=label_; } } public static void main(String[] args) throws Exception { JFrame frame= new JFrame("Hello Swing"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); frame.setVisible(true); final JLabel label = new JLabel("Label"); frame.add(label); TimeUnit.SECONDS.sleep(1); myClass c=new myClass(label); SwingUtilities.invokeLater(c);
- Vytvárať si kvôli každému volaniu inštanciu triedy je trochu nepraktické a preto namiesto toho môžeme použiť ako parameter funkcie
- SwingUtilities.invokeLater(new myClass(label));
- Nemusíme si dokonca kvôli tomu vytvárať ani celú tú triedu - môžeme využiť tzv. anonymnú triedu
SwingUtilities.invokeLater(new Runnable() { @Override public void run() { label.setText("Changed Label"); }});
- Každým jedným použitím SwingUtilities.invokeLater() vlastne hovoríme, čo sa má v grafickom okne vykonať (okrem akcií, ktoré si grafické okno spravuje samé)
- Teda by sme mali nejakým spôsobom prepísať aj samotné vytvorenie okna
- Najjednoduchšia možnosť by bola použiť presne to isté, ako pri zmene nápisu - vytvorenie anonymnej triedy
- Problém je, že vytvorenie frame a label (pomocou new) musí byť mimo tejto triedy, aby sme k nim mohli pristupovať
final JFrame frame = new JFrame("Hello Swing"); final JLabel label = new JLabel("Label"); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); frame.setVisible(true); frame.add(label); }});
- Nie je to však veľmi pekné riešenie, aj keď je korektné, lebo vytvorením frame a label ešte vlastne nevykonávame nič naozaj 'Swingové'.
- Môžeme si však vytvoriť novú neanonymnú triedu, ako sme to robili pred tým a v nej implementovať run. Táto trieda bude mať premenné frame a label.
static class theWindow implements Runnable { JFrame frame; JLabel label; @Override public void run(){ frame = new JFrame("Hello Swing"); label = new JLabel("Label"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); frame.setVisible(true); frame.add(label); } } ... final theWindow window=new theWindow(); SwingUtilities.invokeLater(window); TimeUnit.SECONDS.sleep(1); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { window.label.setText("Changed Label"); }});
Button a jeho stlačenie
- Doteraz sme síce menili grafické okno, ale nemali sme (okrem zavretia) žiadnu interakciu s používateľom
- Ukážeme si najjednoduchší ovládací prvok - Button a jednoduchú akciu - stlačenie gombíka
- Button pridáme podobne ako Label pomocou frame.add()
- Stlačenie gombíka je akcia, ktorá sa môže alebo aj nemusí stať - záleží od používateľa - preto na ňu musíme čakať a v prípade, že nastane nejako ju spracovať
- Použijeme na to interface, ktorý implementujeme anonymnou triedou. V našom prípade potrebujeme interface ActionListener, ktorý má jedinú funkciu actionPerformed() s parametrom ActionEvent, ktorý hovorí parametre tej udalosti (na ktorom objekte nastalo a nejaké bližšie informácie)
import javax.swing.*; import java.awt.*; import java.awt.event.*; public class JavaApplication7 { static class theWindow implements Runnable { JFrame frame; JButton button; @Override public void run(){ frame = new JFrame("Hello Swing"); button = new JButton("Button"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); frame.setVisible(true); frame.add(button); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e){ System.err.println("action!"); } }); } } public static void main(String[] args) { final theWindow window=new theWindow(); SwingUtilities.invokeLater(window); } }
- beží to v SWINGu - teda vo vlákne, grafického okna - zaraďuje to akcie priamo do fronty
- môžeme priamo - t.j. bez invokeLater pracovať s objektami grafického okna
- kým sa to spracováva tak grafické okno 'zatuhne'
button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e){ button.setText("Ha!"); ((JButton)e.getSource()).setText("He!"); } });
- Jeden ActionListener môžem nasadiť aj na viac objektov - napríklad niekoľko gombíkov
- Urobím si triedu, ktorá implementuje ActionListener a používam ju neanonymne
- vďaka ActionEvent a jej funkcii getSource viem identifikovať, kde daná udalosť nastala
Viac akcií
- Pre rôzne ovládacie prvky existujú rôzne akcie, ktoré sa môžu vykonať - v závislosti od konkrétneho ovládacieho prvku
- zoznam je dlhý link title
- Už pre gombík môžeme okrem stlačenia pozorovať napríklad pohyb myši - interface MouseListener
- tento interface však má nie jednu ale 5 funkcií, ktoré môžu nastať - mouseClicked, mouseEntered, mouseExited, mousePressed, mouseReleased
- je možné, že nás všetky nezaujímajú, ale napriek tomu (keďže je to interface) ich musíme všetky implementovať (aspoň prázdnou funkciou)
button.addMouseListener(new MouseListener() { @Override public void mouseClicked(MouseEvent e) { System.err.println("click!"); } @Override public void mouseEntered(MouseEvent e) { System.err.println("enter!"); } @Override public void mouseExited(MouseEvent e) { System.err.println("exit!");} @Override public void mousePressed(MouseEvent e) { System.err.println("press!"); } @Override public void mouseReleased(MouseEvent e) { System.err.println("release!"); } });
- Takýmto spôsobom môžeme mať na jeden prvok zavesených viacerých, ktorí čakajú na nejakú svoju udalosť (napríklad ActionListener a MouseListener)
Adaptéry
- Je nepraktické implementovať aj funkcie, o ktoré vôbec nemám záujem - potrebujem si zohnať ich zoznam a vytvorí to kopu zbytočného kódu
- Navyše sa môže ľahko stať, že časom nejaké funkcie pribudnú a program sa stane neskompilovateľným (lebo nami vytvorená trieda nebude implementovať všetky funkcie)
- Preto existuje defaultná implementácia každého listenera - tzv. adaptér, ktorý každú funkciu implementuje prázdnou funkciou
- My môžeme potom iba vytvoriť (anonymnú alebo neanonymnú) triedu, ktorá rozširuje tento adaptér a preťažuje tie funkcie, ktoré ma zaujímajú
button.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { System.err.println("click2!"); } });
- Zaujímavé je, že na jeden objekt môžeme dokonca nasadiť aj viacerých, ktorí očakávajú tú istú udalosť (listeneri sa sčítajú a nerušia navzájom)
Swing naklikaním
- V Netbeans a v iných editoroch je možnosť si GUI aj naklikať
- Aktuálne Netbeans majú inú verziu
- Prestali defaultne podporovať SWING - nové JavaFX
- Ak chceme vytvárať SWING pomocou klikania, Tools->Plugins->Available Plugins->SWING Application Framework
- Potom v bežnej Java Application vieme vytvoriť SWINGové okno pomocou New-> JFrame Form (pozor na umiestnenie do vhodného package)
- V súbore NewJFrame.java (alebo ako si triedu pre okno nazvete) sa potom dá okno nielen naprogramovať ale aj naklikať
- Pôvodne (staršie verzie)to fungovalo takto:
- vytvoríme projekt typu Desktop Application, vytvoria sa tri základné súbory
- DesktopApplication.java - obsahuje nastavenia pred štartom SWINGu a samotné spustenie SWINGu
- DesktopApplicationView.java - priamo práca grafického okna - nastavenie prvkov, udalosti
- DesktopApplicationAboutBox.java - čo sa objaví keď chcem About
- V nich sa vytvorí rovno potrebný zdroják
- vytvoríme projekt typu Desktop Application, vytvoria sa tri základné súbory
- Všeobecne sa naklikať dá zhruba nasledovné veci:
- vytvorenie objektov jButton1 = new javax.swing.JButton();
- ich inicializácia jButton1.setText(resourceMap.getString("jButton1.text"));
- vytvorenie listenerov
jButton1.addMouseListener(new java.awt.event.MouseAdapter() { public void mouseClicked(java.awt.event.MouseEvent evt) { jButton1MouseClicked(evt); } });
- Pre akcie, ktoré sme si naklikali (vybrali z menu Properties) sa vytvoria prázdne funkcie, ktorých obsah (pozor nie deklaráciu) budeme dopĺňať
- nemôžem napríklad pridat throws Exception!
private void jButton1MouseClicked(java.awt.event.MouseEvent evt) { System.err.println("Click"); try { Thread.sleep(10000);} catch(Exception e) {} }
Cvičenia 19
- Vytvorte aplikáciu, ktorá bude mať jeden gombík a bude do konzolového okna vypisovať, koľký krát bol gombík stlačený.
- Ako by ste rozlíšili rôzne typy stlačení - myšou/klávesnicou
- Aplikáciu skúste vytvoriť oboma spôsobmi - vlastným naprogramovaním aj naklikaním aplikácie
- Vytvorte aplikáciu, ktorá bude mať tri gombíky s popiskami 1,2 a 3. Do konzolového okna bude vypisovať číslo gombíku, ktorý bol stlačený.
- Použite iba jeden Actionlistener, ktorý čaká na ľubovoľnú udalosť stlačenia gombíka.
- Skúste, ako môžete meniť polohu a veľkosť prvkov v okne.
- Potrebujete nastaviť frame.setLayout(null)
- setLocation, setSize, .. pozrite si aj ostatné vlastnosti prvkov
- Vytvorte aplikáciu, ktorá vytvorí v okne veľkosti 500x500 gombík veľkosti 20x20 v strede. Pomocou šípok meňte polohu gombíka.
- Vytvorte text, ktorý sa bude sám hýbať po obrazovke v istých časových intervaloch. Premyslite si čím text zastavíte resp. ako aplikáciu ukončiť.
Prednáška 32
Pozície a layouty
- Na cvičení ste menili pozície a veľkosť gombíkov a textu. V prípade, že ste to robili naklikaním, nebol s tým problém, ale ak ste skúšali napísať si program sami, veselo vás ignoroval.
- Súvisí to s rôznym rozložením ovládacích prvkov - layoutami.
- Na to, aby sa dalo meniť polohy prvkov, potrebujeme nastaviť frame.setLayout(null);
frame.setLayout(null); label=new JLabel("Label"); button=new JButton("Button"); label.setLocation(10, 10); label.setSize(50,30); button.setLocation(100, 10); button.setSize(100,30); frame.add(button); frame.add(label);
- Pozície, na ktoré sú umiestňované jednotlivé komponenty normálne nie sú určené priamo súradnicami, ale Layout managerom
- Layout nastavíme pomocou setLayout(...), ktoré môžeme použiť pre frame, window, dialog ...
- Rôzne layouty spôsobia rôzne veľkosti, tvary a umiestnenia prvkov
- Navyše rozloženie a veľkosť prvkov ovplyvňuje aj veľkosť okna - pri zmene sa môžu meniť aj prvky
- Ukážeme si umiestňovanie gombíkov do zopár layoutov
Border Layout
- Defaultné nastavenie Layout managera
- Má 5 možností, kam umiestniť prvok (CENTER, NORTH, SOUTH, EAST, WEST)
- Default je CENTER
- Ak umiestnime viacero prvkov na jedno miesto, budú cez seba
// frame.setLayout(new BorderLayout()); -- defaultný layout pre frame button=new JButton("BUTTON"); // defaultné umiestnenie je borderLayout.CENTER frame.add(new JButton("NORTH"),BorderLayout.NORTH); frame.add(new JButton("SOUTH"), BorderLayout.SOUTH);
Flow layout
- Vkladá prvky do radu v poradí, ako sú vkladané.
- Môže sa vkladať zľava doprava alebo naopak
- Používa prirodzené alebo preferované veľkosti prvkov
- Prvky sú defaultne v riadku vycentrované
- V prípade, že sa nezmestia pokračuje ďalším radom.
Kombinácia Layoutov
- Väčšinou preddefinované layouty nepostačujú na to, čo chceme vytvoriť - prečo sú potom také?
- Layouty nepoužívame samotné, ale nejakú ich kombináciu.
- Samotné okno má ako default BorderLayout, ktorý zodpovedá rozloženiu rôznych menu v základnom okne
- Do stredu potom potrebujeme vložiť taký prvok, ktorý bude vhodne rozkladať prvky, vnútri - JPanel, Box ...
- Týmto prvkom môžeme zadať ich Layout (a defaultný Layout majú iný ako má Jframe)
panel=new JPanel(); // panel.setLayout(new FlowLayout()); - default pre Panel je FlowLayout frame.add(panel); panel.add(new JButton("BUTTON"); panel.add(new JLabel("LABEL")); frame.add(new JButton("HORNY GOMBIK"), BorderLayout.NORTH); frame.add(new JButton("DOLNY GOMBIK"), BorderLayout.SOUTH);
Menu
static class theWindow implements Runnable { JFrame frame; @Override public void run() { frame = new JFrame("Hello Swing"); JMenuBar menubar = new JMenuBar(); JMenu menu1 = new JMenu("Riadok1"); JMenuItem eMenuItem = new JMenuItem("Exit"); eMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { System.exit(0); } }); // mam obrazok icon.gif v Source Packages ImageIcon icon = new ImageIcon(getClass().getResource("icon.gif")); JMenuItem iconMenuItem = new JMenuItem("Ikonka",icon); iconMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { System.err.println("Mam ikonku."); } }); menu1.add(iconMenuItem); menu1.add(eMenuItem); menubar.add(menu1); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300, 300); frame.setVisible(true); frame.add(menubar,BorderLayout.NORTH); } }
- JMenuBar obsahuje JMenu a to obsahuje JMenuItem.
- Namiesto JMenuItem môžem pridávať znovu JMenu a tým vytvoriť submenu
Viacoknové aplikácie a dialogy
- Doteraz sme robili aplikácie, ktoré mali jedno okno, v ktorom sa všetko odohrávalo.
- Niekedy potrebujeme nejakú akciu vyvolať v inom okne
Dialógové okno
- Najjednoduchšie dialógové okno je MessageDialog, ktorý má iba jednu možnosť ako ďalej pokračovať a to, že zavrieme okno a informáciu sme vzali na vedomie
JOptionPane.showMessageDialog(frame, "Haf!", "Dialog Haf", JOptionPane.INFORMATION_MESSAGE, new ImageIcon(getClass().getResource("icon.gif"))); JOptionPane.showMessageDialog(frame, "Haf!", "ERROR HAF", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(frame, "Mnau!");
- Ak chceme odozvu, máme niekoľko možností: OptionDialog, ConfirmDialog alebo InputDialog [7]
- Ich návratovú hodnotu už vieme používať ďalej v programe
if (JOptionPane.showConfirmDialog(frame, "Haf?", "Dialog Haf", JOptionPane.YES_NO_OPTION)==0) JOptionPane.showMessageDialog(frame, "Haf!"); else JOptionPane.showMessageDialog(frame, "Mnau!");
JDialog a JFrame
- Ak chcem vytvoriť viacero okien, mám v zásade dve možnosti - ďalší JFrame a JDialog
- JFrame sa správa ako klasické okno s tým, že mi nebráni prepnúť do pôvodného okna bez ukončenia
JFrame frame2=new JFrame("2nd Frame"); frame2.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); frame2.setVisible(true);
- JDialog sa môže správať rovnako ako klasický frame, nemá však možnosť maximalizácie, minimalizácie ...
- Výhodou JDialogu je možnosť nastavenia jeho modality - t.j. že sa nedá bez ukončenia pokračovať v programe
- setVisible(true) pre okno, ktoré je modálne sa zablokuje kým sa okno nezavrie
JDialog frame2=new JDialog(frame, "2nd Frame - Dialog"); frame2.setModal(true); frame2.setVisible(true);
- Z vedľajších okien často potrebujeme dostať nejakú informáciu - vyplnený text, ktorý gombík bol stlačený ...
- Keďže aj pri zatvorení okno zostane v pamäti môžeme pristupovať k jeho položkám a volať jeho metódy
- V prípade, že potrebujeme infomáciu z akcie tak si to uložíme ako parameter nášho rozšírenia JDialog/JFrame
class myYesNoDialog extends JDialog{ JTextField field; boolean res; myYesNoDialog(JFrame owner, String title) { super(owner); setModal(true); res=false; JPanel panel = new JPanel(); JButton yes = new JButton("yes"); JButton no = new JButton("no"); field = new JTextField(10); final myYesNoDialog dialog=this; // implemntačný problém ako dostať túto triedu do anonymných tried k akciám yes.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { dialog.res=true; dialog.setVisible(false); } }); no.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { dialog.res=false; dialog.setVisible(false); } }); add(panel); panel.add(yes); panel.add(no); panel.add(field); pack(); // nastaví veľkosť dialogu tak, aby sa všetko zmestilo setVisible(true); // zablokuje volajúceho až kým sa okno nezavrie } public boolean result() { return res; } public String value() { return field.getText(); } } ... myYesNoDialog d = new myYesNoDialog(this.frame,"Yes or no?"); if (d.result()) JOptionPane.showMessageDialog(this.frame, "Yes - "+d.value()); else JOptionPane.showMessageDialog(this.frame, "No - "+d.value());
Zložitejší príklad
- Zložitejší layout - nebojte sa kombinovať
- Niekoľko zložitejších ovládacích prvkov
- JTextField - v našom prípade iba ak stlačíme gombík pracujeme s textom v ňom
- JList - zoznam z ktorého môžeme vyberať (ale ho aj meniť za behu)
- každý JList má vlastný ListModel (čo s tým zoznamom môžeme robiť - v našom prípade pridávanie a mazanie na konkrétnych miestach zoznamu)
- ScrollPane - obdoba JPanel, ktorý v prípade potreby prida scrollbar - už pri vzniku potrebuje vedieť prvok, ktorý bude na ňom
- Využitie dedenia - štruktúra vlastných tried aj dedenie od ovládacích prvkov
- Pes a jeho podtriedy Civava a Bernardin
- Dedenie od JButton v prípade, že chceme rozšíriť napr. o tag - v našom prípade informáciu ku ktorej triede gombík prislúcha
Zdrojový kód programu
package javaapplication10; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class JavaApplication10 { static class Dog { String name; public String bark() { return "Haf!"; } public String getName() { return name; } protected Dog(String name) { this.name=name; } public @Override String toString() { return name; } } static class StBernard extends Dog { public StBernard(String name) { super(name); } @Override public String bark() { return "HAF!"; } } static class Chihuahua extends Dog { public Chihuahua(String name) { super(name); } @Override public String bark() { return "Hif..."; } } static class DogButton extends JButton implements ActionListener { Dog dog; public DogButton(Dog dog_) { super(dog_.getName()); addActionListener(this); dog=dog_; } @Override public void actionPerformed(ActionEvent event) { JOptionPane.showMessageDialog(this, dog.bark(),dog.getName()+" says:",JOptionPane.INFORMATION_MESSAGE); } public Dog getDog() { return dog; } } static class theWindow implements Runnable { @Override public void run() { final JFrame frame = new JFrame("Playground"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final JMenuBar menu = new JMenuBar(); final JMenu menuFile=new JMenu("File"); final JMenuItem menuExit = new JMenuItem("Exit"); menu.add(menuFile); menuFile.add(menuExit); menuExit.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); frame.add(menu,BorderLayout.NORTH); final JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel,BoxLayout.PAGE_AXIS)); frame.add(panel); final JPanel top_panel = new JPanel(); final JPanel middle_panel = new JPanel(); final JPanel bottom_panel = new JPanel(); middle_panel.setPreferredSize(new Dimension(0,50)); middle_panel.setLayout(new BoxLayout(middle_panel,BoxLayout.LINE_AXIS)); panel.add(top_panel); panel.add(middle_panel); panel.add(bottom_panel); final JTextField field = new JTextField(10); top_panel.add(field); final DefaultListModel model = new DefaultListModel(); final JButton addButton = new JButton("Add big"); addButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Dog dog=new StBernard(field.getText()); middle_panel.add(new DogButton(dog)); middle_panel.revalidate(); model.add(model.getSize(), dog); } }); top_panel.add(addButton); final JButton addButton2 = new JButton("Add small"); addButton2.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Dog dog=new Chihuahua(field.getText()); middle_panel.add(new DogButton(dog)); middle_panel.revalidate(); model.add(model.getSize(), dog); } }); top_panel.add(addButton2); final JList list = new JList(); list.setModel(model); // scrollPane vyzaduje svoj obsah ako argument, nepouziva klasicky add final JScrollPane scroll = new JScrollPane(list); bottom_panel.setLayout(new BorderLayout()); bottom_panel.add(scroll); final JButton deleteButton=new JButton("Delete"); deleteButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (list.getSelectedIndex()!=-1) { for(Component x : middle_panel.getComponents()) { if (x instanceof DogButton) { if (((DogButton)x).getDog()==list.getSelectedValue()) { middle_panel.remove(x); middle_panel.revalidate(); model.remove(list.getSelectedIndex()); break; } } } } } }); bottom_panel.add(deleteButton,BorderLayout.LINE_END); frame.pack(); frame.setVisible(true); } } public static void main(String[] args) { final theWindow window=new theWindow(); SwingUtilities.invokeLater(window); } }
Úvod do kreslenia v SWINGu
Vo SWINGu neexistuje priamo Canvas, ktorý by bol určený čisto na vykresľovanie. Vykresliť však môžeme na rôzne komponenty napríklad na JPanel. Každý komponent má funkciu paintComponent, ktorý ho vykreslí. Tento si my môžeme v našej triede rozšíriť podľa potreby.
Jednoduché nakreslenie prvkov na plochu
- Vytvoríme si teda vlastný grafický panel MyPanel, ktorý bude rozšírením JPanel.
- Potrebujeme preťažiť jeho funkciu paintComponent.
- Najprv zavoláme samotné vykreslenie komponentu (predchodcu).
- Potom môžeme využívať funkcie na rôzne vykreslenia prvkov - textu, štvorca, kruhu..
- Týmto dostávame okno, v ktorom je niečo vykreslené, ale nie je to 'aktívne'.
import javax.swing.*; import java.awt.*; class MyPanel extends JPanel { public MyPanel() { setBorder(BorderFactory.createLineBorder(Color.black)); } @Override public Dimension getPreferredSize() { return new Dimension(250,200); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); // Draw Text g.drawString("This is my custom Panel!",10,20); // Draw Rectangle g.drawRect(50,50,50,50); } }
Aktívne prekresľovanie
- Keď chceme meniť to, čo je vykreslené, nemôžeme odhocikiaľ zavolať nakreslenie textu, štvorca ...
- Vykreslenie (t.j. zavolanie paintComponent) sa volá vždy, keď sa okno nejakým spôsobom zmení - zmena veľkosti ...
- Čo však môžeme urobiť je odniekiaľ oznámiť, že sa má okno prekresliť
- Doteraz sme to robili pomocou revalidate, ktoré skúsilo odznovu zmeniť layout, ale keďže v paneli sa nič nezmenilo, tak sa ani nebude volat paintComponent.
- Môžeme ho k tomu donútiť napríklad zmenou veľkosti mp.setSize(mp.getWidth()+1,mp.getHeight()); ale je to mierne nepraktické.
- Pre prekreslenie - t.j. zavolanie paintComponent, bez zmeny layoutu vieme použiť funkciu repaint(), čo prekreslí celý panel.
- Môžeme prekresliť iba tú časť, ktorá je zaujímavá (t.j. pôvodný štvorec a nový štvorec) s použitím repaint s vhodnými parametrami.
class MyPanel extends JPanel { private int squareX = 50; private int squareY = 50; private int squareW = 20; private int squareH = 20; public MyPanel() { setBorder(BorderFactory.createLineBorder(Color.black)); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { moveSquare(e.getX(),e.getY()); } }); addMouseMotionListener(new MouseAdapter() { @Override public void mouseDragged(MouseEvent e) { moveSquare(e.getX(),e.getY()); } }); } private void moveSquare(int x, int y) { int OFFSET = 1; if ((squareX!=x) || (squareY!=y)) { repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET); squareX=x; squareY=y; repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET); } } @Override public Dimension getPreferredSize() { return new Dimension(250,200); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawString("This is my custom Panel!",10,20); g.setColor(Color.RED); g.fillRect(squareX,squareY,squareW,squareH); g.setColor(Color.BLACK); g.drawRect(squareX,squareY,squareW,squareH); } }
- Ako to, že vo funkcii moveSquare to funguje?
- Volanie repaint totiž neznamená priamo volanie paintComponent v tom okamihu, ale iba akési označenie, čo chcem prekresliť, keď na to bude čas.
Cvičenia 20
- Urobte v hlavnom okne 10 gombíkov, z ktorých každý bude vypisovať svoje číslo. Skúste urobiť v rôznych layoutoch (napr. null, Border, Box, Flow, Grid), aby ste videli, ako sa gombíky umiestňujú.
- Vyskúšajte úlohu aj všeobecne (t.j. bez použitia 10 konkrétnych premenných typu JButton).
- Vytvorte aplikáciu, v ktorej zadáte do textového poľa uhol v stupňoch a vypíše do vedľajšieho textového poľa hodnotu v radiánoch. Vyskúšajte si rovnakú úlohu aj so zadávaním hodnoty do dialógového okna a s odpoveďou v dialógovom okne.
- Vytvorte v hlavnom okne JList, do ktorého budete po stlačení gombíka Add pridávať postupne čísla počínajúc od 0 a po stlačení gombíka Delete vybrané číslo zmaže.
- Vytvorte v hlavnom okne gombík Add a ScrollPane, na ktorom bude Panel s BoxLayoutom. Na tento po každom stlačení Add pribudne ďalší gombík.
DÚ14
Odovzdávanie DÚ14 max. 10 bodov, termín odovzdania streda 10.4. o 22:00.
Cieľom tejto úlohy je precvičiť si prácu s Collections, iterátormi, generickým programovaním a testovaním pomocou JUnit.
V tejto úlohe sa budeme zaoberať Josephusovým problémom: V kruhu stojí n ľudí odsúdených na smrť. Kat začne na určitom mieste a v každom kroku preskočí k-1 ľudí v kruhu a k-teho popraví. Potom sa kruh zmenší a proces pokračuje od miesta, kde sa skočilo. Napríklad, ak ľudia stáli v poradí (A,B,C,D) a k=3, kat najskôr sa preskočí A a B a popraví C, potom preskočí D a A a popraví B, potom preskočí D a A a popraví D a posledný popravený bude A.
Vašou úlohou je
- naprogramovať simuláciu tohto procesu, pričom implementujte triedu JosephusIterator a jej metódy podľa špecifikácie uvedenej v komentároch nižšie.
- napísať aspoň 10 testov v systéme JUnit, ktoré budú testovať rôzne okrajové aj typické prípady použitia rôznych metód triedy JosephusIterator (vrátane hádzania výnimiek tam, kde to komentáre k metódam špecifikujú). Ku každému testu napíšte krátky komentár, aký prípad testuje. Snažte sa pokryť čo najviac situácií, ktoré by mohli byť pre implementácie tejto triedy problematické.
Trieda JosephusIterator reprezentuje špeciálny iterátor, ktorý pri volaní next vždy preskočí k-1 prvkov nejakej Collection a vráti ten ďalší, pričom ak medzitým prišiel na koniec Collection, začne znova od začiatku. Vie tiež vymazať posledný vrátený prvok z Collection. Tento iterátor by mal fungovať pre každú Collection, ktorá má svoj vlastný iterátor s implementovanou metódou remove (napr. ArrayList, LinkedList, TreeSet,...). Samotný Josephusov proces vyraďovania potom pomocou JosephusIteratora simulujte v statickej metóde josephus. Váš program nemusí mať metódu main, stačí použiť JUnit testy.
Odovzdávajte zip súbor s dvoma súbormi: JosephusIterator.java a JosephusIteratorTest.java.
Špecifikácia a kostra triedy JosephusIterator:
package josephus; import java.util.ArrayList; import java.util.Iterator; import java.util.Collection; /** Trieda, ktorá umožňuje iterovať cez Collection tak, * že v každom kroku sa preskočí k-1 prvkov Collection a vráti sa k-ty a * keď príde na koniec, začne znovu od začiatku. * Napr. ak Collection obsahuje zoznam (1,2,3,4) a k=3, * tak prvé volanie next() vráti 3, druhé volanie vráti 2, * tretie 1, štvrté 4, piate opäť 3 a odvtedy * sa situácia cyklicky opakuje donekonečna (za predpokladu, * že nič nebudeme mazať). */ public class JosephusIterator<E> implements Iterator<E> { // VASE PREMENNE /** Konštruktor, ktorý vytvorí Josephusov iterátor pre * Collection c a parameter preskakovania k>=1. */ public JosephusIterator(Collection<E> c, int k) { // VAS KOD } /** Vráti true, ak next() vráti ďalší prvok Collection, * čo je pravda vždy ak Collection nie je prázdna. */ public boolean hasNext() { // VAS KOD } /** Vráti ďalší prvok z Collection, pričom vždy k-1 prvkov preskočí * a keď príde na koniec, pokračuje od začiatku. * @throws NoSuchElementException ak hasNext je false. */ public E next() { // VAS KOD } /** Zmaže prvok z Collection vrátený pomocou predchádzajúceho volania next * @throws IllegalStateException ak next ešte nebola zavolaná alebo ak remove * bola už zavolaná od posledného volania next. */ public void remove() { // VAS KOD } /** Na Collection c simuluje Josephusov proces vyraďovania * a vráti ArrayList prvkov v tom poradí, ako boli vyradení. * Na konci bude Collection c prázdna. */ public static <E> ArrayList<E> josephus(Collection<E> c, int k) { // VAS KOD } }
Cvičenia 21
- Urobte v hlavnom okne panel, ktorý je potomkom JPanel. Na paneli vznikne po kliknutí myšou červený štvorček. Už vykreslené predchádzajúce štvorčeky by nemali zmiznúť.
- Využite informácie o akcii. Pouvažujte nad tým, ako si pozície štvorčekov zachovávať, aby nezmizli ani pri zmene v okne.
- Vytvorte v hlavnom okne aplikácie menu, v ktorom budete vyberať typ a farbu vykresľovaných útvarov. Vyskúšajte viacvrstvové menu a tie viac položiek v základnom Menubare.
- Na vami vytvorenom grafickom paneli vytvorte MouseListener, ktorý celý čas pohybu myšou po paneli vypisuje (do textového popisku - JLabel) súradnice myši.
Prednáška 33
Kreslenie so SWINGom
Opakovanie
- Neexistuje priamo Canvas, ktorý by bol určený čisto na vykresľovanie. Vykresliť však môžeme na rôzne komponenty napríklad na JPanel.
- Vytvoríme si teda vlastný grafický panel MyPanel, ktorý bude rozšírením JPanel.
- Potrebujeme preťažiť jeho funkciu paintComponent - funkcia zodpovedná za vykreslenie/zobrazenie objektu
- Vykreslenie (t.j. zavolanie paintComponent) sa volá vždy, keď sa okno nejakým spôsobom zmení - zmena veľkosti
- Viem ho vyvolať pomocou funkcie repaint
- Volanie repaint však neznamená priamo volanie paintComponent v tom okamihu, ale iba akési označenie, čo chcem prekresliť, keď na to bude čas.
Práca s vykreslenými objektami
- Aby sme vedeli s vykreslenými objektami nejako pracovať potrebujeme vedieť kde máme čo vykreslené.
- Program si upravíme, aby sa dal využiť kúsok všeobecnejšie. Vytovoríme si triedu červený štvorček, ktorá bude vedieť svoju veľkosť a bude sa vedieť vykresliť.
class MyRedSquare{ private int xPos = 50; private int yPos = 50; private int width = 20; private int height = 20; public void setX(int xPos){ this.xPos = xPos; } public int getX(){ return xPos; } public void setY(int yPos){ this.yPos = yPos; } public int getY(){ return yPos; } public int getWidth(){ return width; } public int getHeight(){ return height; } public void paintSquare(Graphics g){ g.setColor(Color.RED); g.fillRect(xPos,yPos,width,height); g.setColor(Color.BLACK); g.drawRect(xPos,yPos,width,height); } } ... private MyRedSquare sq=new MyRedSquare(); ... @Override public void paintComponent(Graphics g) { super.paintComponent(g); sq.paintSquare(g); }
- Teraz máme už rozumnú štruktúru programu
- v MyPanel.paintComponent vykreľujeme všetko, čo potrebujeme vykresliť (pomocou metódy paintXYZ konkrétneho objektu XYZ)
- MyPanel má tiež MouseListener, ktorý pri akcií zavolá metódu move objektu
Viacero vykreslených objektov
- Samozrejme v prípade, že máme objektov viac ja potrebné pri akcii myši rozmýšľať, ktorého objektu sa akcia má týkať.
- Vytvoríme si preto celé pole objektov, ktoré sa budú vykresľovať a pri akcii budeme rozhodovať, či sa vykoná na nejakom objekte alebo na paneli.
class MySquare{ .. public void paintSquare(Graphics g){..} public void changeColor(){..} boolean inSquare(int x, int y){..} } class MyPanel extends JPanel { private ArrayList<MySquare> squares=new ArrayList<MySquare>(); public MyPanel() { setBorder(BorderFactory.createLineBorder(Color.black)); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { boolean wasChanged=false; int index=0; // zisťujem ktorý štvorček má zmeniť farbu for (int i = 0; i < squares.size(); i++) { if (squares.get(i).inSquare(e.getX(),e.getY())){ squares.get(i).changeColor(); wasChanged=true; index=i; } } // pri kliknutí mimo štvorčekov sa pridá nový if (!wasChanged) { squares.add(new MySquare(e.getX(),e.getY(),20,20)); index=squares.size()-1; } MySquare sq=squares.get(index); repaint(sq.getX(),sq.getY(), sq.getWidth()+1, sq.getHeight()+1); } }); @Override public void paintComponent(Graphics g) { super.paintComponent(g); for (int i = 0; i < squares.size(); i++) { MySquare sq=squares.get(i); sq.paintSquare(g); } } }
- Voláme repaint síce iba nejaký výsek z paintComponent, ale prečo to naozaj nevykreslí všetko odznovu? Graphics si pamätá Clip, t.j. časti, ktoré sa majú prekresliť. Keď vyvolám maľovanie, tak sa prekreslí iba to, ktoré je v Clipe.
- Jednoduchým rozšírením vieme napríklad dosiahnuť iba jeden vybraný prvok, s ktorým môžeme potom niečo robiť (vypísať jeho vlastnosti, vymazať...
Rôzne tvary
- Čo keď chcem robiť okrem veľa štvorčekov aj kolieska?
- Môžeme si skopírovať kód pre štvorčeky a upraviť na kolieska - a potom nám prídu trojuholníky
- Alebo sa zamyslím, čo majú štvorčeky a kolieska spoločné.
abstract class MyShape { protected int xPos; protected int yPos; protected int width; protected int height; protected Color col; public MyShape(int x, int y, int width, int height){ ... } public void setX(int xPos){ this.xPos = xPos; } public int getX(){ return xPos; } public void setY(int yPos){ this.yPos = yPos; } public int getY(){ return yPos; } public int getWidth(){ return width; } public int getHeight(){ return height; } abstract public void paint(Graphics g); // ako sa kreslí musí vedieť každý tvar sám public void changeColor(){ ... } boolean inBoundingBox(int x, int y){ ... } } ... private ArrayList<MyShape> shapes=new ArrayList<MyShape>(); ... if (!wasChanged) { // ale ktorý pridávam shapes.add(new MyCircle(e.getX(),e.getY(),20,20)); shapes.add(new MySquare(e.getX(),e.getY(),20,20)); }
Doplňujúce informácie k akcii
Ako pri klinutí získam navyše informácie (napríklad v predchádzajúcom informáciu, či vytváram štvorce alebo kruhy)?
Dopredu nastavené
- Jednoduchá možnosť, ako vedieť, aký typ mám vytvárať je mať v okne nastavenie.
- Na začiatku (bez používateľovej akcie) je nastavený nejaký default.
- Prepínaním gombíka, listu, radio gombíka a pod. môže používateľ toto nastavenie zmeniť
Súčasť akcie
- Ako sme si povedali každý event má niekoľko vlastností ako konkrétne nastal - môžeme teda kontrolovať, či bolo stlačené pravé alebo ľavé tlačítko a podľa toho vybrať akciu
- a čo keď budem mať aj trojuholníky?
Dialóg
- Pri kliknutí na plochu sa môže vyvolať nejaké nové okno alebo dialóg, kde si používateľ vyberie doplňujúce informácie.
- Treba vyriešiť, čo sa stane, ak si nevyberie nič alebo zavrie okno (napr. vtedy môže nastať default - v našom prípade kruh).
private Object[] possibilities = {"circle", "square"}; ... final MyPanel tmp=this; ... addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { ... String s = (String)JOptionPane.showInputDialog( tmp, "Choose the shape", "Shape Dialog", JOptionPane.PLAIN_MESSAGE, null, possibilities, "circle"); if (("square").equals(s)) shapes.add(new MySquare(e.getX(),e.getY(),20,20)); else shapes.add(new MyCircle(e.getX(),e.getY(),20,20)); ...
- Popup menu sa konštruuje podobne ako klasické menu a pomocou neho si môžeme vybrať niektorú položku alebo nastavenie.
- JPopupMenu obsahuje podobne ako klasick0 menu jednotlive JMenuItem, na ktoré môžem jednoducho pripojiť čakanie na akciu - výber tejto položky
final JPopupMenu pop=new JPopupMenu(); JMenuItem menu1=new JMenuItem("Square"); JMenuItem menu2=new JMenuItem("Circle"); pop.add(menu1); pop.add(menu2); menu1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // vybrali sme štvorec } }); menu2.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // vybrali sme kruh } });
- Zavolanie (vyskočenie) popup menu potom vykonáme v MouseListener na MyPanel. Pre popup je potrebné vedieť predka(v našom prípade tento MyPanel) a miesto, kde má okno vyskočiť.
final MyPanel tmp=this; addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { ... pop.show(tmp,e.getX(),e.getY()); ... } }
- Na rozdiel od dialogového okna nás nič nenúti dostať odpoveď (a ani sa na túto odpoveď nečaká), preto je rozumné po vyskočení okna už nepredpokladať ďalšie udalosti závislé od odpovede (tie by mali byť v reakciách na jednotlivý výber z menu)
- Ďalšia vec, ktorá môže byť miernym problémom je potom samotné vytvorenie vzniknutého kruhu/štvorca
- Z predchádzajúceho je jasné, že nemôže toto vytvorenie byť za pop.show()
- Vykonáme to teda v menu.addActionListener(), kde už vieme, čo máme vytvoriť (a že naozaj)
- Na tomto mieste však nevieme, kde máme kruh/štvorec urobiť - musíme na to myslieť dopredu a pozíciu si uložiť
final Point p=new Point(0,0); menu1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { MyShape sq=new MySquare((int)p.getX(),(int)p.getY(),20,20); shapes.add(sq); repaint(sq.getX(),sq.getY(), sq.getWidth()+1, sq.getHeight()+1); } }); ... // v mieste vyskočenia okna pop.show(tmp,e.getX(),e.getY()); p.setLocation(e.getX(),e.getY()); return; ...
JavaFX
Ak chceme vytvoriť aplikáciu s JavaFX musíme na to myslieť už pri vzniku projektu, kedy musíme vybrať JavaFX Application. V opačnom prípade (na rozdiel od SWINGu, ktorý sme mohli vytvoriť z ničoho iba importovaním) sa nám už JavaFX spustiť nepodarí (aj keď napíšeme ten istý kód).
Základné vlastnosti
- Vytvorenie základného okna vo SWINGu sa volalo z main hlavnej triedy pomocou SwingUtilities.invokeLater();, ktorá mala parameter nejakú triedu implementujúcu interface Runnable. Táto trieda vytvorila grafické okno a starala sa o jeho komponenty. Keďže bola zavolaná pomocou invokeLater, bežala vo SWINGovom vlákne.
- JavaFx síce tiež v hlavnej triede aplikácie obsahuje main, avšak pri správnom behu programu k jeho vykonaniu nedôjde. Hlavná trieda totiž musí implementovať interface Application, a tým pádom preťažiť funkciu start.
import javafx.application.Application; import javafx.stage.Stage; public class MyMainClass extends Application { @Override public void start(Stage primarnyStage) { primaryStage.setTitle("Hello World!"); Pane root = new Pane(); ... primaryStage.setScene(new Scene(root, 300, 250)); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
- Znamená to, že my vlastne ďalšie prvky neumiestňujeme do primaryStage (ekvivalent hlavného framu) ani do Scene ale až na prvý panel, ktorý na ňom umiestnime.
Komponenty a ich umiestňovanie
- Podobne ako vo SWINGu existujú tradičné komponenty Label, Button, .. ktoré sa vkladajú na iné komponenty Panel, Pane ..
- Umiestňovanie je oproti SWINGu trochu odlišné a je závislé na type layoutu.
- Všeobecne sa komponent X umiestňuje na komponent Y pomocou Y.getChildren().add(X);
- Pre niektoré rodičovské komponenty (rôzne druhy panelov, ktoré sú pomenované priamo podľa zodpovedajúcich layoutov) však funguje iné pridávanie - napr. pre Y typu BorderPane sa pridáva pomocou Y.setTop(X)
- Pre umiestňovanie s null layoutom sa použije namiesto Pane komponent Group a potom pomocou metód setLayoutX resp. setLayouY nastavíme prvkom súradnice.
Akcie
Podobne ako vo SWINGu akcie sa vykonávajú pomocou Eventov. JavaFX poskytuje niekoľko druhov Eventov napr. DragEvent, KeyEvent, MouseEvent, ScrollEvent, atď.
final Button button = new Button("Button"); button.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent e) { button.setText("Hi"); ((Button)e.getTarget()).setText("Ha"); } });
Event v sebe obsahuje informáciu o type vykonanej akcie, zdroji a cieli akcie.
Kreslenie
Na rozdiel od SWINGu existuje špeciálny Canvas, na ktorý sa v JavaFX kreslí. Na ňom vieme umiestniť grafický obsah,
Canvas canvas = new Canvas(300, 250); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.strokeLine(40, 10, 10, 40); ...
- Tomuto Canvasu vieme samozrejme umiestniť Eventy, na ktoré čaká pomocou canvas.addEventHandler().
- Na rozdiel od nešpecifických Panelov v SWINGu, Canvas je prispôsobený tomu, že naň chceme kresliť a teda podporuje layering a rôzne grafické finty (gradienty, ...).
Dalšou možnosťou je umiestňovanie grafických objektov do normálneho panelu do skupiny (Group), kde s grafickými objektami vieme pracovať ako s ľubovoľnými komponentami (napríklad gombíky a pod.). Aj pri tejto možnosti JavaFX podporuje tieňovanie a dokonca animácie ako vidno v | tutoriáli.
Kompatibilita so SWINGom
- Vytvorenie JavaFX komponentu vo SWINGu
//vo SWINGovom vlakne final JFXPanel fxPanel = new JFXPanel(); frame.add(fxPanel); Platform.runLater(new Runnable() { @Override public void run() { initFX(fxPanel); //táto metóda už je v JavaFX vlákne } });
- Zmeny JavaFX kompomentov však treba robiť v JavaFX vlákne, takže kód pre zmenu týchto komponentov treba obaliť funkciou runLater.
jbutton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Platform.runLater(new Runnable() { @Override public void run() { fxlabel.setText("Swing button clicked!"); } }); } });
- Podobne samozrejme naopak, ak meníme SWINGové komponenty, obalíme kód funkciou invokeLater.
SwingUtilities.invokeLater(new Runnable() { @Override public void run() { //Code to change Swing data. } });
Projekty
Program vykresľovanie
package javaapplication12; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.ArrayList; public class JavaApplication12 { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { createAndShowGUI(); } }); } private static void createAndShowGUI() { JFrame frame = new JFrame("Swing Paint Demo"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel panel = new JPanel(); final MyPanel mypanel = new MyPanel(); JButton delButton = new JButton("Delete"); delButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { mypanel.removeSelectedSquare(); } }); frame.add(panel,BorderLayout.EAST); frame.add(mypanel); panel.add(delButton); frame.pack(); frame.setVisible(true); } } class MySquare{ private int xPos; private int yPos; private int width; private int height; private Color col; public MySquare(int x, int y, int width, int height){ col=Color.RED; this.xPos=x; this.yPos=y; this.height=height; this.width=width; } public void setX(int xPos){ this.xPos = xPos; } public int getX(){ return xPos; } public void setY(int yPos){ this.yPos = yPos; } public int getY(){ return yPos; } public int getWidth(){ return width; } public int getHeight(){ return height; } public void paintSquare(Graphics g){ g.setColor(col); g.fillRect(xPos,yPos,width,height); g.setColor(Color.BLACK); g.drawRect(xPos,yPos,width,height); } public void changeColor(){ if (col==Color.RED) col=Color.BLUE; else col=Color.RED; } boolean inSquare(int x, int y){ if ((x>=this.xPos)&&(x<=this.xPos+this.width)&&(y>=this.yPos)&&(y<=this.yPos+this.height)) return true; else return false; } } class MyPanel extends JPanel { private ArrayList<MySquare> squares=new ArrayList<MySquare>(); private int selected=-1; public int getSelected() { return this.selected; } public MySquare getSelectedSquare() { if ((selected>-1)&&(selected<squares.size())) return this.squares.get(selected); else return null; } public void removeSelectedSquare(){ if ((selected>-1)&&(selected<squares.size())) { MySquare sq=squares.get(selected); squares.remove(selected); repaint(sq.getX(),sq.getY(), sq.getWidth()+1, sq.getHeight()+1); } selected=-1; } public MyPanel() { setBorder(BorderFactory.createLineBorder(Color.black)); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { boolean wasChanged=false; int index=0; int index2=0; for (int i = 0; i < squares.size(); i++) { if (squares.get(i).inSquare(e.getX(),e.getY())){ index=i; index2=selected; if (selected==-1) { squares.get(i).changeColor(); selected=i; } else if (selected==i) { squares.get(i).changeColor(); selected=-1; } else { squares.get(i).changeColor(); squares.get(selected).changeColor(); selected=i; } wasChanged=true; } } if (!wasChanged) { squares.add(new MySquare(e.getX(),e.getY(),20,20)); index=squares.size()-1; } MySquare sq=squares.get(index); repaint(sq.getX(),sq.getY(), sq.getWidth()+1, sq.getHeight()+1); if ((index2>-1)&&(index2<=squares.size()-1)){ sq=squares.get(index2); repaint(sq.getX(),sq.getY(), sq.getWidth()+1, sq.getHeight()+1); } } }); } @Override public Dimension getPreferredSize() { return new Dimension(250,200); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); for (int i = 0; i < squares.size(); i++) { MySquare sq=squares.get(i); sq.paintSquare(g); } } }
Cvičenia 22
Na týchto cvičeniach sa vrátime k stromom a rekurzii, ale pripomenieme si aj Collections a výnimky. Nižšie nájdete triedu Node reprezentujúcu vrchol binárneho stromu, ktorý obsahuje okrem referencií na ľavý a pravý podstrom aj dátovú premennú typu int. Všimnite si, že po vytvorení vrchola ho už nie je možné existujúcimi metódami meniť.
Úloha 1
Napíšte metódu static Node generate(int n), ktorá vygeneruje najväčší možný strom s týmito vlastnosťami:
- V koreni má uložené číslo 0.
- Ak má nejaký vrchol ľavého syna, v tomto synovi je uložené číslo 1.
- Ak má nejaký vrchol pravého syna, v tomto synovi je uložené číslo 2.
- Súčet čísel vo vrcholoch na každej ceste z koreňa do niektorého listu je n.
Jednotlivé cesty z koreňa do listov teda zodpovedajú všetkým spôsobom, ako zapísať číslo n ako súčet jednotiek a dvojok, pričom záleží na poradí. Napr. pre n=2 bude strom vyzerať takto:
0 / \ 1 2 / 1
Návod: použite rekurziu, možno budete potrebovať pomocnú rekurzívnu metódu s ďalšími parametrami.
Úloha 2
Napíšte triedu PreorderTreeIterator, ktorá bude implementovať interface Iterator<Node>. Metóda remove vyhodí výnimku typu UnsupportedOperationException. Metóda next by mala postupne vracať všetky vrcholy stromu v prefixovom poradí a metóda hasNext by mala testovať, či je ešte nejaký nenavštívený vrchol. Konštruktor triedy dostane koreň stromu.
Návod: V iterátore si vytvorte zásobník vrcholov, ktoré ešte treba spracovať. Použite na to generickú triedu LinkedList, ktorá má metódy push a pop.
Overte, že nasledujúci kód vypíše to isté ako volanie metódy tree.outPrefix():
PreorderTreeIterator it = new PreorderTreeIterator(tree); while (it.hasNext()) { System.out.print(" " + it.next().getData()); }
Úloha 3
Aj keď trieda Node reprezentuje vrchol stromu, nie je zaručené, že ide skutočne o strom. Napríklad nasledujúcimi dvoma príkazmi môžeme spraviť strom t1, v ktorom je vrchol t0 použitý dvakrát, ako ľavé aj pravé dieťa t1:
Node t0 = new Node(0, null, null); Node t1 = new Node(1, t0, t0);
Napíšte metódu static boolean uniqueNodes(Node tree), ktorá vráti true, ak v danom strome nie je žiaden vrchol použitý viackrát.
Návod: prejdite strom (rekurzívne alebo iterátorom) a ukladajte si navštívené vrcholy napr. do HashSet. Nakoľko sme v triede Node nepreťažili metódu hashCode, vrcholy budú porovnávané len podľa adresy v pamäti, čo je v tomto prípade žiadané správanie.
Úloha 4
Zmeňte triedu Node tak, aby nebolo možné jeden vrchol použiť ako dieťa viacerých vrcholov, čím sa zabráni prípadom ako v predchádzajúcej úlohe. Pri volaní konštruktoru, ktoré by k takejto situácii viedlo, konštruktor vyhodí výnimku.
Návod: vrchol si pamätá svojho otca, na vhodných miestach sa táto informácia kontroluje a mení.
Úloha 5
Vytvorte aplikáciu, ktorá vykreslí jeden štvorček. Po kliknutí naň sa štvorček buď posunie, na miesto, kde sa najbližšie klikne, alebo zmaže alebo mu zmení farba. Vyskúšajte výber akcie urobiť rôznym spôsobom. Pri presúvaní štvorčeka si dajte pozor, aby sa akcie, ktoré nasledujú (napr. očakávanie nového klinutia) vykonávali na vhodnom mieste - t.j. aby nebol problém, ak sa používateľ správa nevhodne napr. zavrie dialóg bez výberu.
Trieda Node
class Node { private int data; private Node left, right; public Node(int data_, Node left_, Node right_) { data = data_; left = left_; right = right_; } public int getData() { return data; } public Node getLeft() { return left; } public Node getRight() { return right; } /** Vypise v prefixovom poradi */ public void outPrefix() { if (this != null) { System.out.print(" " + data); if (this.left != null) { left.outPrefix(); } if (this.right != null) { right.outPrefix(); } } } }
DÚ15
Odovzdávanie DÚ15 max. 20 bodov, termín odovzdania streda 24.4. o 22:00.
Cieľom tejto úlohy je precvičiť si tvorbu GUI a prácu so SWINGom.
Úlohou je vytvoriť grafické prostredie pre vytváranie a manipuláciu s lomenými čiarami.
Popis prostredia
V úvodnom okne bude menu, grafický panel, zoznam vrcholov a štyri gombíky ako na obrázku.
- V grafickom okne budeme zobrazovať lomené čiary (vrcholy aj úsečky medzi nimi).
- Vrcholy vznikajú klikaním myšou na grafickú plochu. Ak používateľ klikne na prázdne miesto na ploche, pridá tým nový posledný vrchol lomenej čiary.
- Zoznam vrcholov obsahuje súradnice jednotlivých zlomov na lomenej čiare. Vybraný vrchol zo zoznamu (ak taký je) bude mať inú farbu ako zvyšné vrcholy.
- Vybrať vrchol možno aj klinutím naň v grafickej ploche.
- Pomocou tlačítok HORE(^) a DOLE(v) je možné meniť poradie vrcholov na lomenej čiare - vybraný vrchol sa v zozname posunie vyššie alebo nižšie, čo ovplyvní aj následné vykreslenie lomenej čiary.
- Tlačidlo DELETE vybraný vrchol zo zoznamu aj z lomenej čiary zruší.
- Tlačidlo MOVE je na posúvanie vrcholov. Po jeho stlačení sa tlačidlo zmení na tlačidlo Cancel, ktoré posúvanie vrchola zruší.
- Vybraný vrchol sa presunie na súradnicu, kam používateľ klikne myšou. Kým toto používateľ neurobí, ostatné akcie (vytváranie vrcholov, ich vyberanie a podobne) ostávajú zablokované.
- Menu má položky New, Open, Save a Exit.
- New otvorí dialógové okno a umožní vybrať N (0..12) a vytvorí v grafickom paneli vodorovnú čiaru s N vrcholmi v rovnakých odstupoch.
- Open otvorí súbor polyline.txt, v ktorom sú súradnice vrcholov nejakej lomenej čiary. Túto zobrazíme v okne a vypíšeme do zoznamu.
- Vstupný súbor má na začiatku N (počet vrcholov na lomenej čiare) a na každom ďalšom riadku dvojicu súradníc vrchola.
- Save uloží do súboru polyline.txt aktuálne vytvorenú lomenú čiaru.
- Exit ukončí program.
Podmienky
Program si musí vedieť poradiť aj s nesprávnymi vstupmi od používateľa (alebo zabezpečiť, aby nenastali). Príkladmi takýchto potenciálnych problémov môžu byť napríklad:
- Vybratie prvého vrchola a jeho posun hore (alebo naopak posledného dolu)
- Zmazanie/posúvanie bez vybraného vrchola
- Zlý formát vstupného súboru
- Pokus niečo robiť počas presunu vrchola
- A iné...
Odovzdávanie
- Odovzdávajte jeden .zip súbor obsahujúci adresár gui a v ňom všetky zdrojové súbory (*.java) vášho balíčka.
- Odporúčame nerobiť všetko v jednom súbore, ale pre každú významnejšiu triedu urobiť samostatný súbor.
Prednáška 34
Organizačné poznámky
- Tento týždeň prednáška aj cvičenia podľa bežného rozvrhu, zverejnenie DÚ16
- Budúci týždeň utorok dekanské voľno (študentská vedecká konferencia), cvičenia v stredu budú. Termín odovzdania DÚ15.
- Týždeň 29.4.-5.5. v stredu sviatok, prednáška v utorok
- Týždeň 6.-12.5. v stredu sviatok, prednáška v utorok, náhradné cvičenia v pondelok, termín DÚ16, prihlásenie sa na projekt
- Týždeň 13.-19.5. posledný týždeň semestra, prednáška aj cvičenia podľa bežného rozvrhu
- Týždeň 20.-26.5. riadny termín písomky, odovzdávanie nepovinných projektov
Študentská vedecká konferencia
- Študenti prezentujú výsledky svojho výskumu
- Hlavne diplomovky a doktorandi, ale aj bakalárske práce prípadne iné projekty
- Na konferencii resp. na posteroch zistíte, čo robia školitelia, môže Vám to pomôcť nájsť si školiteľa bakalárky, príp. aj ročníkového projektu
Úvod
- Zvyšok semestra sa budeme venovať práci s grafmi
- Grafy poznáte z predmetu Úvod do kombinatoriky a teórie grafov
- Ďalšie algoritmy pre grafy v treťom ročníku na predmete Tvorba efektívnych algoritmov
- Ďalšia teória grafov + nejaké algoritmy povinne voliteľný predmet pre tretí ročník Teória grafov
Príklady využitia grafov
- Mapa, cestná sieť: vrcholy sú križovatky, mestá, obce, hrany sú cesty (podobne železnice, letecká sieť, ulice v rámci mesta ...)
- Počítačové siete, elektrické obvody, siete potrubí
- Web: vrcholy sú webstránky, hrany sú odkazy (podobne napríklad vedecké články a citácie medzi nimi)
- Sociálne siete: vzťahy, kontakty medzi ľudmi, šírenie správ
- Závislosti medzi činnosťami: ak máme vykonať niekoľko činností, ale niektoré sa dajú vykonať iba ak sú iné už hotové, ako ich usporiadať
- Preferencie: napr. pri tvorbe rozvrhu sa určitý predmet môže konať len v určité časy, môžeme spájať hranou predmety a časy
Anglická terminológia:
- graf = graph, vrchol = vertex (mn.č. vertices), hrana = edge
Reprezentácia grafov
Na dnešnej prednáške budeme uvažovať neorientovaný graf s vrcholmi očíslovanými 0,1,...,n-1.
- Príklad: V={0,...,6}, E={{0,5},{1,2},{2,3},{1,3},{3,4}}
Do akých dátových štruktúr takýto graf uložíme?
- V našom prípade množinu vrcholov reprezentujeme jednoduchom počtom vrcholov n, zostáva uložiť hrany
Zoznam hrán
- Najjednoduchšia reprezentácia je zoznam hrán
- Vytvoríme si pomocnú triedu pre hranu, ktorá obsahuje čísla jej koncových vrcholov
- Všetky hrany uložíme do poľa alebo spájaného zoznamu (ArrayList, LinkedList)
(0,5), (1,2), (2,3), (1,3), (3,4)
Matica susednosti (adjacency matrix)
- Matica nxn napr typu boolean
- Políčko a[i][j]=true ak {i,j} je hrana
- Pre neorientovaný graf symetrická matica
0 1 2 3 4 5 6 0 F F F F F T F 1 F F T T F F F 2 F T F T F F F 3 F T T F T F F 4 F F F T F F F 5 T F F F F F F 6 F F F F F F F
Zoznamy susedov (adjacency lists)
- Pre každý vrchol zoznam jeho susedov
- Uložíme ako pole potrebnej veľkosti alebo spájaný zoznam (ArrayList, LinkedList)
0: 5 1: 2,3 2: 1,3 3: 1,2,4 4: 3 5: 0 6:
Graf ako abstraktný dátový typ: Interface Graph
- Ukážeme si konkrétne implementácie pre maticu a zoznamy susedov, potrebujeme však vedieť, aké operácie by mal graf poskytovať.
- Tu je jednoduchý interface pre graf, do ktorého vieme pridávať hrany, testovať či hrana existuje a prechádzať cez susedov určitého vrcholu
/** Interface reprezentujúci graf s n vrcholmi očíslovanými 0..n-1. */ interface Graph { /** Vráti počet vrcholov grafu n. */ int getNumberOfVertices(); /** Vráti počet hrán grafu. */ int getNumberOfEdges(); /** Do grafu pridá hranu z vrcholu from do vrcholu to, * vráti true ak sa ju podarilo pridať. */ boolean addEdge(int from, int to); /** Vráti true, ak existuje hrana z vrcholu from * do vrcholu to. */ boolean existsEdge(int from, int to); /** Vráti iterovateľnú skupinu susedov vrchola vertex. * V prípade orientovaného grafu vracia iba hrany vychádzajúce z vrchola. * Metóda remove iterátora nemusí byť podporovaná. */ Iterable<Integer> adjVertices(int vertex); }
- adjVertices vráti Iterable, čo je interface s jedinou predpísanou metódou iterator(), ktorá vráti iterátor cez ten istý typ
- Objekty typu Iterable sa dajú použiť vo for-cykle typu for(int v : g.adjVertices(u))
Príklad použitia: vypísanie grafu v poradí počet vrcholov, počet hrán a potom zoznam hrán, každá daná koncovými vrcholmi:
/** Graph g vypíše do výstupného streamu */ static void printGraph(Graph g, PrintStream out) { int n = g.getNumberOfVertices(); out.println(n + " " + g.getNumberOfEdges()); for (int u = 0; u < n; u++) { for(int v : g.adjVertices(u)) { if (u < v) { // kvoli symetrii v neorientovaných grafoch out.println(u + " " + v); } } } }
Napr. pre graf vyššie
7 5 0 5 1 2 1 3 2 3 3 4
Implementácia pomocou zoznamov susedov: Trieda AdjListsGraph
- Pre každý vrchol si udržiavame zoznam susedných vrcholov v ArrayListe
- Ako iterátor cez susedov použijeme jednoducho iterátor z ArrayListu
/** Trieda reprezentujúca neorientovaný graf pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class AdjListsGraph implements Graph { /** Zoznam susedov pre každý vrchol */ private ArrayList<ArrayList<Integer>> adjLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<ArrayList<Integer>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<Integer>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { adjLists.get(from).add(to); // pridaj hranu v oboch smeroch adjLists.get(to).add(from); numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); // pozri do zoznamu susedov pre from } @Override public Iterable<Integer> adjVertices(int vertex) { return adjLists.get(vertex); // vrati ArrayList } }
Implementácia pomocou matice susednosti: Trieda AdjMatrixGraph
- V konštruktore vyrobíme maticu vyplnenú false
- O niečo zložitejšia metóda adjVertices
- Dal by sa spraviť aj iterátor vlastnou triedou bez použitia pomocného poľa
/** Trieda reprezentujúca neorientovaný graf pomocou matice susednosti. */ class AdjMatrixGraph implements Graph { /** Matica susednosti */ private boolean[][] matrix; /** Počet hrán grafu */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjMatrixGraph(int numVertices) { matrix = new boolean[numVertices][numVertices]; for (int i = 0; i < numVertices; i++) { for (int j = 0; j < numVertices; j++) { matrix[i][j] = false; } } numEdges = 0; } @Override public int getNumberOfVertices() { return matrix.length; } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { // nepridava existujuce hrany return false; } matrix[from][to] = true; //prida hranu v oboch smeroch matrix[to][from] = true; numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return matrix[from][to]; } @Override public Iterable<Integer> adjVertices(int vertex) { // vytvori pomocne pole a, // vlozi do neho vsetkych susedov a vrati jeho iterator ArrayList<Integer> a = new ArrayList<Integer>(); for (int i = 0; i < matrix[vertex].length; i++) { if (matrix[vertex][i]) { a.add(i); } } return a; } }
Vytvorenie grafu
- Vytvoríme prázdny graf s určitým počtom vrcholov, po jednom pridávame hrany
/** Zo scannera s načíta graf vo formáte: počet vrcholov, * počet hrán a zoznam hrán zadaných koncami vrcholov. * Ak je matrix true, uloží ho ako AdjMatrixGraph, * inak ako AdjListsGraph. */ static Graph readGraph(Scanner s, boolean matrix) { int n = s.nextInt(); int m = s.nextInt(); Graph g; if (matrix) { g = new AdjMatrixGraph(n); } else { g = new AdjListsGraph(n); } for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; }
Porovnanie reprezentácií
- Majme graf s n vrcholmi a m hranami
- Počet hrán m môže byť od 0 po n(n-1)/2
- Vyjadríme čas rôznych operácií v O-notácii:
- O(1): robíme len konštantný počet operácií bez ohľadu na veľkosť grafu
- O(n): čas operácie rastie v najhoršom prípade lineárne s počtom vrcholov grafu
- O(m): čas operácie rastie v najhoršom prípade lineárne s počtom hrán grafu
- Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} : čas operácie rastie v najhoršom prípade kvadraticky s počtom vrcholov grafu
- Väčšinou máme viac hrán ako vrcholov, takže O(1) je lepšie ako O(n), to je lepšie ako O(m) a to je lepšie Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)}
Zoznam hrán | Matica susednosti | Zoznamy susedov | |
Pamäť | O(m) | Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} | O(n+m) |
Vytvoriť graf bez hrán | O(1) | Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} | O(n) |
addEdge | O(1) | O(1) | O(1) |
existsEdge | O(m) | O(1) | O(n) |
Prejdenie susedov vrchola | O(m) | O(n) | O(stupeň) |
Výpis grafu pomocou adjVertices | O(nm) | Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle O(n^2)} | O(n+m) |
- Matica susednosti
- Rýchla operácia existsEdge
- Ak je graf riedky (málo hrán), zaberá zbytočne veľa pamäte a dlho trvá prejdenie susedov vrchola
- Zoznamy susednosti
- Vhodný aj pre riedke grafy
- Dlho trvá nájdenie konkrétnej hrany, ale všetkých susedov vrchola vieme prejsť rýchlo
- Najvhodnejšia reprezentácia na väčšinu algoritmov, ktoré uvidíme
Poznámky a obmeny
Orientované grafy
- Ak v metóde addEdge pridáme hranu iba jedným smerom, dostaneme orientovaný graf
- adjIterator vráti iterátor cez vychádzajúce hrany
Grafy s násobnými hranami
- AdjMatrixGraph nepovoľuje násobné hrany
- Ak chceme prirobiť, museli by sme v matici mať int-y vyjadrujúce násobnosť hrany
- AdjListsGraph nekontroluje násobnosť hrany, dovoľuje opakovanie
- Kontrola násobnosti by dlho trvala - pri addEdge by sme museli pozrieť, či už v poli nie je
Metóda remove v iterátore pre adjVertices
- g.adjVertices(int vertex).iterator() je iterátor cez prvky typu Integer
- Iterátor špecifikuje nepovinnú metódu remove na mazanie prvkov
- Ak nie je implementovaná, mala by hádzať výnimku
- V našom prípade sa dá spustiť, ale nerobí to, čo by sme chceli
- Preto ju nie je vhodné používať
- Takýto stav však nie je ideálny, ľahko vediet k chybám
- Náprava: adjVertices by mohol vracať našu vlastnú Iterable triedu, ktorá vracia náš vlastný iterátor, ktorý remove nedovoľuje (alebo ho robí poriadne)
Ohodnotené grafy
- V grafe si pre jednotlivé vrcholy a hrany môžeme chcieť pamätať ďalšie dáta
- Napr. ak modelujeme cestnú sieť, hrana môže mať určitú dĺžku
- Vrcholy môžu mať mená, súradnice a pod.
- Nabudúce pridáme takéto dáta do implementácie
Dynamické grafy
- Náš graf možno meniť len pridaním hrán
- V grafe môžeme chcieť robiť aj iné zmeny
- Mazanie hrán by sa ľahko doplnilo, rýchle pre maticu, pomalšie pre zoznamy susedov
- Pridávanie a mazanie vrcholov ťažšie -- potrebujeme číslovanie 0..n-1
- Neskôr si ukážeme všeobecnejšiu štruktúru, ktorá to umožňuje
Súvislosť grafu, prehľadávanie do hĺbky (depth-first search, DFS)
- Zameriame sa na zisťovanie, či sú dva vrcholy spojené v grafe cestou, t.j. či sú v tom istom komponente súvislosti (v neorientovanom grafe)
- Napr. vrchol 1 je v komponente súvislosti s vrcholmi 2,3,4
- Použijeme prehľadávanie do hĺbky, podobné ako sme použili na vyfarbovanie súvislých oblastí v matici (ostrovy)
- Mapu ostrovov môžeme zapísať ako graf, pričom každé políčko ostrovu bude vrchol a dve políčka budú spojené hranou, ak spolu susedia
- Ostrovy sú potom komponenty súvislosti
Existuje cesta z u do v?
- Poďme teda zisťovať, či sú u a v v tom istom komponente
- Rekurzívne prehľadávame začínajúc od vrcholu u
- Vytvoríme si pole booleanov visited, v ktorom si značíme už navštívené vrcholy
- Z každého vrcholu rekurzívne prehľadáme zatiaľ nenavštívených susedov
/** Pomocná metóda pre metódu connected. * Dostane graf g, vrchol vertex * a pole s poznačenými navštívenými vrcholmi, pričom * visited[vertex] by malo byť false. * Rekurzívne prehľadá nenavštívené vrcholy, ktoré sa z * vrcholu vertex dajú dosiahnuť. */ static void search(Graph g, int vertex, boolean[] visited) { visited[vertex] = true; for(int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (!visited[neighbor]) { search(g, neighbor, visited); // navstivime ho rekurzivne } } } /** Metóda, ktorá zistí, či sú vrcholy from a to v grafe g * spojené cestou. */ static boolean connected(Graph g, int from, int to) { // vytvor pole visited vyplnené false boolean[] visited = new boolean[g.getNumberOfVertices()]; for (int i = 0; i < visited.length; i++) { visited[i] = false; } search(g, from, visited); // zavolaj rekurziu return visited[to]; // dostali sme sa do vrchola to? }
Hľadanie komponentov súvislosti
- Ak by sme chceli testovať spojenie medzi veľa dvojicami vrcholov, oplatí sa nám nájsť komponenty súvislosti v celom grafe naraz.
- Komponenty očísľujeme 0,1,...,k-1. Pre každý vrchol máme v poli číslo jeho komponentu.
- Potom dva vrcholy sú spojené cestou práve vtedy, keď majú rovnaké číslo komponentu.
- Toto robí nasledujúca trieda
/** Trieda obsahujúca rozdelenie vrcholov grafu do komponentov súvislosti */ class Components { /** Pre kazdy vrchol cislo jeho komponentu 0..pocet komponentov-1 */ private int[] componentId; /** pocet komponentov grafu */ private int numComponents; /** samotny graf */ private Graph g; /** Konstruktor, ktory dostane graf a prehladavanim do hlbky * hlada komponenty suvislosti */ public Components(Graph g) { this.g = g; // uloz graf numComponents = 0; // inicializuj pocet koponentov int n = g.getNumberOfVertices(); // pocet vrcholov grafu // vytvor pole cisel koponentov a inicializuj na -1 - nevyfarbene componentId = new int[n]; for (int i = 0; i < n; i++) { componentId[i] = -1; } // prechadzaj cez vrchola a ak najdes nevyfarbeny, spusti prehladavanie for (int i = 0; i < n; i++) { if (componentId[i] == -1) { search(i, numComponents); // vyfarbi cislom numComponents numComponents++; // zvys numComponents } } } /** Pomocna rekurzivna metoda pouzivana v konstruktore na vyfarbenie * jedneho komponentu cislom id. */ private void search(int vertex, int id) { componentId[vertex] = id; for(int neighbor : g.adjVertices(vertex)) { if (componentId[neighbor] == -1) { search(neighbor, id); // navstivime ho rekurzivne } } } /** Vrati true ak vrchol from a to su v tom istom koponente */ public boolean areConnected(int from, int to) { return componentId[from] == componentId[to]; } /** Vrati pocet kponentov grafu */ public int getNumberOfComponents() { return numComponents; } }
- Čas výpočtu pri použití zoznamov susedov je O(n+m)
- Vedeli by sme triedu obohatiť aj o iterovanie cez vrcholy v jednotlivých komponentoch
Príklad použitia prehľadávania do hĺbky: valec lesom
- Ukážeme si trochu netradičný príklad, v ktorom sa dajú grafy a prehľadávanie použiť
- Máme daný obdĺžnikový pozemok obohnaný plotom, na ktorom rastú stromy, ktoré si predstavíme ako body rovine
- Pri západnom okraji pozemku stojí valec s polomerom r (t.j. v rovine si ho predstavíme ako kruh), pričom pri západnej strane plotu je dosť miesta, aby popri nej valec prešiel od severu na juh
- Cieľom je zistiť, či sa valec dá pretlačiť pomedzi stromy na východnú stranu pozemku
Na prvý pohľad geometrická úloha sa prevedie na grafovú takto:
- Vytvoríme vrcholy S a J reprezentujúce severnú a južnú stranu pozemku
- Vytvoríme tiež vrchol pre každý strom
- Vrcholy pre dva stromy spojíme hranou, ak sú bližšie ako 2r, t.j. valec sa medzi ne neprepchá
- Podobne vrchol pre strom spojíme hranou s vrcholom S alebo J, ak je k príslušnému okraju pozemku bližšie ako 2r.
- Ak S a J sú v tom istom komponente súvislosti, valec nie je možné cez les prepchať, lebo existuje lomená čiara spájajúca stromy na ceste z S do J, cez ktorú valec nevie prejsť
- Ak S a J nie sú v tom istom komponente súvislosti, valec môžeme posúvať po južnej strane komponentu obsahujúceho vrchol S
- Na odpoveď, či sa dá valec presunúť lesom, teda stačí spustiť connected(g,S,J).
Zhrnutie
- Grafy sa používajú na veľa problémov
- Reprezentujeme ich ako zoznamy susedov, prípadne ako maticu susednosti
- V neorientovanom grafe vieme prehľadávaním do hĺbky nájsť komponenty súvislosti
- Ako je to v orientovaných grafoch?
- Celý program (vrátane prehľadávania do šírky z ďalšej prednášky) pozri nižšie
Zdrojový kód programu, grafy
Program k prednáškam: #Prednáška 34 a #Prednáška 35
- dve reprezentácie grafu, prehľadávanie do hĺbky a do šírky
package prog; import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.Scanner; /** Interface reprezentujúci graf s n vrcholmi očíslovanými 0..n-1. */ interface Graph { /** Vráti počet vrcholov grafu n. */ int getNumberOfVertices(); /** Vráti počet hrán grafu. */ int getNumberOfEdges(); /** Do grafu pridá hranu z vrcholu from do vrcholu to, * vráti true ak sa ju podarilo pridať. */ boolean addEdge(int from, int to); /** Vráti true, ak existuje hrana z vrcholu from * do vrcholu to. */ boolean existsEdge(int from, int to); /** Vráti iterovateľnú skupinu susedov vrchola vertex. * V prípade orientovaného grafu vracia iba hrany vychádzajúce z vrchola. * Metóda remove iterátora nemusí byť podporovaná. */ Iterable<Integer> adjVertices(int vertex); } /** Trieda reprezentujúca neorientovaný graf pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class AdjListsGraph implements Graph { /** Zoznam susedov pre každý vrchol */ private ArrayList<ArrayList<Integer>> adjLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<ArrayList<Integer>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<Integer>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { adjLists.get(from).add(to); // pridaj hranu v oboch smeroch adjLists.get(to).add(from); numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); // pozri do zoznamu susedov pre from } @Override public Iterable<Integer> adjVertices(int vertex) { return adjLists.get(vertex); // vrati ArrayList } } /** Trieda reprezentujúca neorientovaný graf pomocou matice susednosti. */ class AdjMatrixGraph implements Graph { /** Matica susednosti */ private boolean[][] matrix; /** Počet hrán grafu */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjMatrixGraph(int numVertices) { matrix = new boolean[numVertices][numVertices]; for (int i = 0; i < numVertices; i++) { for (int j = 0; j < numVertices; j++) { matrix[i][j] = false; } } numEdges = 0; } @Override public int getNumberOfVertices() { return matrix.length; } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { // nepridava existujuce hrany return false; } matrix[from][to] = true; //prida hranu v oboch smeroch matrix[to][from] = true; numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return matrix[from][to]; } @Override public Iterable<Integer> adjVertices(int vertex) { // vytvori pomocne pole a, // vlozi do neho vsetkych susedov a vrati jeho iterator ArrayList<Integer> a = new ArrayList<Integer>(); for (int i = 0; i < matrix[vertex].length; i++) { if (matrix[vertex][i]) { a.add(i); } } return a; } } /** Trieda obsahujúca rozdelenie vrcholov grafu do komponentov súvislosti */ class Components { /** Pre kazdy vrchol cislo jeho komponentu 0..pocet komponentov-1 */ private int[] componentId; /** pocet komponentov grafu */ private int numComponents; /** samotny graf */ private Graph g; /** Konstruktor, ktory dostane graf a prehladavanim do hlbky * hlada komponenty suvislosti */ public Components(Graph g) { this.g = g; // uloz graf numComponents = 0; // inicializuj pocet koponentov int n = g.getNumberOfVertices(); // pocet vrcholov grafu // vytvor pole cisel koponentov a inicializuj na -1 - nevyfarbene componentId = new int[n]; for (int i = 0; i < n; i++) { componentId[i] = -1; } // prechadzaj cez vrchola a ak najdes nevyfarbeny, spusti prehladavanie for (int i = 0; i < n; i++) { if (componentId[i] == -1) { search(i, numComponents); // vyfarbi cislom numComponents numComponents++; // zvys numComponents } } } /** Pomocna rekurzivna metoda pouzivana v konstruktore na vyfarbenie * jedneho komponentu cislom id. */ private void search(int vertex, int id) { componentId[vertex] = id; for (int neighbor : g.adjVertices(vertex)) { if (componentId[neighbor] == -1) { search(neighbor, id); // navstivime ho rekurzivne } } } /** Vrati true ak vrchol from a to su v tom istom koponente */ public boolean areConnected(int from, int to) { return componentId[from] == componentId[to]; } /** Vrati pocet kponentov grafu */ public int getNumberOfComponents() { return numComponents; } } /** Trieda, ktorá reprezentuje najkratšie cesty a vzdialenosti * z jedného vrchola do ostatných. */ class ShortestPaths { /** Graf, v ktorom ratame cesty */ private Graph g; /** Startovaci vrchol, z ktoreho ratame najkratsie cesty */ private int start; /** Pre kazdy vrchol v grafe vzdialenost od startu * alebo -1 ak je v inom komponente. */ private int[] dist; /** Pre kazdy vrchol u predchadzajuci vrchol na ceste zo start do u */ private int[] prev; /** Konstruktor, ktory dostane graf a startovaci vrchol a najde najkratsei cesty */ public ShortestPaths(Graph g, int start) { this.g = g; this.start = start; int n = g.getNumberOfVertices(); // inicializacia poli - vyplnime -1 dist = new int[n]; prev = new int[n]; for (int i = 0; i < n; i++) { dist[i] = -1; prev[i] = -1; } // prehladavanie do sirky // vytvorime rad a vlozime do neho startovaci vrchol LinkedList<Integer> queue = new LinkedList<Integer>(); queue.addLast(start); dist[start] = 0; // sam od seba ma vzdialenost 0 while (!queue.isEmpty()) { // vyberieme prvok na zaciatku radu int vertex = queue.removeFirst(); // prejdeme cez jeho susedov a ak este neboli navstiveni // urcime im vzdialenost a pridame ich do radu for (int neighbor : g.adjVertices(vertex)) { if (dist[neighbor] < 0) { dist[neighbor] = dist[vertex] + 1; prev[neighbor] = vertex; queue.addLast(neighbor); } } } } /** Je vrchol vertex spojeny so startovacim vrcholom? */ public boolean isConnected(int vertex) { return dist[vertex] >= 0; } /** Vrati vzdialenost vrcholu vertex od starovacieho vrcholu. * Ak su v roznych komponentoch, vrati -1. */ public int distance(int vertex) { return dist[vertex]; } /** Vrati najkratsiu cestu zo startovacieho vrcholu * do vrcholu vertex (postupnost vrcholov, cez ktore cesta ide). * Ak su v roznych komponentoch, vrati null. */ public int[] shortestPath(int vertex) { if (!isConnected(vertex)) { // vybav rozne koponenty return null; } int[] path = new int[dist[vertex] + 1]; // alokujeme cestu int v = vertex; //posledny vrchol bude vertex path[dist[vertex]] = v; for (int i = dist[vertex] - 1; i >= 0; i--) { // odzadu pridavame vrcholy v = prev[v]; // posunieme sa na predchadzajuci vrchol na ceste path[i] = v; } return path; } } public class Prog { /** Zo scannera s načíta graf vo formáte: počet vrcholov, * počet hrán a zoznam hrán zadaných koncami vrcholov. * Ak je matrix true, uloží ho ako AdjMatrixGraph, * inak ako AdjListsGraph. */ static Graph readGraph(Scanner s, boolean matrix) { int n = s.nextInt(); int m = s.nextInt(); Graph g; if (matrix) { g = new AdjMatrixGraph(n); } else { g = new AdjListsGraph(n); } for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; } /** Graph g vypíše do výstupného streamu */ static void printGraph(Graph g, PrintStream out) { int n = g.getNumberOfVertices(); out.println(n + " " + g.getNumberOfEdges()); for (int u = 0; u < n; u++) { for (int v : g.adjVertices(u)) { if (u < v) { // kvoli symetrii v neorientovaných grafoch out.println(u + " " + v); } } } } /** Pomocná metóda pre metódu connected. * Dostane graf g, vrchol vertex * a pole s poznačenými navštívenými vrcholmi, pričom * visited[vertex] by malo byť false. * Rekurzívne prehľadá nenavštívené vrcholy, ktoré sa z * vrcholu vertex dajú dosiahnuť. */ static void search(Graph g, int vertex, boolean[] visited) { visited[vertex] = true; for (int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (!visited[neighbor]) { search(g, neighbor, visited); // navstivime ho rekurzivne } } } /** Metóda, ktorá zistí, či sú vrcholy from a to v grafe g * spojené cestou. */ static boolean connected(Graph g, int from, int to) { // vytvor pole visited vyplnené false boolean[] visited = new boolean[g.getNumberOfVertices()]; for (int i = 0; i < visited.length; i++) { visited[i] = false; } search(g, from, visited); // zavolaj rekurziu return visited[to]; // dostali sme sa do vrchola to? } public static void main(String[] args) throws FileNotFoundException { Scanner s; Graph g; PrintStream out; // nacitame graph ako maticu susedov a vypiseme s = new Scanner(new File("graph-in.txt")); g = readGraph(s, true); s.close(); out = new PrintStream("graph-out1.txt"); printGraph(g, out); out.close(); // nacitame graf ako zoznamy susedov a vypiseme s = new Scanner(new File("graph-in.txt")); g = readGraph(s, false); s.close(); out = new PrintStream("graph-out2.txt"); printGraph(g, out); out.close(); // ratame pre dvojice vrcholov ci su spojene jednorazovou funkciou System.out.println("Are 1 and 4 connected? " + connected(g, 1, 4)); System.out.println("Are 0 and 1 connected? " + connected(g, 0, 1)); // predpocitame komponenty, potom zistime ich pocet a testujeme spojitost Components comp = new Components(g); System.out.println("The number ofconnected components: " + comp.getNumberOfComponents()); System.out.println("Are 1 and 4 connected? " + comp.areConnected(1, 4)); System.out.println("Are 0 and 1 connected? " + comp.areConnected(0, 1)); // najdeme najkratsie vzdialenosti z 1 do ostatnych vrcholov ShortestPaths paths = new ShortestPaths(g, 1); System.out.println("Are 1 and 4 connected? " + paths.isConnected(4)); System.out.println("Distance of 1 and 4: " + paths.distance(4)); System.out.println("Shortest path from 1 to 4: " + Arrays.toString(paths.shortestPath(4))); } }
Cvičenia 23
- Nižšie je verzia prehľadávania do hĺbky, ktorá do poľa whenVisited čísluje vrcholy v poradí, v akom boli navštívené. Odsimulujte algoritmus na grafe nakreslenom vpravo a zistite, v akom poradí budú vrcholy navštívené, ak začneme prehľadávať vo vrchole 0. Predpokladajte, že adjVertices vracia susedov v poradí podľa čísla vrcholu.
- Do reprezentácie grafu pomocou zoznamov susedov z prednášky pridajte metódu addVertex, ktorá pridá do grafu nový vrchol.
- Máme danú šachovnicu n x m, na ktorej sú niektoré políčka obsadené figúrkami. Na políčku (i,j) stojí kôň. Na ktoré políčka šachovnice vie preskákať, pričom môže použiť aj viacero ťahov, ale môže chodiť iba po prázdnych políčkach?
- Použite prehľadávanie do hĺbky na grafe, v ktorom sú vrcholy jednotlivé políčka a hrany spájajú dvojice voľných políčok, medzi ktorými môže kôň skočiť. Môžete vytvoriť graf v niektorej reprezentácii, alebo to riešiť priamo v matici visited rozmerov n x m.
- Šachový kôň môže z pozície (i,j) skočiť na jednu z pozícií (i+2,j+1), (i+2,j-1), (i-2,j+1), (i-2,j-1), (i+1,j+2), (i+1,j-2), (i-1,j+2), (i-1,j-2)
- Reprezentáciu grafu pomocou matice susednosti z prednášky zmeňte tak, aby adjVertices vrátil objekt vašej vlastnej triedy implementujúcej Iterable<Integer>. Tá by zase vedela vrátiť váš vlastný iterátor triedy implementujúcej Iterator<Integer>. Tento objekt by si pamätal iba číslo vrchola, cez ktorého susedov iterujeme a pozíciu v príslušnom riadku matice susednosti. Pri každom volaní next hľadá ďalšiu hodnotu true v tomto riadku. Iterátoru tiež správne implementujte metódu remove, aby mazala hranu (obidve jej kópie). Kostru nájdete nižšie.
Prehľadávanie s číslovaním pre prvý príklad
/** Trieda obsahujuca prehladavanie do hlbky s cislovanim vrcholov */ class Search { /** Pre kazdy vrchol poradove cislo kedy bol navstiveny */ private int[] whenVisited; /** Pocet navstivenych vrcholov */ private int numVisited; /** samotny graf */ private Graph g; /** Konstruktor, ktory dostane graf a prehladava do hlbky * so zaciatkom vo vrchole start */ public Search(Graph g, int start) { this.g = g; // uloz graf numVisited = 0; // inicializuj pocet navstivenych vrcholov int n = g.getNumberOfVertices(); // pocet vrcholov grafu whenVisited = new int[n]; // vytvor a inicializuj pole for (int i = 0; i < n; i++) { whenVisited[i] = -1; } search(start); // prehladavaj rekurzive } /** Pomocna rekurzivna metoda pouzivana v konstruktore * na rekurzivne prehladavanie. */ private void search(int vertex) { whenVisited[vertex] = numVisited; numVisited++; for(int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (whenVisited[neighbor] == -1) { search(neighbor); // navstivime ho rekurzivne } } } @Override public String toString() { String res = ""; for(int i=0; i<g.getNumberOfVertices(); i++) { res = res + i + ": " + whenVisited[i] + "\n"; } return res; } }
Kostra pre implementáciu vlastného iterátora
/** Trieda reprezentujúca neorientovaný graf pomocou matice susednosti. */ class AdjMatrixGraph implements Graph { /** Matica susednosti */ private boolean[][] matrix; /** Počet hrán grafu */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjMatrixGraph(int numVertices) { matrix = new boolean[numVertices][numVertices]; for (int i = 0; i < numVertices; i++) { for (int j = 0; j < numVertices; j++) { matrix[i][j] = false; } } numEdges = 0; } @Override public int getNumberOfVertices() { return matrix.length; } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { // nepridava existujuce hrany return false; } matrix[from][to] = true; //prida hranu v oboch smeroch matrix[to][from] = true; numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return matrix[from][to]; } @Override public Iterable<Integer> adjVertices(int vertex) { // vytvori pomocne pole a, // vlozi do neho vsetkych susedov a vrati jeho iterator return new AdjIterable(vertex); } /** Vnútorná trieda pre Iterable cez susedov. */ private class AdjIterable implements Iterable<Integer> { int vertex; public AdjIterable(int vertex_) { vertex = vertex_; } @Override public Iterator<Integer> iterator() { return new AdjIterator(vertex); } } /** Vnútorná trieda pre Iterator cez susedov. * Vie pristupovať k premenným a metódam grafu, napr getNumberOfVertices(), matrix atd. */ private class AdjIterator implements Iterator<Integer> { int vertex; // vrchol, ktorého susedov hľadáme int pos = -1; // posledný sused doteraz vrátený pomocou next private AdjIterator(int vertex_) { vertex = vertex_; } @Override public boolean hasNext() { // VAS KOD TU } @Override public Integer next() { // VAS KOD TU } @Override public void remove() { // VAS KOD TU } } } public class Prog { /** Zo scannera s načíta graf vo formáte: počet vrcholov, * počet hrán a zoznam hrán zadaných koncami vrcholov. * Ak je matrix true, uloží ho ako AdjMatrixGraph, * inak ako AdjListsGraph. */ static Graph readGraph(Scanner s, boolean matrix) { int n = s.nextInt(); int m = s.nextInt(); Graph g; if (matrix) { g = new AdjMatrixGraph(n); } else { g = new AdjListsGraph(n); } for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; } /** Graph g vypíše do výstupného streamu */ static void printGraph(Graph g, PrintStream out) { int n = g.getNumberOfVertices(); out.println(n + " " + g.getNumberOfEdges()); for (int u = 0; u < n; u++) { for (int v : g.adjVertices(u)) { if (u < v) { // kvoli symetrii v neorientovaných grafoch out.println(u + " " + v); } } } } public static void main(String[] args) throws FileNotFoundException { Scanner s; Graph g; PrintStream out; // nacitame graf ako zoznamy susedov s = new Scanner(new File("graph-in.txt")); g = readGraph(s, true); s.close(); // zmazeme vsetky hrany for (int u = 0; u < g.getNumberOfVertices(); u++) { Iterator<Integer> it = g.adjVertices(u).iterator(); while (it.hasNext()) { it.next(); it.remove(); } } System.out.println("Number of edges: " + g.getNumberOfEdges()); // malo by byt 0 } }
DÚ16
Odovzdávanie DÚ16 max. 10 bodov, termín odovzdania štvrtok 9.5. o 22:00.
Cieľom tejto domácej úlohy je precvičiť si prácu s grafmi a prehľadávaním do hĺbky.
Uvažujme jednoduchú hru, v ktorej máme obdĺžnikovú hraciu plochu rozdelenú na políčka, pričom každé políčko je buď voľné alebo obsadené stenou. Na tejto ploche sa pohybuje červík, ktorý vždy zaberá dve voľné susedné políčka: na jednom má hlavu a na jednom chvost. Poloha červíka je teda daná súradnicami jeho hlavy a smerom (hore, dole, vľavo, vpravo), v ktorom sa nachádza chvost vzhľadom na hlavu. V každom ťahu sa červík môže hlavou posunúť na jedno zo susedných voľných políčok (teda takých, kde nie je stena ani chvost) a chvost sa posunie na to políčko, kde bola hlava.
Úlohou je zistiť, či sa červík vie takýmito ťahmi dostať z jednej zadanej polohy do druhej. Váš program by mal načítať vstup zo súboru vstup.txt. Na prvom riadku súboru sú dve čísla udávajúce počet riadkov a počet stĺpcov hracej plochy. Nasleduje samotná plocha, pričom bodka znamená voľné políčko a X znamená stenu. V posledných dvoch riadkoch súboru je potom zadaná počiatočná a cieľová poloha červíka. Obidve polohy sú zadané ako štyri čísla udávajúce riadok a stĺpec hlavy a riadok a stĺpec chvosta (riadky a stĺpce číslujeme od nuly). Môžete predpokladať, že obidve polohy sú platné, t.j. hlava aj chvost sú na susedných voľných políčkach hracej plochy. Môžete tiež predpokladať, že na kraji hracej plochy sú všade steny. Váš program by mal na konzolu vypísať dve hodnoty, každú do zvláštneho riadku: boolean (true alebo false) vyjadrujúci, či sa červík vie dostať do cieľovej polohy a počet polôh, do ktorých sa červík vie dostať z počiatočnej polohy (vrátane počiatočnej polohy samotnej).
Príklady vstupu a výstupu
Vstup:
3 6 XXXXXX X....X XXXXXX 1 2 1 1 1 1 1 2
Správny výstup:
false 3
V tomto príklade sa pýtame, či sa červík vie otočiť, t.j. vymeniť polohu hlavy a chvosta. Nakoľko však stojí v úzkej uličke, nie je to možné. Vie dosiahnuť 3 polohy, lebo jediné, čo vie spraviť, je posúvať sa doprava a môže sa posunúť najviac dvakrát.
Vstup:
4 8 XXXXXXXX X......X XXXX..XX XXXXXXXX 1 3 1 2 1 1 1 2
Správny výstup:
true 14
V tomto príklade sa červík vie vo výklenku vpravo otočiť a dostať sa tak až na ľavý okraj. Tam už však je vždy otočený iba doľava. Naopak sa môže rozhodnúť ísť aj na pravý okraj hornej chodby. Všimnite si, že aj keď na obrázku je iba 8 voľných políčok, máme až 14 dosiahnuteľných polôh, lebo na niektorých políčkach je viacero možných polôh chvosta.
Algoritmus
Celú situáciu vieme reprezentovať ako orientovaný graf, v ktorom vrcholy zodpovedajú polohe červíka a orientované hrany idú z jednej polohy do druhej, ak sa tak červík vie posunúť v jednom ťahu. Pre červíka uprostred väčšej prázdnej plochy teda máme tri vychádzajúce hrany pre tri voľné smery, štvrtý smer je blokovaný chvostom.
Prehľadávaním do hĺbky môžeme aj v orientovanom grafe nájsť všetky vrcholy dosiahnuteľné zo zadaného vrcholu. Pre každý navštívený vrchol pozrieme všetky hrany z neho vychádzajúce a ak ešte príslušné vrcholy neboli navštívené, navštívime ich rekurzívne. (Pozor, komponenty súvislosti v orientovanom grafe nie sú tak jednoduché definovať, lebo ak sa vieme dostať z jedného vrcholu do druhého, neznamená to, že vieme ísť aj naspäť).
V tomto príklade sa ponúkajú dve možnosti ako graf reprezentovať:
- Použijete zoznamy susedov z prednášky, pričom ich mierne upravíte pre orientované grafy. Po načítaní vstupu vytvoríte graf s potrebnými vrcholmi a hranami a spustíte prehľadávanie do hĺbky podobne ako na prednáške. Dôležité je pri vytváraní grafu zistiť čísla vrcholov pre počiatočnú a cieľovú polohu.
- Druhá možnosť je nevytvárať graf explicitne. Použijete iba maticu visited pre všetky polohy, pričom táto matica môže byť trojrozmerná, kde prvé dva rozmery zdpovedajú súradniciam hlavy a tretí rozmer vyjadruje smer, v ktorom je od hlavy chvost. Aktuálny vrchol vo vyhľadávaní bude tiež zadanými týmito troma údajmi. Z matice reprezentujúcej hernú plochu potom vieme určiť, ktoré zo susedných políčok môže červík navštíviť a tie rekurzívne prehľadáme. Hrany grafu teda generujeme za behu a nikde ich neukladáme.
Odovzdávanie
- Váš balíček nazvite worm a v ňom triedu s metódou main nazvite Worm.
- Odovzdávajte jeden .zip súbor obsahujúci adresár worm a v ňom všetky zdrojové súbory (*.java) vášho balíčka.
Cvičenia 24
Úlohou tohto cvičenia je zoznámiť sa s knižnicou GraphGUI, ktorú budete používať na skúške. Táto knižnica zahŕňa programovanie vo Swingu a prácu s grafmi.
Úloha A: pozrite si popis knižnice
Úloha B: Stiahnite si graphgui.zip a rozzipujte ho. Potom si ho otvorte v Netbeans pomocou New project, ako typ projektu zvoľte Java Project with Existing Sources, na ďalšej obrazovke vyplňte Project Name graphgui a na ďalšej pomocou Add Folder pridajte adresár s rozzipovanými súbormi. Je dobré si tento postup vyskúšať v učebni, aby ste na skúške nemali problémy. Projekt skúste skompilovať, spustiť a pozrite si, čo program robí.
Úloha C: Do súboru Editor.java doprogramujte, aby sa po stlačení tlačidla Edit otvorilo editovacie okienko, v ktorom môže užívateľ zadať tri reálne čísla medzi 0 a 1, ktoré zodpovedajú červenej, zelenej a modrej zložke farby a následne sa práve vybraný vrchol prefarbí na túto farbu. Ak žiaden vrchol nie je vybraný, program vypíše dialogové okienko s upozornením o chybe, podobne aj keď užívateľ zadá neplatné hodnoty, ktoré nie je možné previesť na číslo alebo ktoré nie sú v rozsahu medzi 0 a 1. Pomôcky:
- Trieda Color v balíčku java.awt má konštruktor Color(float r, float g, float b), ktorý vytvorí farbu so zadanými intenzitami červenej (r), zelenej (g) a modrej (b).
- Primitívny typ float sa podobá na double, je však menej presný. Trieda Float reprezentujúca jeho objektový variant má konštruktor Float(String s), ktorý z reťazca načíta číslo a ak v reťazci nie je dané platné číslo, vyhodí výnimku triedy NumberFormatException.
Úloha D: Do súboru GraphAlgorithm.java doprogramujte, aby po stlačení tlačidla Action program zistil stupeň (t.j. počet susedov) označeného vrcholu. Týmto susedom zmeňte farbu na žltú. V konštruktore pre GraphAlgorithm si uložte alebo vytvorte potrebné premenné. Označený vrchol bude zadaný v parametri selected konštruktora. Ak je selected rovné null, vyhoďte NoSuchElementException. V metóde performAlgorithm spočítajte počet susedov a vrcholy prefarbite žltou farbou (Color.YELLOW). Farby ostatných vrcholov nechajte pôvodné. Metóda getMessage, ktorá sa volá až po performAlgorithm, by mala vrátiť reťazec vo formáte "Pocet susedov: 6".
Úloha E: Zmeňte riešenie úlohy D tak, aby program zistil počet všetkých dosiahnuteľných vrcholov z označeného vrcholu. Po zavolaní getMessage vráti reťazec vo formáte "Pocet dosiahnutelnych vrcholov: 6". Všetkým dosiahnuteľným vrcholom nastavte farbu na žltú, ostatným na bielu. Adaptujte prehľadávanie do hĺbky z prednášky 34.
Prednáška 35
Oznamy
- Na minulých cvičeniach bola predstavená knižnica ku skúške, doporučujeme sa s ňou oboznámiť
- Túto aj budúcu stredu je sviatok, nebudú teda cvičenia.
- Budúci pondelok 6.5. o 13:30 v M218 a M217 uskutočnia náhradné cvičenia. Nebude na nich rozcvička, ale bude sa precvičovať dôležité učivo z grafových algoritmov, preto odporúčame účasť.
- Do budúceho utorka 7.5. si v prípade záujmu vyberte tému nepovinného projektu.
- Do budúceho štvrtka 9.5. odovzdávajte domácu úlohu 16.
- Pondelok 20.5. o 10:00 V B bude písomný test
- Streda 22.5. termín odovzdania projektu v Moodli
- Štvrtok 23.5. predvádzanie projektov (v prípade konfliktu iný termín)
- Prvý termín skúšky 22.5.?
Opakovanie
Reprezentácia grafov
Neorientovaný graf s vrcholmi očíslovanými 0,1,...,n-1.
- Príklad: V={0,...,6}, E={{0,5},{1,2},{2,3},{1,3},{3,4}}
Interface Graph
/** Interface reprezentujúci graf s n vrcholmi očíslovanými 0..n-1. */ interface Graph { /** Vráti počet vrcholov grafu n. */ int getNumberOfVertices(); /** Vráti počet hrán grafu. */ int getNumberOfEdges(); /** Do grafu pridá hranu z vrcholu from do vrcholu to, * vráti true ak sa ju podarilo pridať. */ boolean addEdge(int from, int to); /** Vráti true, ak existuje hrana z vrcholu from * do vrcholu to. */ boolean existsEdge(int from, int to); /** Vráti iterovateľnú skupinu susedov vrchola vertex. * V prípade orientovaného grafu vracia iba hrany vychádzajúce z vrchola. * Metóda remove iterátora nemusí byť podporovaná. */ Iterable<Integer> adjVertices(int vertex); }
Matica susednosti (adjacency matrix), trieda AdjMatrixGraph
- Matica nxn napr typu boolean
- Políčko a[i][j]=true ak {i,j} je hrana
0 1 2 3 4 5 6 0 F F F F F T F 1 F F T T F F F 2 F T F T F F F 3 F T T F T F F 4 F F F T F F F 5 T F F F F F F 6 F F F F F F F
Zoznamy susedov (adjacency lists), trieda AdjListsGraph
- Pre každý vrchol zoznam jeho susedov
- Uložíme ako pole potrebnej veľkosti alebo spájaný zoznam (ArrayList, LinkedList)
- Lepšia reprezentácia pre väčšinu algoritmov, obzvlášť ak je graf riedky, t.j. má pomerne málo hrán
0: 5 1: 2,3 2: 1,3 3: 1,2,4 4: 3 5: 0 6:
Prehľadávanie do hĺbky, komponenty súvislosti
- Vrcholy neorientovaného grafu vieme rozdeliť na komponenty súvislosti
- V grafe existuje cesta z vrcholu u do vrcholu v práve vtedy, keď sú v tom istom komponente súvislosti
- Mnohé úlohy vieme formulovať ako zisťovanie komponentov súvislosti (videli sme valec lesom, šachového koňa, červíka v bludisku)
- Prehľadávanie do hĺbky začne z jedného vrcholu a rekurzívne prehľadá celý komponent.
- Môžeme teda postupným volaním prehľadávania očísľovať komponenty 0,1,...,k-1 a pre každý vrchol uložiť číslo jeho komponentu.
- Potom dva vrcholy sú spojené cestou práve vtedy, keď majú rovnaké číslo komponentu.
- Dáta potrebné počas rekurzie a výsledky si uložíme do pomocnej triedy
/** Trieda obsahujúca rozdelenie vrcholov grafu do komponentov súvislosti */ class Components { /** Pre kazdy vrchol cislo jeho komponentu 0..pocet komponentov-1 */ private int[] componentId; /** pocet komponentov grafu */ private int numComponents; /** samotny graf */ private Graph g; /** Konstruktor, ktory dostane graf a prehladavanim do hlbky * hlada komponenty suvislosti */ public Components(Graph g) { this.g = g; // uloz graf numComponents = 0; // inicializuj pocet koponentov int n = g.getNumberOfVertices(); // pocet vrcholov grafu // vytvor pole cisel koponentov a inicializuj na -1 - nevyfarbene componentId = new int[n]; for (int i = 0; i < n; i++) { componentId[i] = -1; } // prechadzaj cez vrchola a ak najdes nevyfarbeny, spusti prehladavanie for (int i = 0; i < n; i++) { if (componentId[i] == -1) { search(i, numComponents); // vyfarbi cislom numComponents numComponents++; // zvys numComponents } } } /** Pomocna rekurzivna metoda pouzivana v konstruktore na vyfarbenie * jedneho komponentu cislom id. */ private void search(int vertex, int id) { componentId[vertex] = id; for(int neighbor : g.adjVertices(vertex)) { if (componentId[neighbor] == -1) { search(neighbor, id); // navstivime ho rekurzivne } } } /** Vrati true ak vrchol from a to su v tom istom koponente */ public boolean areConnected(int from, int to) { return componentId[from] == componentId[to]; } /** Vrati pocet kponentov grafu */ public int getNumberOfComponents() { return numComponents; } }
Najkratšie cesty, prehľadávanie do šírky (breath-first search, BFS)
- Ak sú dva vrcholy v jednom komponente súvislosti, mohli by sme chcieť vypísať aj cestu, ktorá ich spája
- Najlepšie najkratšiu cestu, teda takú, ktorá obsahuje najmenej hrán
- To nám prehľadávanie do hĺbky nenájde, použijeme prehľadávanie do šírky
- Začneme v nejakom vrchole s
- V prehľadávaní do šírky najprv navštívime všetky vrcholy vo vzdialenosti 1 od s, potom vo vzdialenosti 2, ...
- Pre každý navštívený vrchol v spočítame jeho vzdialenosť od s aj predposledný vrchol na najkratšej ceste z s do v
- Hrany medzi v a pred[v] tvoria strom (kostru grafu, ak je súvislý)
- Ak chceme vypísať cestu, po strome sa pohybujeme smerom k jeho koreňu vo vrchole s
- Algoritmus používa rad (frontu, queue), do ktorej dáva vrcholy, ktoré už majú známu vzdialenosť, ale ešte sme nepozreli ich susedov
- V rade sú v každej chvíli vrcholy najviac dvoch vzdialeností od s: najprv nejaké vrcholy vo vzdialenosti d, potom nejaké vrcholy vo vzdialenosti d+1
- Zložitosť je tiež O(n+m), ak použijeme zoznamy susedov
/** Trieda, ktorá reprezentuje najkratšie cesty a vzdialenosti * z jedného vrchola do ostatných. */ class ShortestPaths { /** Graf, v ktorom ratame cesty */ private Graph g; /** Startovaci vrchol, z ktoreho ratame najkratsie cesty */ private int start; /** Pre kazdy vrchol v grafe vzdialenost od startu * alebo -1 ak je v inom komponente. */ private int[] dist; /** Pre kazdy vrchol u predchadzajuci vrchol na ceste zo start do u */ private int[] prev; /** Konstruktor, ktory dostane graf a startovaci vrchol a najde najkratsei cesty */ public ShortestPaths(Graph g, int start) { this.g = g; this.start = start; int n = g.getNumberOfVertices(); // inicializacia poli - vyplnime -1 dist = new int[n]; prev = new int[n]; for (int i = 0; i < n; i++) { dist[i] = -1; prev[i] = -1; } // prehladavanie do sirky // vytvorime rad a vlozime do neho startovaci vrchol LinkedList<Integer> queue = new LinkedList<Integer>(); queue.addLast(start); dist[start] = 0; // sam od seba ma vzdialenost 0 while (!queue.isEmpty()) { // vyberieme prvok na zaciatku radu int vertex = queue.removeFirst(); // prejdeme cez jeho susedov a ak este neboli navstiveni // urcime im vzdialenost a pridame ich do radu for (int neighbor : g.adjVertices(vertex)) { if (dist[neighbor] < 0) { dist[neighbor] = dist[vertex] + 1; prev[neighbor] = vertex; queue.addLast(neighbor); } } } } /** Je vrchol vertex spojeny so startovacim vrcholom? */ public boolean isConnected(int vertex) { return dist[vertex] >= 0; } /** Vrati vzdialenost vrcholu vertex od starovacieho vrcholu. * Ak su v roznych komponentoch, vrati -1. */ public int distance(int vertex) { return dist[vertex]; } /** Vrati najkratsiu cestu zo startovacieho vrcholu * do vrcholu vertex (postupnost vrcholov, cez ktore cesta ide). * Ak su v roznych komponentoch, vrati null. */ public int[] shortestPath(int vertex) { if (!isConnected(vertex)) { // vybav rozne koponenty return null; } int[] path = new int[dist[vertex] + 1]; // alokujeme cestu int v = vertex; //posledny vrchol bude vertex path[dist[vertex]] = v; for (int i = dist[vertex] - 1; i >= 0; i--) { // odzadu pridavame vrcholy v = prev[v]; // posunieme sa na predchadzajuci vrchol na ceste path[i] = v; } return path; } }
Opakovanie: prehľadávanie s návratom (backtracking)
- Prehľadávanie s návratom sme videli minulý semester napr. na problém 8 dám
- Riešenie úlohy je nejaká postupnosť, rekurzívne skúšame pridať všetky možnosti ďalšieho prvku postupnosti
- Prehľadávame exponenciálny počet možností, algoritmus môže byť veľmi pomalý
- Snažíme sa ukončiť prehľadávanie vždy, keď už nie je šanca dosiahnuť platné riešenie
Príklad: vypisujeme všetky variácie bez opakovania dĺžky k z prvkov {0..n-1}
package prog; import java.util.Scanner; public class Prog { static void vypis(int[] a) { for (int x : a) { System.out.print(" " + x); } System.out.println(); } static void generuj(int[] a, boolean[] bolo, int i, int n) { /* v poli a dlzky k mame prvych i cifier, * v poli bolo mame zaznamenane, ktore cifry su uz pouzite, * chceme vygenerovat vsetky moznosti * poslednych k-i cifier */ if (i == a.length) { vypis(a); } else { for (int x = 0; x < n; x++) { if (!bolo[x]) { a[i] = x; bolo[x] = true; generuj(a, bolo, i + 1, n); bolo[x] = false; } } } } public static void main(String[] args) { int k, n; Scanner s = new Scanner(System.in); k = s.nextInt(); n = s.nextInt(); int[] a = new int[k]; boolean[] bolo = new boolean[n]; for (int i = 0; i < n; i++) { bolo[i] = false; } generuj(a, bolo, 0, n); } }
Prehľadávanie s návratom na grafoch
- Veľa úloh na grafoch však nevieme riešiť rýchlymi algoritmami, backtrackingom vieme spočítať odpoveď aspoň pre malé vstupy
Hľadanie ciest dĺžky k
- Cesta v grafe je postupnosť vrcholov Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_0,v_1,...,v_k} taká, že každé dva za sebou idúce vrcholy Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_i} a Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_{i+1}} sú spojené hranou a žiaden vrchol sa na ceste neopakuje. Dĺžka tejto cesty je k, t.j. počet hrán na ceste.
- Prehľadávaním do šírky vieme nájsť najkratšiu cestu z u do v.
- Čo ak chceme nájsť cestu z u do v dĺžky presne k?
Príklad použitia: vypisujeme cesty rôznych dĺžok medzi vrcholmi from=0 a to=3:
for (int length = 1; length < g.getNumberOfVertices(); length++) { System.out.println("Cesty dlzky " + length + ":"); FixedLengthPaths p = new FixedLengthPaths(g, 0, 3, length); }
Výstup pre graf s V={0,...,4} a E={{0,1},{0,2},{0,3},{1,2},{2,3},{2,4},{3,4}}
Cesty dlzky 1: 0 3 Cesty dlzky 2: 0 2 3 Cesty dlzky 3: 0 1 2 3 0 2 4 3 Cesty dlzky 4: 0 1 2 4 3
- Opäť použijeme pomocnú triedu na ukladanie všetkých premenných potrebných v rekurzii
- Pole path s práve vyrábanou cestou
- Pole visited, v ktorom máme poznačené, ktoré vrcholy sú použité na ceste
- Vstupné dáta: graf, 2 vrcholy, požadovaná dĺžka
- V rekurzii skúšame k ceste pridať ďalší vrchol, pričom vyberáme z vrcholov susedných s posledným vrcholom na ceste
- Keď nájdeme cestu požadovanej dĺžky, ktorá končí v cieľovom vrchole, cestu vypíšeme
/** Trieda, ktora umoznuje najst vsetky cesty zadanej * dlzky medzi dvoma danymi vrcholmi. */ class FixedLengthPaths { /** samotny graf */ private Graph g; /** pociatocny a koncovy vrchol */ private int from, to; /** pozadovana dlzka cesty */ private int length; /** tvorena cesta */ private int[] path; /** vrcholy, ktore su na ceste pouzite */ private boolean[] visited; /** Konstruktor, ktory dostane graf, pociatocny a koncovy vrchol * a pozadovanu dlzku cesty a rovno aj spusti rekurzivne vyhladavanie * a vypisuje vysledky. */ public FixedLengthPaths(Graph g, int from, int to, int length) { this.g = g; // uloz vstupne data this.from = from; this.to = to; this.length = length; int n = g.getNumberOfVertices(); // pocet vrcholov grafu visited = new boolean[n]; // vytvor a inicializuj polia visited a path for (int i = 0; i < n; i++) { visited[i] = false; } path = new int[length + 1]; // length hran, teda length+1 vrcholov search(0, from); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda ako i-ty vrchol cesty ulozi vertex a * potom skusa vsetky moznosti ako sa na length-i krokov * dostat do cieloveho vrcholu. */ private void search(int i, int vertex) { // ulozime vrchol do cesty a poznacime ho ako pouzity path[i] = vertex; visited[vertex] = true; // ak uz mame cestu dlzky length, pozrieme, // ci konci v spravnom vrchole a vypiseme if (i == length) { if (vertex == to) { printPath(); } } else { // ak este nemame cestu dlzky length, // pozrieme na susedov aktualneho vrcholu vertex for(int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (!visited[neighbor]) { // ak este sused nebol navstiveny, preskumame ho rekurzivne search(i + 1, neighbor); } } } visited[vertex] = false; } private void printPath() { for (int i = 0; i < path.length; i++) { System.out.print(" " + path[i]); } System.out.println(); } }
Cvičenia:
- Namiesto vypisovania ciest iba spočítajte, koľko ich je
- Zistite, či cesta dĺžky k existuje, po nájdení prvej cesty je už možné ukončiť ďalšie prehľadávanie
- Navrhnite spôsoby, ako v niektorých prípadoch zistiť, že aktuálne rozrobenú cestu už nie je možné požadovaným spôsobom rozšíriť
Hľadanie najdlhšej cesty
- V grafe chceme nájsť najdlhšiu cestu z u do v
- Malé zmeny v predchádzajúcom programe
- Pamätáme si najdlhšiu nájdenú cestu
- Vždy keď prídeme do cieľového vrcholu, porovnáme dĺžku aktuálnej cesty s najdlhšou nájdenou doteraz
/** Trieda, ktora umoznuje najst najdlhsiu cestu medzi dvoma danymi vrcholmi. */ class LongestPath { /** samotny graf */ private Graph g; /** pociatocny a koncovy vrchol */ private int from, to; /** najvyssia najdena dlzka cesty */ private int maxLength; /** najlepsia najdena cesta */ private int[] longestPath; /** tvorena cesta */ private int[] path; /** vrcholy, ktore su na ceste pouzite */ private boolean[] visited; /** Konstruktor, ktory dostane graf, pociatocny a koncovy vrchol * a spusti rekurzivne vyhladavanie */ public LongestPath(Graph g, int from, int to) { this.g = g; // uloz vstupne data this.from = from; this.to = to; int n = g.getNumberOfVertices(); // pocet vrcholov grafu visited = new boolean[n]; // vytvor a inicializuj pole visited for (int i = 0; i < n; i++) { visited[i] = false; } path = new int[n]; maxLength = -1; search(0, from); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda ako i-ty vrchol cesty ulozi vertex a * potom skusa vsetky moznosti ako v ceste pokracovat dalej. */ private void search(int i, int vertex) { // ulozime vrchol do cesty a poznacime ho ako pouzity path[i] = vertex; visited[vertex] = true; // ak sme v cielovom vrchole, porovname cestu s maximom if (vertex == to) { if (i > maxLength) { // ak sme nasli lepsiu cestu, ulozime si ju longestPath = Arrays.copyOf(path, i + 1); // kopiruj prvych i+1 prvkov maxLength = i; } } else { // ak este nie sme vo vrchole to, // pozrieme na susedov aktualneho vrcholu vertex for(int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (!visited[neighbor]) { // ak este sused nebol navstiveny, preskumame ho rekurzivne search(i + 1, neighbor); } } } visited[vertex] = false; } int[] longestPath() { return Arrays.copyOf(longestPath, maxLength + 1); } }
Použitie triedy:
LongestPath p = new LongestPath(g, 0, 3); System.out.println("Najdlhsia cesta: " + Arrays.toString(p.longestPath()));
Príklad výstupu na rovnakom grafe ako vyššie:
Najdlhsia cesta: [0, 1, 2, 4, 3]
Hľadanie najdlhšej cesty v grafe s ohodnotenými hranami
Mierna obmena predchádzajúceho problému
- Každá hrana v grafe má priradené číslo (váha alebo dĺžka hrany)
- Hľadáme cestu z u do v s najväčším súčtom váh hrán
Napr. ak v grafe vyššie majú všetky hrany dĺžku 1, ale hrany {0,2} a {2,3} majú dĺžku 10, dostávame:
Najdlhsia cesta: [0, 2, 3]
Pozor, na hľadanie najkratšej cesty v ohodnotenom grafe sa nedá použiť prehľadávanie do šírky, algoritmy uvidíte v treťom ročníku.
Interface pre graf s ohodnotenými hranami
Potrebujeme v prvom rade takýto graf reprezentovať. V abstraktnom dátovom type potrebujeme aspoň tieto zmeny:
- Možnosť pri vkladaní hrany uložiť aj jej dĺžku
- Možnosť zistiť dĺžku hrany medzi dvoma vrcholmi, ak existuje (nebudeme implementovať)
- Možnosť pri iterovaní cez susedov zistiť dĺžku hrany k susedovi
/** Pomocna trieda reprezentujuca dvojicu sused a * vaha hrany k tomuto susedovi. */ class WeightedNeighbor { private int vertex; // cislo suseda private double weight; // vaha hrany k susedovi /** Konstruktor dostane vrchol v a vahu w */ public WeightedNeighbor(int v, double w) { vertex = v; weight = w; } public int vertex() { return vertex; } double weight() { return weight; } } /** Interface pre graf s hranami ohodnotenymi * desatinnymi cislami. */ interface WeightedGraph extends Graph { /** Pridaj hranu s danou vahou */ boolean addEdge(int from, int to, double weight); /** Iterovatelny zoznam ovahovanych susedov */ Iterable <WeightedNeighbor> weightedAdjVertices(int vertex); }
Implementácia ohodnoteného grafu zoznamami susedov
- podobná na neváhovanú verziu
/** Trieda reprezentujúca neorientovaný graf s hranami ohodnotenymi * desatinnymi cislami. Graf je ulozeny pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class WeightedAdjListsGraph implements WeightedGraph { /** Zoznam susedov pre každý vrchol (dvojice vrchol, vaha hrany) */ private ArrayList<ArrayList<WeightedNeighbor>> adjLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public WeightedAdjListsGraph(int numVertices) { adjLists = new ArrayList<ArrayList<WeightedNeighbor>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<WeightedNeighbor>()); } numEdges = 0; } public int getNumberOfVertices() { return adjLists.size(); } public int getNumberOfEdges() { return numEdges; } /** Pridanie hrany s vahou 0 */ public boolean addEdge(int from, int to) { return addEdge(from, to, 0); } /** Pridanie hrany s danou vahou */ public boolean addEdge(int from, int to, double weight) { adjLists.get(from).add(new WeightedNeighbor(to, weight)); // pridaj hranu v oboch smeroch adjLists.get(to).add(new WeightedNeighbor(from, weight)); numEdges++; return true; } /** Test na existenciu hrany */ public boolean existsEdge(int from, int to) { // prejdeme cez susedov from iteratorom, testujeme na rovnost s to for(WeightedNeighbor other : weightedAdjVertices(from)) { if (other.vertex() == to) { return true; } } return false; } /** iterator cez vahovanych susedov vrchola */ public Iterable <WeightedNeighbor> weightedAdjVertices(int vertex) { return adjLists.get(vertex); } /** iterator cez susedov vrchola */ public Iterable <Integer> adjVertices(int vertex) { ArrayList <Integer> result = new ArrayList<Integer>(); for(WeightedNeighbor other : weightedAdjVertices(vertex)) { result.add(other.vertex()); } return result; } }
Samotné prehľadávanie
- Okrem aktuálnej cesty si musíme pamätať aj jej dĺžku, t.j. súčet váh hrán na ceste
- Rekurzia riešená trochu inak: v cykle cez susedov rovno ukladáme suseda do všetkých potrebných polí (lebo vieme váhu hrany)
- Rekurzívne prehľadávanie sa väčšinou dá zapísať veľa rôznymi spôsobmi
- Treba si vždy dobre rozmyslieť, čo robí rekurzívna funkcia a čo by malo platiť o jednotlivýh premenných
/** Trieda, ktora umoznuje najst najdlhsiu cestu * medzi dvoma danymi vrcholmi v ovahovanom grafe. */ class LongestWeightedPath { /** samotny graf */ private WeightedGraph g; /** pociatocny a koncovy vrchol */ private int from, to; /** najvyssia najdena dlzka cesty */ private double maxLength; /** najlepsia najdena cesta */ private int[] longestPath; /** tvorena cesta */ private int[] path; /** dlzka tvorenej cesty */ private double length; /** vrcholy, ktore su na ceste pouzite */ private boolean[] visited; /** Konstruktor, ktory dostane graf, pociatocny a koncovy vrchol * a spusti rekurzivne vyhladavanie */ public LongestWeightedPath(WeightedGraph g, int from, int to) { this.g = g; // uloz vstupne data this.from = from; this.to = to; int n = g.getNumberOfVertices(); // pocet vrcholov grafu visited = new boolean[n]; // vytvor a inicializuj pole visited for (int i = 0; i < n; i++) { visited[i] = false; } path = new int[n]; // vytvor cestu dlzky 0 obsahujucu iba zac. vrchol path[0] = from; visited[from] = true; length = 0; maxLength = -1; // najlespia najdena cesta search(1); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda skusa pridat novy vrchol na poziciu i a * potom skusa vsetky moznosti ako v ceste pokracovat dalej. */ private void search(int i) { int vertex = path[i - 1]; // predchadzajuci vrchol na ceste // ak sme v cielovom vrchole, porovname cestu s maximom if (vertex == to) { if (length > maxLength) { // ak sme nasli lepsiu cestu, ulozime si ju longestPath = Arrays.copyOf(path, i); // kopiruj prvych i prvkov maxLength = length; } } else { // ak este nie sme v cielovom vrchole, // pozrieme na susedov predchadzajuceho vrcholu vertex for(WeightedNeighbor other : g.weightedAdjVertices(vertex)) { // prejdi cez susedov int neighbor = other.vertex(); // sused vrcholu vertex if (!visited[neighbor]) { // ak este sused nebol navstiveny, ulozime ho do cesty // a zavolame rekurziu path[i] = neighbor; visited[neighbor] = true; double weight = other.weight(); length += weight; search(i + 1); length -= weight; visited[neighbor] = false; } } } } int[] longestPath() { return Arrays.copyOf(longestPath, longestPath.length); } }
Programy
Programy z tejto prednášky nájdete tu:
- Prehľadávanie do šírky: #Prednáška 34#Zdrojový kód programu, grafy
- #Zdrojový kód programu, prehľadávanie s návratom na neohodnotených grafoch
- #Zdrojový kód programu, prehľadávanie s návratom na ohodnotených grafoch
Zdrojový kód programu, prehľadávanie s návratom na neohodnotených grafoch
- niekoľko prehľadávaní s návratom v neohodnotenom grafe (cesta zadanej dĺžky, najdlhšia cesta, najväčšia klika)
package prog; import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.Scanner; /** Interface reprezentujúci graf s n vrcholmi očíslovanými 0..n-1. */ interface Graph { /** Vráti počet vrcholov grafu n. */ int getNumberOfVertices(); /** Vráti počet hrán grafu. */ int getNumberOfEdges(); /** Do grafu pridá hranu z vrcholu from do vrcholu to, * vráti true ak sa ju podarilo pridať. */ boolean addEdge(int from, int to); /** Vráti true, ak existuje hrana z vrcholu from * do vrcholu to. */ boolean existsEdge(int from, int to); /** Vráti iterovateľnú skupinu susedov vrchola vertex. * V prípade orientovaného grafu vracia iba hrany vychádzajúce z vrchola. * Metóda remove iterátora nemusí byť podporovaná. */ Iterable<Integer> adjVertices(int vertex); } /** Trieda reprezentujúca neorientovaný graf pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class AdjListsGraph implements Graph { /** Zoznam susedov pre každý vrchol */ private ArrayList<ArrayList<Integer>> adjLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<ArrayList<Integer>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<Integer>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { adjLists.get(from).add(to); // pridaj hranu v oboch smeroch adjLists.get(to).add(from); numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); // pozri do zoznamu susedov pre from } @Override public Iterable<Integer> adjVertices(int vertex) { return adjLists.get(vertex); // vrati ArrayList } } /** Trieda, ktora umoznuje najst vsetky cesty zadanej * dlzky medzi dvoma danymi vrcholmi. */ class FixedLengthPaths { /** samotny graf */ private Graph g; /** pociatocny a koncovy vrchol */ private int from, to; /** pozadovana dlzka cesty */ private int length; /** tvorena cesta */ private int[] path; /** vrcholy, ktore su na ceste pouzite */ private boolean[] visited; /** Konstruktor, ktory dostane graf, pociatocny a koncovy vrchol * a pozadovanu dlzku cesty a rovno aj spusti rekurzivne vyhladavanie * a vypisuje vysledky. */ public FixedLengthPaths(Graph g, int from, int to, int length) { this.g = g; // uloz vstupne data this.from = from; this.to = to; this.length = length; int n = g.getNumberOfVertices(); // pocet vrcholov grafu visited = new boolean[n]; // vytvor a inicializuj polia visited a path for (int i = 0; i < n; i++) { visited[i] = false; } path = new int[length + 1]; // length hran, teda length+1 vrcholov search(0, from); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda ako i-ty vrchol cesty ulozi vertex a * potom skusa vsetky moznosti ako sa na length-i krokov * dostat do cieloveho vrcholu. */ private void search(int i, int vertex) { // ulozime vrchol do cesty a poznacime ho ako pouzity path[i] = vertex; visited[vertex] = true; // ak uz mame cestu dlzky length, pozrieme, // ci konci v spravnom vrchole a vypiseme if (i == length) { if (vertex == to) { printPath(); } } else { // ak este nemame cestu dlzky length, // pozrieme na susedov aktualneho vrcholu vertex for(int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (!visited[neighbor]) { // ak este sused nebol navstiveny, preskumame ho rekurzivne search(i + 1, neighbor); } } } visited[vertex] = false; } private void printPath() { for (int i = 0; i < path.length; i++) { System.out.print(" " + path[i]); } System.out.println(); } } /** Trieda, ktora umoznuje najst najdlhsiu cestu medzi dvoma danymi vrcholmi. */ class LongestPath { /** samotny graf */ private Graph g; /** pociatocny a koncovy vrchol */ private int from, to; /** najvyssia najdena dlzka cesty */ private int maxLength; /** najlepsia najdena cesta */ private int[] longestPath; /** tvorena cesta */ private int[] path; /** vrcholy, ktore su na ceste pouzite */ private boolean[] visited; /** Konstruktor, ktory dostane graf, pociatocny a koncovy vrchol * a spusti rekurzivne vyhladavanie */ public LongestPath(Graph g, int from, int to) { this.g = g; // uloz vstupne data this.from = from; this.to = to; int n = g.getNumberOfVertices(); // pocet vrcholov grafu visited = new boolean[n]; // vytvor a inicializuj pole visited for (int i = 0; i < n; i++) { visited[i] = false; } path = new int[n]; maxLength = -1; search(0, from); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda ako i-ty vrchol cesty ulozi vertex a * potom skusa vsetky moznosti ako v ceste pokracovat dalej. */ private void search(int i, int vertex) { // ulozime vrchol do cesty a poznacime ho ako pouzity path[i] = vertex; visited[vertex] = true; // ak sme v cielovom vrchole, porovname cestu s maximom if (vertex == to) { if (i > maxLength) { // ak sme nasli lepsiu cestu, ulozime si ju longestPath = Arrays.copyOf(path, i + 1); // kopiruj prvych i+1 prvkov maxLength = i; } } else { // ak este nie sme vo vrchole to, // pozrieme na susedov aktualneho vrcholu vertex for(int neighbor : g.adjVertices(vertex)) { // prejdi cez susedov if (!visited[neighbor]) { // ak este sused nebol navstiveny, preskumame ho rekurzivne search(i + 1, neighbor); } } } visited[vertex] = false; } int[] longestPath() { return Arrays.copyOf(longestPath, maxLength + 1); } } /** Trieda, ktora umoznuje najst maximalnu kliku v danom grafe. */ class MaximumClique { /** samotny graf */ private Graph g; /** zoznam vrcholov v najvacsej doteraz najdenej klike */ private LinkedList<Integer> maxClique; /** zoznam vrcholov v aktualnej klike */ private LinkedList<Integer> clique; /** Konstruktor, ktory dostane graf a spusti rekurzivne vyhladavanie */ public MaximumClique(Graph g) { this.g = g; // uloz vstupny graf // vytvor dve prazdne kliky clique = new LinkedList<Integer>(); maxClique = new LinkedList<Integer>(); search(0); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda skusi pouzit aj vynechat vrchol vertex * potom skusa vsetky moznosti pre vrcholy vertex+1...n-1. */ private void search(int vertex) { // ak aktualna klika je vacsia ako doterajsie maximum, uloz ju if (clique.size() > maxClique.size()) { // konstruktor LinkedListu moze vytvorit kopiu inej Collection maxClique = new LinkedList<Integer>(clique); } // ak uz sme vycerpali vsetky vrcholy, nie je co skusat dalej if (vertex == g.getNumberOfVertices()) { return; } // otestuj, ci sa vrchol vertex da pridat do kliky // a ci ma sancu byt vo vacsej klike ako doteraz najdena if (isConnected(g, vertex, clique) && degree(g, vertex) + 1 > maxClique.size()) { // ak ano, pridaj ho do kliky a zavolaj rekurziu clique.addLast(vertex); search(vertex + 1); // odober vertex x kliky clique.removeLast(); } // preskoc vertex a zavolaj rekurziu na ostatne vrcholy search(vertex + 1); } /** pomocna metoda, ktora overi, ci vrchol vertex je v grafe g * spojeny s kazdym z vrcholov v mnozine vertexSet. */ private static boolean isConnected(Graph g, int vertex, Collection<Integer> vertexSet) { // iterujeme cez mnozinu vertexSet for(int v : vertexSet) { if (!g.existsEdge(vertex, v)) { // over hranu return false; // hrana nie je } } return true; // vsetky hrany najdene } /** pomocna metoda, ktora zisti stupen vrchola vertex v grafe g */ private static int degree(Graph g, int vertex) { // iterujeme cez susedov vrchola, zvysujeme pocitadlo result int result = 0; for(int x : g.adjVertices(vertex)) { result++; } return result; } /** vrati maximalnu kliku najdenu v grafe g */ public LinkedList<Integer> maxClique() { // vrat kopiu nasej najlepsej kliky return new LinkedList<Integer>(maxClique); } } public class Prog { /** Zo scannera s načíta graf vo formáte: počet vrcholov, * počet hrán a zoznam hrán zadaných koncami vrcholov. * Uloží ho ako AdjMatrixGraph. */ static Graph readGraph(Scanner s) { int n = s.nextInt(); int m = s.nextInt(); Graph g = new AdjListsGraph(n); for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; } public static void main(String[] args) throws FileNotFoundException { Scanner s = new Scanner(new File("graph.txt")); Graph g = readGraph(s); s.close(); for (int length = 1; length < g.getNumberOfVertices(); length++) { System.out.println("Cesty dlzky " + length + ":"); FixedLengthPaths p = new FixedLengthPaths(g, 0, 3, length); } LongestPath p = new LongestPath(g, 0, 3); System.out.println("Najdlhsia cesta: " + Arrays.toString(p.longestPath())); MaximumClique c = new MaximumClique(g); System.out.println( "Maximalna klika: " + c.maxClique().toString()); } } /* Priklad vstupu (ulozit do graph.txt) 5 7 0 1 0 2 0 3 1 2 2 3 2 4 3 4 */
Zdrojový kód programu, prehľadávanie s návratom na ohodnotených grafoch
- graf s ohodnotenými hranami a hľadanie najdlhšej cesty v ňom
package prog; import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.Scanner; /** Interface reprezentujúci graf s n vrcholmi očíslovanými 0..n-1. */ interface Graph { /** Vráti počet vrcholov grafu n. */ int getNumberOfVertices(); /** Vráti počet hrán grafu. */ int getNumberOfEdges(); /** Do grafu pridá hranu z vrcholu from do vrcholu to, * vráti true ak sa ju podarilo pridať. */ boolean addEdge(int from, int to); /** Vráti true, ak existuje hrana z vrcholu from * do vrcholu to. */ boolean existsEdge(int from, int to); /** Vráti iterovateľnú skupinu susedov vrchola vertex. * V prípade orientovaného grafu vracia iba hrany vychádzajúce z vrchola. * Metóda remove iterátora nemusí byť podporovaná. */ Iterable<Integer> adjVertices(int vertex); } /** Trieda reprezentujúca neorientovaný graf pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class AdjListsGraph implements Graph { /** Zoznam susedov pre každý vrchol */ private ArrayList<ArrayList<Integer>> adjLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<ArrayList<Integer>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<Integer>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { adjLists.get(from).add(to); // pridaj hranu v oboch smeroch adjLists.get(to).add(from); numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); // pozri do zoznamu susedov pre from } @Override public Iterable<Integer> adjVertices(int vertex) { return adjLists.get(vertex); // vrati ArrayList } } /** Pomocna trieda reprezentujuca dvojicu sused a * vaha hrany k tomuto susedovi. */ class WeightedNeighbor { private int vertex; // cislo suseda private double weight; // vaha hrany k susedovi /** Konstruktor dostane vrchol v a vahu w */ public WeightedNeighbor(int v, double w) { vertex = v; weight = w; } public int vertex() { return vertex; } double weight() { return weight; } } /** Interface pre graf s hranami ohodnotenymi * desatinnymi cislami. */ interface WeightedGraph extends Graph { /** Pridaj hranu s danou vahou */ boolean addEdge(int from, int to, double weight); /** Iterovatelny zoznam ovahovanych susedov */ Iterable <WeightedNeighbor> weightedAdjVertices(int vertex); } /** Trieda reprezentujúca neorientovaný graf s hranami ohodnotenymi * desatinnymi cislami. Graf je ulozeny pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class WeightedAdjListsGraph implements WeightedGraph { /** Zoznam susedov pre každý vrchol (dvojice vrchol, vaha hrany) */ private ArrayList<ArrayList<WeightedNeighbor>> adjLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public WeightedAdjListsGraph(int numVertices) { adjLists = new ArrayList<ArrayList<WeightedNeighbor>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<WeightedNeighbor>()); } numEdges = 0; } public int getNumberOfVertices() { return adjLists.size(); } public int getNumberOfEdges() { return numEdges; } /** Pridanie hrany s vahou 0 */ public boolean addEdge(int from, int to) { return addEdge(from, to, 0); } /** Pridanie hrany s danou vahou */ public boolean addEdge(int from, int to, double weight) { adjLists.get(from).add(new WeightedNeighbor(to, weight)); // pridaj hranu v oboch smeroch adjLists.get(to).add(new WeightedNeighbor(from, weight)); numEdges++; return true; } /** Test na existenciu hrany */ public boolean existsEdge(int from, int to) { // prejdeme cez susedov from iteratorom, testujeme na rovnost s to for(WeightedNeighbor other : weightedAdjVertices(from)) { if (other.vertex() == to) { return true; } } return false; } /** iterator cez vahovanych susedov vrchola */ public Iterable <WeightedNeighbor> weightedAdjVertices(int vertex) { return adjLists.get(vertex); } /** iterator cez susedov vrchola */ public Iterable <Integer> adjVertices(int vertex) { ArrayList <Integer> result = new ArrayList<Integer>(); for(WeightedNeighbor other : weightedAdjVertices(vertex)) { result.add(other.vertex()); } return result; } } /** Trieda, ktora umoznuje najst najdlhsiu cestu * medzi dvoma danymi vrcholmi v ovahovanom grafe. */ class LongestWeightedPath { /** samotny graf */ private WeightedGraph g; /** pociatocny a koncovy vrchol */ private int from, to; /** najvyssia najdena dlzka cesty */ private double maxLength; /** najlepsia najdena cesta */ private int[] longestPath; /** tvorena cesta */ private int[] path; /** dlzka tvorenej cesty */ private double length; /** vrcholy, ktore su na ceste pouzite */ private boolean[] visited; /** Konstruktor, ktory dostane graf, pociatocny a koncovy vrchol * a spusti rekurzivne vyhladavanie */ public LongestWeightedPath(WeightedGraph g, int from, int to) { this.g = g; // uloz vstupne data this.from = from; this.to = to; int n = g.getNumberOfVertices(); // pocet vrcholov grafu visited = new boolean[n]; // vytvor a inicializuj pole visited for (int i = 0; i < n; i++) { visited[i] = false; } path = new int[n]; // vytvor cestu dlzky 0 obsahujucu iba zac. vrchol path[0] = from; visited[from] = true; length = 0; maxLength = -1; // najlespia najdena cesta search(1); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda skusa pridat novy vrchol na poziciu i a * potom skusa vsetky moznosti ako v ceste pokracovat dalej. */ private void search(int i) { int vertex = path[i - 1]; // predchadzajuci vrchol na ceste // ak sme v cielovom vrchole, porovname cestu s maximom if (vertex == to) { if (length > maxLength) { // ak sme nasli lepsiu cestu, ulozime si ju longestPath = Arrays.copyOf(path, i); // kopiruj prvych i prvkov maxLength = length; } } else { // ak este nie sme v cielovom vrchole, // pozrieme na susedov predchadzajuceho vrcholu vertex for(WeightedNeighbor other : g.weightedAdjVertices(vertex)) { // prejdi cez susedov int neighbor = other.vertex(); // sused vrcholu vertex if (!visited[neighbor]) { // ak este sused nebol navstiveny, ulozime ho do cesty // a zavolame rekurziu path[i] = neighbor; visited[neighbor] = true; double weight = other.weight(); length += weight; search(i + 1); length -= weight; visited[neighbor] = false; } } } } int[] longestPath() { return Arrays.copyOf(longestPath, longestPath.length); } } public class Prog { /** Zo scannera s načíta graf vo formáte: počet vrcholov, * počet hrán a zoznam hrán zadaných koncami vrcholov a vahami. */ static WeightedGraph readGraph(Scanner s) { int n = s.nextInt(); int m = s.nextInt(); WeightedGraph g = new WeightedAdjListsGraph(n); for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); double weight = s.nextDouble(); g.addEdge(u, v, weight); } return g; } public static void main(String[] args) throws FileNotFoundException { Scanner s = new Scanner(new File("weightedGraph.txt")); WeightedGraph g = readGraph(s); s.close(); LongestWeightedPath p = new LongestWeightedPath(g, 0, 3); System.out.println("Najdlhsia cesta: " + Arrays.toString(p.longestPath())); } } /* Priklad vstupu (ulozit do weightedGraph.txt) 5 7 0 1 1 0 2 10 0 3 1 1 2 1 2 3 10 2 4 1 3 4 1 */
Prednáška 36
- Tento týždeň nie sú cvičenia v stredu
- Do dnes (7.5. do 22:00) výber témy na projekt
- DU16 do štvrtka 9.5. do 22:00
Opakovanie backtracky na grafe
- Na minulej prednáške boli príklady na prehľadávanie grafu s návratom. Jednalo sa o hľadania ciest, ktoré zodpovedajú špecifickým požiadavkám.
- Pri riešení sme si postupne vytvárali cestu pridávaním potenciálnych vrcholov do nej a následným kontrolovaním situácie (hotové riešenie, slepá vetva)
Hľadanie maximálnej kliky
- Pozrime sa teraz na iný typ problému, nehľadáme cestu, ale množinu vrcholov
- Klika je taká množina vrcholov, v ktorej sú každé dva spojené hranou
- Maximálna klika je klika s najväčším počtom vrcholov v danom grafe
Graf G=(V,E), kde V={0,...,4} a E={{0,1},{0,2},{0,3},{1,2},{2,3},{2,4},{3,4}} obsahuje niekoľko klík veľkosti 3, ale žiadnu kliku veľkosti 4
Maximalna klika: [0, 1, 2]
Po pridaní hrany {0,4} dostávame kliku veľkosti 4:
Maximalna klika: [0, 2, 3, 4]
Hľadanie maximálnej kliky:
- Rekurzívne skúšame každý vrchol najprv pridať do kliky, potom vynechať
- Vrchol pridávame do kliky iba ak je spojený so všetkými vrcholmi, ktoré sú v klike
- Vrchol v klike veľkosti k má stupeň (počet susedov) aspoň k-1
- Preto do kliky skúšame dať iba vrcholy, ktoré majú dosť veľký stupeň na to, aby mohli patriť do kliky väčšej ako zatiaľ najväčšia nájdená (to neznamená, že do takej kliky aj patria)
- Aktuálnu kliku aj najväčšiu nájdenú si ukladáme do LinkedList-u.
/** Trieda, ktora umoznuje najst maximalnu kliku v danom grafe. */ class MaximumClique { /** samotny graf */ private Graph g; /** zoznam vrcholov v najvacsej doteraz najdenej klike */ private LinkedList<Integer> maxClique; /** zoznam vrcholov v aktualnej klike */ private LinkedList<Integer> clique; /** Konstruktor, ktory dostane graf a spusti rekurzivne vyhladavanie */ public MaximumClique(Graph g) { this.g = g; // uloz vstupny graf // vytvor dve prazdne kliky clique = new LinkedList<Integer>(); maxClique = new LinkedList<Integer>(); search(0); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda skusi pouzit aj vynechat vrchol vertex * potom skusa vsetky moznosti pre vrcholy vertex+1...n-1. */ private void search(int vertex) { // ak aktualna klika je vacsia ako doterajsie maximum, uloz ju if (clique.size() > maxClique.size()) { // konstruktor LinkedListu moze vytvorit kopiu inej Collection maxClique = new LinkedList<Integer>(clique); } // ak uz sme vycerpali vsetky vrcholy, nie je co skusat dalej if (vertex == g.getNumberOfVertices()) { return; } // otestuj, ci sa vrchol vertex da pridat do kliky // a ci ma sancu byt vo vacsej klike ako doteraz najdena if (isConnected(g, vertex, clique) && degree(g, vertex) + 1 > maxClique.size()) { // ak ano, pridaj ho do kliky a zavolaj rekurziu clique.addLast(vertex); search(vertex + 1); // odober vertex x kliky clique.removeLast(); } // preskoc vertex a zavolaj rekurziu na ostatne vrcholy search(vertex + 1); } /** pomocna metoda, ktora overi, ci vrchol vertex je v grafe g * spojeny s kazdym z vrcholov v mnozine vertexSet. */ private static boolean isConnected(Graph g, int vertex, Collection<Integer> vertexSet) { // iterujeme cez mnozinu vertexSet for(int v : vertexSet) { if (!g.existsEdge(vertex, v)) { // over hranu return false; // hrana nie je } } return true; // vsetky hrany najdene } /** pomocna metoda, ktora zisti stupen vrchola vertex v grafe g */ private static int degree(Graph g, int vertex) { // iterujeme cez susedov vrchola, zvysujeme pocitadlo result int result = 0; for(int x : g.adjVertices(vertex)) { result++; } return result; } /** vrati maximalnu kliku najdenu v grafe g */ public LinkedList<Integer> maxClique() { // vrat kopiu nasej najlepsej kliky return new LinkedList<Integer>(maxClique); } }
Príklad použitia
MaximumClique c = new MaximumClique(g); System.out.println("Maximalna klika: " + c.maxClique().toString());
Cvičenia:
- Čo by program robil, ak by sme vynechali test na stupeň vrchola z rekurzívnej funkcie?
- Čo by program robil, ak by sme vynechali aj test isConnected?
- Čo program robí, ak ho spustíme grafe bez hrán?
- Ktorá reprezentácia grafu je vhodnejšia pre tento algoritmus?
Orientované grafy
Pre orientované grafy môžeme využiť existujúci interface Graph ako sme ho mali definovaný pri neorientovaných grafoch.
- Pri implementácii funkcie addEdge však vložíme hranu iba jedným smerom. Napr. pre hranu (0,1) vložíme 1 medzi susedov 0 ale nie 0 medzi susedov 1.
- Funkcia existsEdge nám potom pre parametre (0,1) odpovie true ale pre (1,0) odpovie false.
- Iterator adjIterator bude poskytovať iba vychádzajúce hrany
Prehľadávanie do hĺbky
Pri takto implementovanom grafe, môžeme použiť prehľadávanie do hĺbky z minulých prednášok na zistenie existencie orientovanej cesty medzi dvoma vrcholmi.
- Treba si však uvedomiť, že podobne, ako existencia hrany (0,1) nehovorí nič o hrane (1,0) ani existencia cesty medzi dvomi vrcholmi nie je obojsmerná
- Keďže však máme k dispozícii iterátor cez výstupné hrany, vytvárame práve správne orientované cesty
void search(int vertex, boolean[] visited) { visited[vertex] = true; //prejdi cez vchadzajuce hrany for (int neighbor: g.adjVertices(vertex)){ // ak este sused nebol vobec navstiveny, navstiv do rekurzivne if (!visited[neighbor]) { search(neighbor,visited); // navstivime ho rekurzivne } } }
- Viacnásobným použitím funkcie vieme získať zoznamy vrcholov, ktoré sú dosiahnuteľné z jednotlivých vrcholov.
- Pri prehľadávaní si zároveň môžeme hrany deliť na stromové, ktoré objavujú nový vrchol a nestromové (tieto prípadne ešte podrobnejšie)
Cvičenie:
- Zamyslite sa čo by robil algoritmus na hľadanie komponentov, ktorý bol na minulej prednáške, na orientovanom grafe - funguje? prečo?
Prehľadávanie do šírky a najkratšie cesty v neohodnotenom grafe
Podobne ako prehľadávanie do hĺbky aj prehľadávanie do šírky je na orientovanom (neohodnotenom) grafe rovnako použiteľné.
- Získame najkratšie orientované cesty z vrchola do všetkých ostatných vrcholov
- Môžeme si hrany rozdeliť na stromové (ktoré objavili vrchol) a ostatné
Existencia cyklu, strom
Čo nás v orientovanom grafe často zaujíma je existencia orientovaného cyklu a s tým súvisiace otázky.
- Orientovaný cyklus existuje, ak vrchol je sám sebe dosiahnuteľný (pritom nerátame dosiahnuteľnosť na 0 krokov)
- Orientovaný cyklus existuje, ak pri prehľadávaní do hĺbky narazíme na svojho predchodcu (potrebujeme si pamätať aktuálnu cestu z vrcholu, ktorým sme začínali
Ďalšia otázka, ktorá nás zaujíma, či je orientovaný graf stromom. Ako by sme mohli postupovať:
- Začneme nejakým vrcholom, ktorý považujeme za koreň / tento vrchol by zjavne nemal mať žiadne hrany čo do neho vedú.
- Ak z koreňa pustíme prehľadávanie dostaneme sa do každého vrchola a nemáme žiadne nestromové hrany (počet hrán si pritom vieme overiť už pred tým).
Topologické triedenie
Motivačná úloha:
- Máme niekoľko vecí, ktoré potrebujeme vybaviť na úrade. Ale číha tam na nás byrokracia. O niekoľkých veciach vieme, že pre tým, ako vybavíme B musíme vybaviť A.
- Úlohou je nájsť poradie (a zistiť, či také vôbec existuje) ako veci na úrade vybavovať.
- Týmto nám zjavne vzniká orientovaný graf, kde vrchol sú povinnosti a hrany prerekvizície medzi nimi.
Pri otázke, či je graf acyklický môžeme buď odpovedať opačne ako pri cykle (alebo upraviť hľadanie cyklu na novú úlohu), alebo skúšať zistiť acyklickosť ináč, kúsok podobne ako pri strome. Na to však potrebujeme mierne rozšíriť grafový interface.
- Už pri strome sme na začiatku položili otázku, že či do vrchola nevstupuje žiadna hrana - túto otázku nevieme rozumne odpovedať, pretože vstupné hrany nikde pri vrchole uložené nemáme
- Rozšírime teda interface, aby si pri vrchole pamätal okrem výstupných aj vstupné hrany a vedel odovzdať ich iterátor.
interface Graph { .. Iterator<Integer> adjInIterator(int vertex); } class AdjListsGraph implements Graph { /** Zoznam susedov pre každý vrchol */ private ArrayList<ArrayList<Integer>> adjLists; /** Zoznam susedov pre každý vrchol */ private ArrayList<ArrayList<Integer>> adjInLists; .. @Override public boolean addEdge(int from, int to) { adjLists.get(from).add(to); // pridaj hranu v jednom smere adjInLists.get(to).add(from); // pridaj hranu ako vstupnu v opacnom smere numEdges++; return true; } .. @Override public Iterable<Integer> adjInVertices(int vertex) { return adjInLists.get(vertex); // vrati ArrayList } }
Samotné topologické triedenie bude pracovať nasledovne:
- Bude si vo fronte pamatať vrcholy, ktoré už môže vypísať - týchto môže byť aj viac a pri niektorých je jedno v akom poradí sú vypísané
- V prípade, že vypisujeme vrchol potrebujeme z grafu vymazať všetky hrany, ktoré z neho vchádzali do iného vrchola a spôsobovali mu vyšší vstupný stupeň
- Každému susedovi vypisovaného vrchola teda umažem vstupnú hranu (z vypisovaného vrchola)
- V prípade, že to bola jeho posledná vstupná hrana, tak vrchol zaradím na vypísanie
static void printTopological(DirectedGraph g) { Queue<Integer> s=new LinkedList<Integer>(); Iterator<Integer> it; // hladanie vrcholov so stupnom 0 for (int i=0; i<g.numVertices(); i++){ if (!g.adjInVertices(i).iterator().hasNext()) s.add(i); } while (!s.isEmpty()){ // vrcholy v zasobniku su uz vypisatelne int v=s.remove(); System.out.print(v+" "); // pre susedov vypisaneho vrchola potrebujem vymazat vstupnu hranu od vypisovaneho vrchola for (int vertex: g.adjVertices(v)){ it=g.adjInVertices(vertex).iterator(); int count=0; while (it.hasNext()){ count++; if (it.next()==v) it.remove(); //pozor aby bolo remove implementované } // a ak to nahodou bola posledna vstupna hrana, tak je vrchol u uz vypisatelny if (count==1) s.add(vertex); } } }
Cvičenie:
- Pomocou topologického triedenia sa dá určiť, či je graf acyklický. Ako? Upravte patrične program.
Ako už som spomínala, úloha ide vyriešiť aj podobnou myšlienkou ako hľadanie cyklov - teda prehľadávaním grafu do hĺbky
Organizácia skúšky
Pozri zvláštnu stránku
Zdrojový kód programu
- Topologické triedenie a detekcia cyklov v orientovanom grafe pomocou spätného prehľadávania do hĺbky
package prog; import java.io.*; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.Queue; import java.util.Scanner; /** Interface reprezentujúci graf s n vrcholmi očíslovanými 0..n-1. */ interface Graph { /** Vráti počet vrcholov grafu n. */ int numVertices(); /** Vráti počet hrán grafu. */ int numEdges(); /** Do grafu pridá hranu z vrcholu from do vrcholu to, * vráti true ak sa ju podarilo pridať. */ boolean addEdge(int from, int to); /** Vráti true, ak existuje hrana z vrcholu from * do vrcholu to. */ boolean existsEdge(int from, int to); /** Vráti iterovateľnú skupinu susedov vrchola vertex. * V prípade orientovaného grafu vracia iba hrany vychádzajúce z vrchola. * Metóda remove iterátora nemusí byť podporovaná. */ Iterable<Integer> adjVertices(int vertex); } interface DirectedGraph extends Graph { /** Vráti iterovateľnú skupinu vstupnych susedov vrchola vertex. * Metóda remove iterátora nemusí byť podporovaná. */ public Iterable<Integer> adjInVertices(int vertex); } /** Trieda reprezentujúca neorientovaný graf pomocou * zoznamov susedov uložených v poli dynamickej dĺžky (ArrayList). */ class AdjListsDiGraph implements DirectedGraph { /** Zoznam susedov pre každý vrchol (vychadzajuce hrany) */ private ArrayList<ArrayList<Integer>> adjLists; /** Zoznam susedov pre každý vrchol (vchadzajuce hrany) */ private ArrayList<ArrayList<Integer>> adjInLists; /** Počet hrán v grafe */ private int numEdges; /** Konštruktor dostane počet vrcholov grafu */ public AdjListsDiGraph(int numVertices) { adjLists = new ArrayList<ArrayList<Integer>>(numVertices); adjInLists = new ArrayList<ArrayList<Integer>>(numVertices); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<Integer>()); adjInLists.add(new ArrayList<Integer>()); } numEdges = 0; } @Override public int numVertices() { return adjLists.size(); } @Override public int numEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { adjLists.get(from).add(to); // pridaj hranu v oboch smeroch adjInLists.get(to).add(from); numEdges++; return true; } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); // pozri do zoznamu susedov pre from } @Override public Iterable<Integer> adjVertices(int vertex) { return adjLists.get(vertex); // vrati ArrayList } @Override public Iterable<Integer> adjInVertices(int vertex) { return adjInLists.get(vertex); // vrati ArrayList } } /** Trieda obsahujuca rozdelenie vrcholov grafu do komponentov suvislosti */ class TopologicalSort { /** samotny graf */ private DirectedGraph g; /** Zoznam vrcholov v topologickom usporiadani */ private ArrayList<Integer> order; /** Indikator, ci je graf acyklicky */ private boolean acyclic; /** Pole indikujuce, ci sme uz vrchol v prehladavani navstivili */ private boolean[] visited; /** Pole indikujuce, ci sme uz ukoncili prehladavanie vrchola * a vsetkych jeho predchodcov */ private boolean[] finished; /** Konstruktor, ktory dostane graf a prehladavanim do hlbky * testuje acyklickost grafu a hlada topologicke usporiadanie */ public TopologicalSort(DirectedGraph g) { this.g = g; // uloz graf int n = g.numVertices(); // pocet vrcholov grafu order = new ArrayList<Integer>(); // inicializuj polia visited = new boolean[n]; finished = new boolean[n]; for (int i = 0; i < n; i++) { visited[i] = false; } acyclic = true; // zatial sme nevideli cyklus // prechadzaj cez vrchol a ak najdes nevyfarbeny, // spusti prehladavanie proti smeru hran for (int i = 0; i < n; i++) { if (!visited[i]) { search(i); } } } /** Pomocna rekurzivna metoda pouzivana v konstruktore * na vyfarbenie vsetkych predchodcov vrchola vertex */ private void search(int vertex) { visited[vertex] = true; // uz sme ho navstivili //prejdi cez vchadzajuce hrany for (int neighbor: g.adjInVertices(vertex)){ // ak uz sme suseda navstivili, ale este nie je // ukonceny, mame cyklus if (visited[neighbor] && !finished[neighbor]) { acyclic = false; } // ak este sused nebol vobec navstiveny, navstiv do rekurzivne if (!visited[neighbor]) { search(neighbor); // navstivime ho rekurzivne } } // ukoncili sme prehladavanie aktualneho vrcholu // poznac ako ukonceny a pridaj ho do zoznamu // - jeho predchodcovia tam uz su finished[vertex] = true; order.add(vertex); } /** vratil, ci je vstupny graf acyklicky */ public boolean isAcyclic() { return acyclic; } /** ak je graf acyklicky, vrati topologicke usporiadanie vrcholov */ public ArrayList<Integer> order() { if (!acyclic) { return null; } return new ArrayList<Integer>(order); } } public class Prog { /** Zo scannera s načíta graf vo formáte: počet vrcholov, * počet hrán a zoznam hrán zadaných koncami vrcholov. * Uloží ho ako AdjMatrixGraph. */ static DirectedGraph readGraph(Scanner s) { int n = s.nextInt(); int m = s.nextInt(); DirectedGraph g = new AdjListsDiGraph(n); for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; } public static void main(String[] args) throws FileNotFoundException { Scanner s = new Scanner(new File("graph.txt")); DirectedGraph g = readGraph(s); s.close(); TopologicalSort sort = new TopologicalSort(g); if (sort.isAcyclic()) { System.out.println("Topologicke usporiadanie: " + sort.order()); } else { System.out.println("Graf ma cyklus"); } } } /** Priklad vstupu bez cyklu: 4 4 1 0 2 0 2 1 3 1 * s cyklom: 4 4 1 0 0 2 2 1 3 1 */
Cvičenia 25
Cieľom tohto cvičenia je precvičiť si prehľadávanie grafu do šírky a úlohy na backtracking na grafoch.
- Prehľadávanie do šírky nájdete v programe pre prednášku 34, trieda ShortestPaths
- Program na hľadanie ciest dĺžky k nájdete v programe pre prednášku 35, trieda FixedLengthPaths, resp. trieda LongestPath pre najdlhšiu cestu
Úloha A: Odsimulujte prácu prehľadávania do šírky na grafe vpravo ak začneme prehľadávať vo vrchole 0. Predpokladajte, že adjVertices vracia susedov v poradí podľa čísla vrcholu. Uveďte, v akom poradí boli vrcholy vložené do queue a vypíšte výsledné polia dist a prev.
Úloha B: Program na hľadanie najdlhšej cesty z u do v v neohodnotenom grafe zmeňte na hľadanie najdlhšieho cyklu, ktorý začína a končí v danom vrchole v
- Cyklus je postupnosť vrcholov Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_0,v_1,...,v_k} taká, že každé dva za sebou idúce vrcholy Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_i} a Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_{i+1}} sú spojené hranou, Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_0=v_k} a žiaden iný vrchol sa v postupnosti neopakuje.
Úloha C: Vráťme sa k príkladu s koňom, ale namiesto prehľadávania do hĺbky použite prehľadávanie do šírky
- Máme danú šachovnicu n x m, na ktorej sú niektoré políčka obsadené figúrkami. Na políčku (i,j) stojí kôň, ktorý sa pohybuje obvyklým spôsobom, ale môže chodiť iba po prázdnych políčkach.
- Pre každé prázdne políčko v šachovnici spočítajte, na aký najmenší počet ťahov na neho vie kôň doskákať. Ak naňho vôbec nevie doskákať, vypíšte -1.
- Použite prehľadávanie do šírky na grafe, v ktorom sú vrcholy jednotlivé políčka a hrany spájajú dvojice voľných políčok, medzi ktorými môže kôň skočiť. Môžete vytvoriť graf v niektorej reprezentácii, alebo to riešiť priamo v matici dist n x m.
- Šachový kôň môže z pozície (i,j) skočiť na jednu z pozícií (i+2,j+1), (i+2,j-1), (i-2,j+1), (i-2,j-1), (i+1,j+2), (i+1,j-2), (i-1,j+2), (i-1,j-2)
Úloha D: Program na hľadanie všetkých ciest dĺžky k z u do v v neohodnotenom grafe zmeňte tak, aby si predpočítal najkratšiu cestu z každého vrcholu do cieľového vrcholu (prehľadávaním do šírky z v) a potom nepokračoval ďalej v prehľadávaní z daného vrchola x, ak už nám v ceste zostáva len nejakých q hrán a najkratšia cesta z x do cieľového vrchola v má dĺžku viac ako q
Prednáška 37
Organizácia
- Zajtra posledné cvičenia (s rozcvičkou)
- Pondelok 20.5. o 10:00 v posluchárni B záverečný test
- Streda 22.5. prvý termín skúšky, druhý riadny termín 5.6., nasledujú už iba opravné
- Streda 22.5. o 22:00 odovzdanie nepovinného projektu
- Štvrtok 23.5. o 10:00 predvádzanie projektov
- Do budúceho pondelka si skontrolujte body z DÚ a rozcvičiek v Moodli a ohláste nam prípadné reklamácie
- Ozvite sa aj ak máte ospravedlnenie na viac ako jednu zmeškanú rozcvičku
- Okrem zajtrajšej rozcvičky a posledných dvoch DÚ (15,16) by všetky body mali byť v Moodli
- Posledné body budú do budúceho pondelka
- Nezabudnite sa zúčastniť študentskej ankety
- V nedeľu 2.6. Katedra informatiky organizuje Výlet 7P pre učiteľov, študentov, absolventov a ďalších záujemcov. Viac na http://new.dcs.fmph.uniba.sk/index.php/Aktualne/7P
Zhrnutie
Čo by ste mali po dvoch semestroch vedieť
- Základy jazykov C/C++, Java: cykly, podmienky, premenné, funkcie, primitívne typy, polia, alokovanie pamäte, reťazce, súbory
- Základy OOP, triedy, dedenie, polymorfizmus, výnimky, generické programovanie
- Základy tvorby GUI v Swingu
- Dátové štruktúry: spájaný zoznam, zásobník a rad, binárne stromy a ich využitie (vyhľadávacie, lexikografické, aritmetické, rozhodovacie,...), hashovacie tabuľky
- Základné algoritmy: triedenia, binárne vyhľadávanie, prehľadávanie grafov a stromov, prehľadávanie s návratom
- Vymyslieť jednoduchý algoritmus, vedieť ho napísať a odladiť, porozumieť hotovým programom
Nadväzujúce predmety
- Algoritmy a dátové štruktúry (2/Z) a Tvorba efektívnych algoritmov (3/Z): viac algoritmov a dátových štruktúr, časová zložitosť
- Programovanie (3) (2/Z): viac programovania v Jave, návrhové vzory
- Ročníkový projekt (1) a (2): píšete väčší program na tému podľa vlastného výberu
Otázky ku skúške
- Potrebné informácie a cvičné príklady nájdete na stránke #Letný semester, test a skúška
Úvod do OOP v C++
Dve doplnkové témy, nebudeme skúšať:
- Objektovo-orientované programovanie v C++
- Ilustračný príklad: použitie triedenia, spracovanie intervalov
Dva príklady na prácu s intervalmi
Máme danú množinu intervalov [a1,b1],[a2,b2],...
- Môžu predstavovať napríklad nejaké udalosti v kalendári (prednášky, rozpis služieb,...)
- Napríklad {[2,4],[1,3],[6,7],[5,8]}
Jednoduchá úloha: Pretínajú sa nejaké dva intervaly v množine?
- Kedy sa pretínajú intervaly [a,b] a [c,d]?
Jednoduchý algoritmus:
- porovnáme každé dva intervaly
- zložitosť O(n^2)
Rýchlejší algoritmus:
- utriedime intervaly podľa začiatku {[1,3],[2,4],[5,8],[6,7]}
- porovnávame len susedné dvojice
- ak sa žiadne nepretínajú, nepretínajú sa ani žiadne iné
Časová zložitosť:
- Triedenie O(n log n), napr. MergeSort
- Prechod poľom O(n)
- Spolu O(n log n)
Triedenie nám často umožní spraviť rýchlejší alebo jednoduchší algoritmus
Výpočet dĺžky zjednotenia intervalov
- Chceme zrátať dĺžku zjednotenia množiny
- Napr, koľko hodín do týždňa prebiehajú na matfyze prednášky
- Príklad:
- množina intervalov {[2,4],[1,3],[6,7],[5,8]}
- zjednotenie intervalov {[1,4],[5,8]}
- dĺžka zjednotenia 6 (dva disjunktné intervaly dĺžky 3)
Ako na to:
- Utriedime intervaly podľa začiatku: {[1,3],[2,4],[5,8],[6,7]}
- Prechádzame zoznam intervalov a pamätáme si aktuálny interval zo zjednotenia [a,b]
- Ak sa práve prezeraný interval [c,d] zo vstupu pretína s intervalom [a,b], pridáme ho k nemu, t.j. do [a,b] uložíme zjednotenie [a,b] a [c,d]
- Ak sa nepretínajú, znamená to, že b<c, a teda ani žiadny ďalší interval zo vstupu sa nebude s [a,b] pretínať
- Dĺžku [a,b] teda pridáme k doteraz spočítanej dĺžke a do [a,b] priradíme [c,d]
- Zložitosť opäť O(n log n)
Implementácia v C++
#include <algorithm> #include <vector> #include <iostream> using namespace std; /** Trieda reprezentujuca uzavrety interval s celociselnymi * suradnicami oboch koncov. */ class Interval { public: /** Konstruktor so zadanymi suradnicami zaciatku a konca */ Interval(int newStart, int newEnd); /** Vrati lavy koniec intervalu */ int getStart() const; /** Vrati pravy koniec intervalu */ int getEnd() const; /** Vrati dlzku intervalu */ int length() const; /** Porovna intervaly najprv podla laveho konca, * pri rovnosti podla praveho. */ bool operator <(const Interval &other) const; /** Spocita najmensi interval obsahujuci zadane dva intervaly */ Interval operator +(const Interval &other) const; private: int start; int end; }; Interval::Interval(int newStart, int newEnd) : start(newStart), end(newEnd) { } int Interval::getStart() const { return start; } int Interval::getEnd() const { return end; } int Interval::length() const { return end - start; } bool Interval::operator<(const Interval& other) const { return start < other.start || (start == other.start && end < other.end); } Interval Interval::operator +(const Interval& other) const { return Interval(min(start, other.start), max(end, other.end)); } /** Pre vektor intervalov utriedenych podla zaciatku * spocita dlzku pokrytu ich zjednotenim. */ int unionLength(const vector <Interval> &a) { if (a.empty()) { return 0; } int length = 0; // doteraz namerana dlzka Interval cur = a[0]; // aktualny interval zo zjednotenia for (int i = 1; i < (int) a.size(); i++) { if (a[i].getStart() > cur.getEnd()) { // cur a a[i] sa nepretinaju length += cur.length(); cur = a[i]; } else { // cur a a[i] sa pretinaju cur = cur + a[i]; // do cur dame zjednenie s a[i] } } length += cur.length(); return length; } int main() { vector <Interval> a; Interval i1(2, 4), i2(1, 3); Interval *pi = new Interval(6, 7); a.push_back(i1); a.push_back(i2); a.push_back(*pi); a.push_back(Interval(5, 8)); sort(a.begin(), a.end()); cout << unionLength(a) << endl; }
Základy OOP v C++
Viac informácií nájdete napríklad v týchto dvoch tutoriáloch:
- http://pages.cs.wisc.edu/~hasti/cs368/CppTutorial/
- http://www.horstmann.com/ccj2/ccjapp3.html
- Základy použitia STL knižnice (generické triedy) pozri #Prednáška 25
Niektoré rozdiely medzi triedami v Jave a C++:
- V C++ oddeľujeme deklaráciu triedy (premenné a hlavičky metód) od implementácie metód
- Implementáciu niektorých veľmi krátkych metód môžeme dať priamo do deklarácie
- V dlhšom programe dáme deklaráciu napr. do súboru Interval.h a implementácie metód do súboru Interval.cpp
- Ak chceme používať intervaly, dáme do hlavičky #include "Interval.h"
- Na rozdiel od Javy pomenovanie súborov nemusí sedieť s menani tried
- Deklarácia triedy rozdelená na časti public, private, protected, za deklaráciou bodkočiarka
- Pri implementácii musíme pred názov dať meno triedy napr. Interval::length
- Štruktúra struct je skoro to isté ako trieda, iba default prístup je public
Konštruktor
- premenné objektu môžeme inicializovať pred začiatkom tela konštruktora
Interval::Interval(int newStart, int newEnd) : start(newStart), end(newEnd) { }
- môžeme to však spraviť aj obyčajnými priradeniami
Interval::Interval(int newStart, int newEnd) { start = newStart; end = newEnd; }
- prvý spôsob vhodný ak premenná je objekt - do zátvorky dáme parametre konštruktora
class SomeClass { Interval myInterval; SomeClass(int start, int end); ... }; SomeClass::SomeClass(int start, int end) : myInterval(start, end) { }
- použitie konštruktora:
- inicializácia lokálnej premennej i1: Interval i1(2, 4)
- vytvorenie nového objektu pomocou new: Interval *pi = new Interval(2, 4);
- vytvorenie anonymnej dočasnej premennej, ktorá sa inicializuje a nakopíruje do vektora: a.push_back(Interval(5, 8));
Preťaženie operátorov
- Väčšinu operátorov, ktoré majú aspoň jeden operand objekt, môžeme preťažiť, teda vymyslieť im vlastný význam
- V našom príklade preťažujeme < (využije sa pri triedení) a + (vytvára najmenší interval pokrývajúci oba operandy)
- Viac #Prednáška 25
Const
- Niektoré metódy sú označené ako const, čo znamená, že nemenia objekt
- Naopak parametre metód môžu byť const referencie
- Nemusia sa kopírovať, ale máme zaručené, že nebudú zmenené
bool Interval::operator<(const Interval& other) const { return start < other.start || (start == other.start && end < other.end); }
Aritmetický strom v C++
- verzia v Jave, pozri #Prednáška 28
class Node { public: virtual int evaluate() const = 0; // abstraktna metoda virtual void print() const = 0; // abstraktna metoda virtual ~Node() { } // destruktor s prazdnym telom }; class BinaryNode : public Node { public : BinaryNode(Node *newLeft, Node *newRight); virtual ~BinaryNode(); protected: Node *left, *right; }; BinaryNode::BinaryNode(Node *newLeft, Node *newRight) : left(newLeft), right(newRight) { } BinaryNode::~BinaryNode() { delete left; delete right; } class ConstantNode : public Node { public : ConstantNode(int newValue); virtual int evaluate() const; virtual void print() const; private: int value; }; ConstantNode::ConstantNode(int newValue) : Node(), value(newValue) { } int ConstantNode::evaluate() const { return value; } void ConstantNode::print() const { cout << value; } class PlusNode : public BinaryNode { public: PlusNode(Node *newLeft, Node *newRight); virtual int evaluate() const; virtual void print() const; }; PlusNode::PlusNode(Node* newLeft, Node* newRight) : BinaryNode(newLeft, newRight) { } int PlusNode::evaluate() const { return left->evaluate()+right->evaluate(); } void PlusNode::print() const { cout << "("; left->print(); cout << "+"; right->print(); cout << ")"; } void treeTest() { Node * tree = new PlusNode(new PlusNode(new ConstantNode(2), new ConstantNode(1)), new ConstantNode(5)); tree->print(); cout << endl << tree->evaluate() << endl; delete tree; }
OOP v C++ (pokračovanie)
Dedenie
- BinaryNode je podtriedou Node:
class BinaryNode : public Node { public : BinaryNode(Node *newLeft, Node *newRight); virtual ~BinaryNode(); protected: Node *left, *right; };
- V hornej časti konštruktora môžeme zavolať konštruktor nadtriedy:
PlusNode::PlusNode(Node* newLeft, Node* newRight) : BinaryNode(newLeft, newRight) { }
- Pozor, ak chceme, aby sa metódy správali polymorfne, musia byť deklarované ako virtual
- Abstraktné metódy sa píšu takto: virtual int evaluate() const = 0;
Deštruktor
- Špeciálna metóda, opak konštruktora
- spúšťa sa automaticky pri zániku objektu (keď na objekt spustíme delete alebo ak objekt v lokálnej premennej a funkcia končí)
- zvyčajne sa používa na odalokovanie pamäte, prípadne ďalšie upratovanie
- deštruktory sú často virtuálne
- po skončení deštruktora sa zavolá deštruktor nadtriedy
BinaryNode::~BinaryNode() { delete left; // rekurzivne zavolá deštruktor na ľavý podstrom delete right; }
Uloženie v pamäti
V Jave:
- každá premenná buď primitívny typ alebo referencia na objekt/pole
- všetky objekty alokované cez new
- odalokované automaticky
- do premennej typu nejaká trieda môžeme priradiť aj referenciu na objekt z podtriedy
V C++:
- premenná môže priamo obsahovať objekt, alebo referenciu na objekt alebo smerník
- naopak smerník môže ukazovať aj na primitívny typ
- objekty alokované cez new treba zmazať
- do premennej typu nejaká trieda sa objekty kopírujú
- môžeme predefinovať operátor priradenia a copy konštruktor, aby fungovali ako treba
- nemôžeme priradiť objekt podtriedy, lebo by sa nemusel zmestiť
- smerníky fungujú podobne ako referencie v Jave, použijeme ak chceme využiť polymorfizmus
Ešte jedna úloha s intervalmi
Daná množina intervalov, nájdite bod, ktorý je pokrytý najväčším počtom intervalov
- napr. interval predstavuje čas, keď má niekto voľno, hľadáme vhodný čas na stretnutie
Cvičenia 26
Na dnešnom cvičení máme dve úlohy: stručne si zopakovať topologické triedenie a vyskúšať si naprogramovať prehľadávanie s návratom v programe #GraphGUI, ktorý budete používať na skúške.
Úloha A: Nájdite všetky topologické usporiadania orientovaného grafu na obrázku vpravo.
Úloha B: Budeme pracovať s programom #GraphGUI, ako na cvičeniach 24. Do súboru GraphAlgorithm.java doprogramujte, aby sa po stlačení tlačidla Action spustil algoritmus hľadania najväčšej kliky a vrcholy v tejto klike aby boli vyfarbené čiernou farbou (statická premenná Color.black). Algoritmus upravte z prednášky (trieda MaximumClique). Metóda getMessage vráti reťazec vo formáte "Pocet vrcholov kliky: 6".
Úloha C: Riešenie úlohy B upravte tak, aby algoritmus nehľadal najväčšiu kliku v celom grafe, ale najväčšiu kliku, ktorá obsahuje vybraný vrchol, ktorý GraphAlgorithm dostane ako parameter. Ak hodnota tohto vrcholu je null, konštruktor vyhodí výnimku typu NoSuchElementException.
Úloha D: Napíšte program, ktorý pomocou prehľadávania s návratom nájde najmenšiu dominujúcu množinu v grafe.
- Dominujúca množina je taká množina vrcholov X, že každý vrchol grafu je buď v X, alebo susedí s nejakým vrcholom v X.
- V každom rekurzívnom volaní vyskúšajte pre jeden vrchol dve možnosti: patrí do dominujúcej množiny alebo nepatrí. Potom rekurzívne zavolajte prehľadávanie pre všetky možnosti ďalších vrcholov.
- Ak ste už spravili rozhodnutie pre každý vrchol, skontrolujte, či je výsledok naozaj dominujúca množina a porovnajte veľkosť s najlepšou zatiaľ nájdenou.
- Viete orezať nejaké neperspektívne vetvy výpočtu? Skúste napríklad niektorú z týchto stratégií:
- napr. nechceme pridať do X vrchol, ktorý nijako nepomôže, lebo aj on aj všetci jeho susedia už majú suseda v X
- alebo nechceme vynechať vrchol, ak niektorý z jeho susedov nemá ešte suseda v X a ani nemá suseda s väčším číslom, ktorý ešte môže byť pridaný do X v ďalších rozhodnutiach
- ak ste už našli dominujúcu množinu určitej veľkosti, nemá zmysel skúmať a rozširovať množiny, ktoré majú zaručene viac prvkov