A szöveg csak Firefox böngészőben jelenik meg helyesen. Használja a fenti PDF file-ra mutató link-et a letöltésre. Az első részben az idei Közép-Európai Informatikai Diákolimpia (CEOI) Kacifántos kerítés című feladatát oldottuk meg egy egyszerű, de nem elég hatékony algoritmussal. A megoldás a kerítésen keresett téglalapokat úgy, hogy végighaladt a kerítéselemeken és minden lehetséges bal felső csúcs magasságához megkereste a legtávolabbi jobb alsó csúcsot, majd egy kombinatorikai képlettel megadta az ebben a részben lévő téglalapokat. A megoldás hatékonyságának növelése érdekében a kerítés egy más felosztását kellett találnunk, ahol nincs szükség keresésre. Ez a felosztás a kerítés alakjából következik: azok a csúcsok egy kerítéselemen, amelyek koordinátája nagyobb mindkét szomszédos kerítéselem magasságánál csak olyan téglalapok csúcsai lehetnek, amelyek abban a kerítéselemben vannak. Az ezekből választott bal felső csúcsok jobb alsó párja a kerítéselem jobb alsó csúcsa, tehát azonnal adódik, keresésre nincs szükség. Nézzük például a hátsó belső borítón megtalálható 1. ábrán a négyszöget.
A rajta található téglalapok koordinátájú bal felső csúcsaihoz csak olyan jobb alsó csúcsok tartozhatnak, amelyek csak ezen a kerítéselemen lehetnek, a legtávolabbi jobb alsó csúcs a . Ugyanakkor az is látszik, hogy az koordinátájú csúcsok csak olyan téglalapok jobb alsó csúcsai lehetnek, amelyek bal felső csúcsa ebben a kerítéselemben van, és a bal felső csúcs koordinátája -nél nagyobb. Miután megszámoltuk az bal felső csúcsokhoz tartozó téglalapokat, ezeket a pontokat elhagyhatjuk, mivel jobb alsó csúcsként is már megszámoltuk őket. Tehát a számolás után a kerítés egyszerűsíthető: a szomszédai közül kiemelkedő kerítéselem mindkét szomszédjánál magasabban fekvő csúcsai elhagyhatók. Így egy olyan kerítéselemünk marad, amely valamelyik szomszédjával azonos magasságú. Az előbbi példában a és csúcsokkal határolt kerítéselem felső vonalát a továbbiakban a és csúcsok alkotják. Az előző lépés tovább folytatható: a téglalap (amely már két kerítéselem része) csúcsaival képzett téglalapok megszámolása után az csúcsok elhagyhatók. Folytatásként az téglalap, majd a téglalap következik. Ez az utolsó lépés természetesen csak akkor következhet, ha a és csúcsok által határolt részben lévő téglalapokat már megszámoltuk. Ezek az elhagyások addig folytathatók, amíg végül egy téglalap marad a kerítésből. Az ezen található téglalapok megszámolása az előzőekhez hasonlóan történhet. A bemutatott eljárás minden lépésében egy ,,kiemelkedő'' téglalapot kapunk, amelynek alsó és jobb oldala kivételével minden pontja olyan téglalapok bal felső csúcsa, melyekhez csak az adott kiemelkedésben és alatta találhatóak a számolásnál figyelembe veendő jobb alsó csúcsok. Az előbbi példában bemutatott téglalap esetében a bal felső csúcsok koordinátáira igaz, hogy és , és a hozzájuk tartozó jobb alsó csúcsok koordinátáira teljesül, hogy és . Nézzük általánosan, tehát legyen egy, az eljárásban talált téglalap bal felső csúcsa és jobb alsó csúcsa (. Az elhagyása előtt megszámolandó téglalapok bal felső csúcsának koordinátáira teljesül, hogy és , míg jobb alsó csúcsainak koordinátáira igaz, hogy és . Egy adott bal felső csúcshoz tartozó téglalapok száma tehát . Ezeket összegezve a lehetséges csúcsokra a téglalapok száma: | | Ebben az összegben minden előforduló tényezőt megszorzunk minden értékkel. A szorzás és az összeadás asszociativitása következében ezt úgy is végezhetjük, hogy először összegezzük a tényezőket külön-külön, majd az így kapott mennyiségeket szorozzuk össze. Tehát a kifejezés kevesebb szorzással fölírva: | | Ebben két számtani sorozat szorzata szerepel, tehát az összegképlet alapján az eredmény: | | A téglalapok megszámolását tehát megkapjuk a fenti eredmény alapján. Most már csak az a kérdés, hogy miként találjuk meg az ,,elhagyható'' kerítésrészeket, amelyek a szomszédos kerítésrészeknél magasabbak. Az algoritmus nem végezhet a kerítéselemeken átnyúló keresést, tehát például a kerítéselemek eredeti sorrendjében kellene haladnia.
1. feladat: Vizsgáljuk meg az 1. ábrát, és vegyük észre, hogy milyen jellemző tulajdonságok alapján találjuk meg a keresett kiemelkedéseket!
Haladjunk a kerítés felső vonalán. Az első kiemelkedés a , amelyet az csúcs zár. Ezt a részt elhagyva és a kerítés vonalán lefelé haladva a pontban érünk a csúcs magasságába, így a téglalapot találjuk. Ennek levágása után a kerítés vonalán tovább menve a csúcsban érünk egy kiemelkedés legjobboldalibb és legalsó pontjához, így elhagyhatjuk a téglalapot. Megfigyelhető, hogy a kerítés vonalán egy csúcstól az alatta lévő csúcsig haladva találunk olyan pontot, amely egy kiemelkedés jobb alsó sarka. A megtaláláshoz csak azt kell tudnunk, hogy a vizsgált kerítésrész jobb oldalához melyik bal oldal tartozik. Nézzük a borítón lévő 2. ábrát.
Az első lefelé haladás az szakaszon történik, ahol az csúcs egy jobb alsó sarok, melynek bal alsó párja a kiemelkedés bal oldalán, a szakaszon található. Számoljuk meg a talált kerítésrészhez tartozó téglalapokat, majd hagyjuk el a és csúcsokat, és vegyünk föl egy új csúcsot, -t. A kerítés vonalán a következő lefelé haladás a szakaszon történik, a bal oldal pedig továbbra is a szakaszra esik, így a szakasz feletti rész téglalapjai megszámolandók és a , , csúcsok elhagyhatók. Az ábrán a kiemelkedések jobb alsó csúcsától a bal alsó csúcsáig egy-egy nyíl mutat. Továbbhaladva a kerítés vonalán lefelé a pontba jutunk, azonban a hozzá tartozó bal oldal már nem a egyenesre esik, hanem az attól balra eső szakaszra. Ez éppen az a rész, ahol a szakaszt megelőzően fölfelé haladtunk. Így látható, hogy a kerítés vonalán a fölfelé és lefelé haladó mozgások szakaszai párba állíthatók. Ha továbbhaladunk a kerítés vonalán, akkor megfigyelhetjük ezeket a párokat az részen haladva is. Itt az utolsó szakaszon lefelé mozogva a ponthoz tartozó bal alsó csúcs ismét az szakaszra esik, miután az fölötti kerítésrészt is elhagytuk.
2. feladat: Adjuk meg, hogyan lehetne könnyen megtalálni egy-egy ,,kiemelkedés'' jobb alsó csúcsához a bal alsó csúcsot, tehát a 2. ábrán jelölt nyilak végpontját! Induljunk el a kerítés bal alsó csúcsától fölfelé és haladjunk végig a kerítés vonalán annak jobb oldali legalsó csúcsáig. Minden fölfelé haladásnak megvan a lefelé haladás párja a kerítés vonalán való mozgás során. Ezek a párok alkotják a kiemelkedések bal és jobb oldali szakaszát. A párok úgy következnek a kerítés vonalán, mint egy kifejezés nyitó és csukó zárójelei, vagyis egymásba ágyazva. Ha egy felfelé mozgás megelőz egy másikat, akkor a hozzá tartozó lefelé haladás később jön, a másikhoz tartozó lefelé mozgás után. Ez egy helyesen zárójelezett kifejezésnél is pontosan így van. Amikor egy kifejezést kiértékelünk, akkor a legbelső zárójelben lévő részt számoljuk ki, majd a zárójeleket elhagyva folytatjuk a számolást. Itt pontosan ugyanezt kell tennünk, csak a kifejezés helyett a téglalapok megszámolását végezzük el, majd a kiemelkedő részt elhagyjuk, ahogyan a kifejezésnél a zárójeleket. Az egymásba ágyazás, a belső párok elhagyása azt sugallja, hogy használjunk vermet a bal oldali szakaszok tárolására. A verem tetején mindig az a szakasz lesz, amelynek jobb oldali párja elsőként következik. A kerítés vonalán való haladás során minden felfelé mozgás egy új pár bal oldalát helyezi a veremre, míg a lefelé haladás egy párt vesz majd ki a veremből. Illetve ez csak akkor teljesül, ha a felfelé és lefelé haladás ugyanolyan magasról indult, tehát például a 2. ábrán az kiemelkedésnél. Általában kicsit összetettebb a helyzet. Például az csúcstól fölfelé mozogva először tegyük a verem tetejére az szakaszt, majd a szakaszon lefelé haladva a verem tetején lévő szakaszt cseréljük az szakaszra. Továbbhaladva a kerítésen az szakaszon lefelé változtassuk a verem tetején lévő szakaszt -re. Innen továbblépve a szakaszon lefelé haladva vesszük ki az szakaszt a veremből. Figyelnünk kell tehát, hogy a lefelé mozgások során a verem tetején lévő szakasz alsó csúcsa és a lefelé mozgás szakaszának alsó csúcsa hogyan viszonyul egymáshoz. Innen látszik, hogy elegendő a veremben a felfelé haladó szakaszok alsó csúcsát elhelyezni, a felső csúcsra nincs szükség. A program elkészítéséhez először be kell olvasnunk az adatokat, majd azokból meg kell adnunk a kerítés vonalán haladáskor érintett csúcsokat. Az 1. ábrán ezeket a csúcsokat betűkkel jelöltük, melyeken ABC rendben fogunk végighaladni. Ezek a csúcsok a bemenetként kapott kerítéselemek magasságából és szélességéből számíthatók, a bemenet sorrendjében követik egymást. Így a bemenet feldolgozásakor egy lineáris futásidejű algoritmussal megadhatjuk a csúcsokat. Ennek a programrésznek az elkészítését az olvasóra bízzuk. Feltételezzük a továbbiakban, hogy a következő programrészek futása előtt már rendelkezésünkre áll egy méretű pont[0..2N+1] tömb, amely sorrendben tartalmazza a kerítés vonalán érintett csúcsok koordinátáit. Ezen előkészítés után következzen a kerítés vonalának bejárása. Vegyünk föl egy v vermet, amely a pont[ ] tömb csúcsainak sorszámát tárolja, azaz a futás során legföljebb csúcs indexét. Helyezzünk a verem tetejére egy -t, az első csúcs sorszámát. Amikor ez a csúcs kikerül a verem tetejéről, akkor bejártuk a kerítés teljes vonalát, tehát az üres verem jelzi a műveletsor végét. A bal és jobb változók mutatják, hogy a bejárás melyik csúcsokat vizsgálja. Kezdetben az első kerítéselem két felső pontjának sorszámát tartalmazzák.
1. Függvény KacifantosKerites2(N,h[0..N-1],w[0..N-1]) : Egész BeolvasasCsucsepites(N,h,w,pont) darab := 0 v.Verembe(0) bal := 1 jobb := 2 Ciklus amíg nem v.Üres( )
A végeredményt adó függvényt több részletben írjuk le, tehát az algoritmusrészletek és magyarázatok felváltva követik egymást. A jobb érthetőség érdekében a függvény sorait számozzuk. A ciklusmagban egy elágazás szerepel, amelyben először megállapítjuk, hogy a kerítésen történő mozgás a jobb és az utána következő csúcs között milyen irányú. Ha felfelé haladunk a kerítés vonalán, akkor vermeljük a jobb oldali alsó csúcsot, vagyis megjegyezzük, hogy itt van egy kerítésrész bal oldali alsó csúcsa, majd továbblépünk a következő kerítéselemre. Ha azonos magasságú kerítéselemek vannak egymás mellett, akkor egyszerűen átmegyünk a következőre.
Ha pont[jobb].y pont[jobb+1].y akkor v.Verembe(jobb) bal := jobb+1 jobb := jobb+2 egyébként ha pont[jobb].y = pont[jobb+1].y akkor jobb := jobb+2 egyébként
Az elágazás utolsó része a leginkább összetett, vagyis amikor lefelé haladunk a kerítés vonalán, tehát amikor . Ekkor három esetet különböztetünk meg aszerint, hogy a verem tetején lévő csúcs milyen magasan van a jobb után következő csúcshoz képest.
Ha pont[v.Felső( )].y pont[jobb+1].y akkor szamol(darab,pont[v.Felső( )].x, pont[jobb].y, pont[jobb].x, pont[v.Felső( )].y) pont[jobb].y := pont[v.Felső( )].y v.Veremből( ) bal := v.Felső( )+1
Amikor a verem tetejéhez tartozó csúcs van magasabban, akkor csak az ő magasságáig lévő téglalapot mint kiemelkedést számoljuk meg és hagyjuk el. A példában ilyen eset a szakaszon történő lefelé mozgás. Ekkor a veremben az és a csúcs indexe van, a tetején a csúcs sorszáma. Mivel a csúcs magasabban van, mint a csúcs, ezért a jobb indexnél lévő csúcsot módosítjuk -re (ezzel hagytuk el a kiemelkedést), majd kivesszük a veremből a csúcs sorszámát, ahol most egyedül az csúcs indexe van. Ezután megadjuk a bal változóban a bal oldali felfelé menő rész felső csúcsának indexét, a példában a csúcsot. Így a folytatásként szóba jöhető kiemelkedés a bal oldala az szakasz, jobb oldala a szakasz.
egyébként ha pont[v.Felső( )].y = pont[jobb+1].y akkor szamol(darab, pont[v.Felső( )].x, pont[jobb].y, pont[jobb].x, pont[v.Felső( )].y) v.Veremből( ) Ha nem v.Üres( ) akkor bal := v.Felső( )+1 jobb := jobb+2 Elágazás vége
Amikor a verem legfelső eleme által hivatkozott csúcs azonos magasságban van a lefelé mozgás alsó csúcsával, akkor a kiemelkedés megszámolása után elhagyjuk ezt a kerítésrészt, vagyis kivesszük a veremből a tetején lévő elemet és továbblépünk előre a jobb változóval. Természetesen ezt csak akkor tehetjük meg, ha nem a -ás csúcsot vettük ki a veremből, hiszen azzal már befejeztük a bejárást. Az ábrán például az kiemelkedésnél az csúcsra mutat a verem felső száma és a szakaszon mozgunk lefelé. Ekkor a számolás után a verem tetején az csúcs sorszáma található, a bal változó a csúcsra, míg a jobb a csúcsra hivatkozik.
egyébként szamol(darab, pont[v.Felső( )].x, pont[jobb].y, pont[jobb].x, pont[jobb+1].y) pont[bal].y := pont[jobb+1].y jobb := jobb + 2 Elágazás vége Elágazás vége Ciklus vége KacinfatosKerates2 := darab 35. Függvény KacifantosKerites2 vége
A harmadik eset az a lehetőség, amikor a verem teteje által mutatott csúcs magassága kisebb, mint a lefelé mozgás alsó csúcsának magassága. Ekkor szintén megszámoljuk a kiemelkedő téglalaprészben lévő téglalapokat, majd a bal oldali szakasz felső csúcsának magasságát állítjuk a jobb oldali szakasz alsó csúcsának magasságára. Ez történik a példában, amikor az szakaszon mozgunk lefelé, és a csúcsból a csúcsba lépünk. A megoldást adó függvény a ciklus befejezése után nem tesz mást, mint a megszámolt téglalapok darab változóban lévő értékét visszaadja. Ehhez az algoritmusban többször is szereplő szamol(darab,bal,fent,jobb,lent) függvényt hívja meg minden kiemelkedő rész elhagyása előtt. A függvényben csak szorzás és 2-vel való osztás szerepel a korábban kiszámított képlet alapján. Csupán arra kell ügyelnünk, hogy a kifejezésben lévő mennyiségek szorzata igen nagy lehet, ezért túlcsordulás történhet. Ennek elkerülésére a mennyiségek 1 000 000 007-tel vett maradékait kell szoroznunk, illetve a szorzat maradékát hozzáadnunk a darab eddigi értékéhez. Ennek a függvénynek a megírását is az olvasóra bízzuk azzal a megjegyzéssel, hogy a -vel való maradékos osztásnál figyeljünk arra, hogy a szorzatok tényezői közül csak a párosakat osszuk. Ezen algoritmus lépésszáma a bemenet elemszámával arányos, tehát a program lineáris futásidejű, így a versenyen is megállta volna a helyét hatékonyság szempontjából is. Schmieder László |