Graham’s Scan

Használható segédanyagok: Haskell könyvtárainak dokumentációja, (lokális!) Hoogle, a tárgy honlapja és a BE-AD rendszerbe feltöltött beadandók. Ha bármilyen kérdés, észrevétel felmerül, azt a felügyelőknek kell jelezni, nem a diáktársaknak!

A feladatok egymásra épülnek ezért érdemes ezeket a megadásuk sorrendjében megoldani, de legalább megérteni az aktuális feladatot megelőző feladatokat! A függvények definíciójában lehet, sőt javasolt is alkalmazni a korábban definiált függvényeket (függetlenül attól, hogy sikerült-e azokat megadni).

Tekintve, hogy a tesztesetek, bár odafigyelés mellett íródnak, nem fedik le minden esetben a függvény teljes működését, határozottan javasolt még külön próbálgatni a megoldásokat beadás előtt, vagy megkérdezni a felügyelőket!

Részpontszámokat csak az elégséges szint elérése után lehet kapni!

A feladat összefoglaló leírása

A feladatban pontok konvex burkának meghatározására készült algoritmust valósítjuk meg. A konvex burka egy Q ponthalmaznak az a legkisebb konvex sokszög, melyre minden Q-beli pont vagy a sokszög csúcsa vagy a sokszögben van. Feltesszük, hogy minden pont különbözik, és Q legalább három pontot tartalmaz, melyek nem esnek egy egyenesre.

A feladatban a pontokat egészek párjaként fogjuk ábrázolni:

type Point = (Int, Int)

A type kulcsszó az egészek párjának másik ,,neve’’ lesz. A programban ez semmilyen további megszorítást nem indukál, csupán a beszédesebb függvénytípusok kialakításában segít.

Műveletek helyvektorokkal

Helyvektoroknak nevezzük azokat a vektorokat, melyek kiindulópontja az origó. A helyvektorokat csak végpontjukkal jellemezzük.

Két helyvektor különbsége (1 pont)

Adjuk meg két helyvektor különbségét, mely v1 − v2 esetén a v2 végpontjából v1 végpontjába mutató vektor eltolása az origóba!


Test>
(1, 1) :: Point
Test>
(4, 2) :: Point
Test>
(3, -1) :: Point

Hossz négyzete (1 pont)

Adjuk meg egy helyvektor hosszának a négyzetét! Egyszerűség kedvéért a függvény a norm nevet viseli.


Test>
2 :: Int
Test>
8 :: Int
Test>
25 :: Int
Test>
25 :: Int
Test>
25 :: Int
Test>
100 :: Int

Vektoriális szorzat hossza (1 pont)

Számítsuk ki két helyvektor vektoriális szorzatának hosszát! Használjuk a következő képletet: p1 ⋅ p2∥ = x1y2 − x2y1, ahol p a p(x, y) helyvektor hossza.


Test>
0 :: Int
Test>
16 :: Int
Test>
-30 :: Int

Továbbhaladás iránya (2 pont)

Az algoritmusban fontos tudnunk, hogy ha egy p0 pontból p1 pontba eljutottunk, és továbbmennénk p2-be, akkor p1-ből milyen irányba kell elindulnunk a p0 és p1 egyeneséhez képest. Három irány elképzelhető: balra, egyenesen, jobbra, melyeket most rendre  − 1, 0, 1 számokkal jellemzünk.

Adjuk meg a függvényt, mely megadja a továbbhaladás irányát! Fontos a paraméterek sorrendje, turn p0 p1 p2 esetén az eredmény p2 iránya, miután eljutottunk p1-be p0-ból.

Az eredmény p2 − p0 és p1 − p0 vektorok vektoriális szorzatának előjele.

Segítség: használjuk a signum függvényt.

turn :: Point -> Point -> Point -> Int

Test>
-1 :: Int
Test>
0 :: Int
Test>
1 :: Int
Test>
-1 :: Int
Test>
1 :: Int

Vektorok közötti kisebb-nagyobb reláció (3 pont)

Egy p0p1 vektort tekintsünk kisebbnek egy p0p2 vektornál, ha, azonos p0 pontból indulva, p0p1 az óramutató járásával egyező irányba helyezkedik el p0p2-höz képest.

Ahhoz, hogy ehhez ne kelljen közbezárt szöget számolni, egy trükkhöz folyamodunk. Onnan tudjuk, hogy p0p1 az óramutató járásával egyező irányban helyezkedik el p0p2-höz képest (tehát p0p1 kisebb, mint p0p2), ha a ∥(p2 − p0) ⋅ (p1 − p0)∥ (vektoriális szorzat nagysága) negatív szám.

Írjunk függvényt, mely p0, p2 és p1 pontok alapján megadja, hogyan viszonyul a p0p2 vektor a p0p1 vektorhoz!

Segítség: vizsgáljuk meg, hogy a vektoriális szorzat nagysága (cross) hogyan viszonyul a nullához a compare segítségével.

Megjegyzés: a jól definiáltság érdekében csak olyan pontokkal dolgozunk, melyek a kitüntetett ponton áthaladó, x tengellyel párhuzamos, az origót nem tartalmazó félsíkban vannak.

compareClockWise :: Point -> Point -> Point -> Ordering

Test>
GT :: Ordering
Test>
LT :: Ordering
Test>
GT :: Ordering
Test>
LT :: Ordering

Legalul lévő pontok (2 pont)

Gyűjtsük ki pontok listájából a koordináta-rendszerben legalul lévő pontokat, azaz azokat, amelyeknek legkisebb a második koordinátája. Több ilyen pont is létezhet, adjuk vissza mindegyiket. A sorrend tetszőleges.

Tipp: rendezzük második koordináta szerint növekvő sorrendbe a pontokat, majd ezek elejéről vegyük azokat a pontokat, melyeknek y koordinátája megegyezik a rendezett lista legelső pontjának y koordinátájával.

bottom :: [Point] -> [Point]

Test>
[(-1, 0), (2, 0)] :: [Point]
Test>
[(10, -3), (15, -3)] :: [Point]
Test>
[(2, 3), (4, 3)] :: [Point]
Test>
[] :: [Point]

Bal alsó sarok (2 pont)

Keressük meg azt a pontot, amelyik a koordináta rendszerben legalul, legbalra található, azaz, mindkét koordinátája rendre a legkisebb. Feltesszük, hogy csak egy ilyen pont létezik, és minden pont különböző.

bottomLeft :: [Point] {- nem üres -} -> Point

Test>
(-1, 0) :: Point
Test>
(10, -3) :: Point
Test>
(2, 3) :: Point

Rendezés elfordulás mértéke szerint (3 pont)

Rendezzük pontok listáját aszerint, hogy egy kitüntetett pontból melyik milyen irányban helyezkedik el. Más szóval az x tengelyhez képesti irány szerint rendezünk. Az első elemhez kell a legkevesebbet elfordulni az x tengelyhez képest az óramutató járásával ellentétesen a kitüntetett pontban, az utolsóhoz kell a legnagyobbat.

Feltesszük, hogy minden pont a kitüntetett ponton áthaladó, x tengellyel párhuzamos, az origót nem tartalmazó félsíkban van.

Segítség: használjuk a vektorok közötti compareClockWise relációt. Az ugyanis pont akkor tekint kisebbnek egy vektort, ha ahhoz kevesebbet kell elfordulni az x tengelyhez képest.

sortByPolarAngle :: Point -> [Point] -> [Point]

Test>
[(1, 1), (0, 1), (-2, 4), (-2, 2)] :: [Point]
Test>
[(2, 2), (1, 4), (0, 3)] :: [Point]

Ponttól legtávolabb eső pont (2 pont)

Keressük meg azt a pontot, mely egy kitüntetett ponttól legtávolabbra esik!

Feltesszük, hogy csak egy ilyen pont létezik, minden pont különböző távolságra van.

farthest :: Point -> [Point] {- nem üres -} -> Point

Test>
(4, 4) :: Point
Test>
(-2, 4) :: Point
Test>
(1, 0) :: Point

Azonos irányban lévő pontok (4 pont)

Csoportosítsuk a pontokat elfordulás iránya szerint, azaz egy csoportba kerüljenek azok a pontok, melyek mindegyike egy kitüntetett ponttal azonos szöget bezárnak be. A csoportokon belüli sorrend tetszőleges.

Feltesszük, hogy minden pont a kitüntetett ponton áthaladó, x tengellyel párhuzamos, az origót nem tartalmazó félsíkban van.

Segítség: használjuk a turn függvényt, melynek eredménye 0, ha a pontok egy egyenesre esnek.

sameAngles :: Point -> [Point] -> [[Point]]

Test>
[[(-1, 1), (-2, 2)], [(1, 1), (2, 2)]] :: [[Point]]
Test>
[[(-1, 4)], [(0, 5), (0, 4)], [(1, 4)]] :: [[Point]]
Test>
[[(-1, 4)], [(0, 4)], [(3, 3), (4, 4)]] :: [[Point]]

Különböző irányban lévő pontok (3 pont)

Szűrjük le pontok listáját úgy, hogy azonos irányban található pontok közül csak a legtávolabbit tartjuk meg. Más szóval szűrjük a pontok listáját úgy, hogy csak a különböző irányú pontokat tartjuk meg, mindegyik irányból a legmesszebbi pontot választva.

Tipp: csoportosítsuk a pontokat bezárt szög szerint, majd minden egyes csoportot rendezzünk távolság alapján. Végül minden csoportból vegyük a legutolsó (legtávolabbi) elemet.

farthestPoints :: Point -> [Point] -> [Point]

Test>
[(2, 2)] :: [Point]
Test>
[(2, 2), (-2, 4)] :: [Point]
Test>
[(0, 1), (-1, -1), (2, 1)] :: [Point]
Test>
[(0, 1), (-1, -1), (2, 1), (2, 2)] :: [Point]
Test>
[(4, 0), (4, 4), (1, 2), (0, 4)] :: [Point]

Az algoritmus állapota

Az algoritmus során egy veremben tároljuk az eddig meglátogatott pontok félkész konvex sokszögét. Az ötlet, hogy mire meglátogatjuk az összes pontot, addigra a verem az összes pontot körülhatároló konvex burkot fogja tartalmazni.

A vermet pontok listájaként fogjuk ábrázolni. A lista első eleme a legújabban bekerült elem (a verem teteje), a lista utolsó eleme a legrégebben bekerült elem (a verem alja).

type Stack = [Point]

Az algoritmus egy lépése (2 pont)

Az algoritmus fontos eleme, hogy a konvex sokszög bejárása során mindig csak balra forduljunk. Így, amikor új pontot szúrnánk be a verembe, ki kell venni a verem tetejéről azokat a pontokat, melyekből az új ponthoz nem balra forduló után jutottunk el (ez egyenest vagy jobbra fordulást jelent).

Vegyük ki a verem tetejéről azokat a pontokat, melyekből nem balra fordulással jutnánk el az új pontba! Tehát, ha a verem tetején p1 és p2 van és a beszúrandó pont p, akkor azt kell vizsgálni, hogy p2p1 megtétele után p-hez merre kell fordulni. Ehhez a verem legfölső két elemét (a legújabb és ahonnan oda jutottunk) és a beszúrandó elemet kell megvizsgálni a turn segítségével, és eldobni a legfölsőt (a második marad), ha nem balra kell onnan fordulni (turn értéke nem  − 1). Amennyiben eldobtuk ezeket a pontokat, szúrjuk be a maradék verem tetejére az új pontot.

Megjegyzés: a veremben egészen biztosan lesz mindig legalább két elem.

step :: Stack -> Point -> Stack

Test>
[(1, 2), (1, 1), (0, 0)] :: Stack
Test>
[(1, 3), (1, 1), (0, 0)] :: Stack
Test>
[(0, 4), (1, 2), (1, 1), (0, 0)] :: Stack
Test>
[(0, 5), (1, 2), (1, 1), (0, 0)] :: Stack
Test>
[(-2, 5), (0, 4), (1, 2), (1, 1), (0, 0)] :: Stack
Test>
[(3, 4), (1, 1), (0, 0)] :: Stack

Konvex burok (3 pont)

Az algoritmus csak akkor működik, ha a bemenete legalább három pontból áll.

Az algoritmus menete a következő:

  1. A konvex burok előállításához szükség van a bal alsó pont meghatározására (bottomLeft).

  2. A maradék pontokból szűrjük le azokat, melyek egy irányba esnek és van náluk messzebbre eső is a bal alsó sarokhoz képest (farthestPoints). Rendezzük is a szűrt pontokat a bal alsó sarokhoz képest az elfordulás mértéke szerint (sortByPolarAngle).

  3. A kezdeti verem alján a bal alsó sarok van, felette az óramutató járásával ellentétesen a legkisebb elfordulást igénylő pont, a felett a második legkisebb elfordulást igénylő pont van.

  4. A maradék n − 3 rendezett pontot szúrjuk be verembe a step segítségével.

  5. Amikor elfogytak a pontok, a verem tartalmazza a konvex burok pontjait, fordított sorrendben. Fordítsuk meg a sorrendet, hogy első pont legyen a bal alsó sarok, és óramutató járásával ellentétes irányba kelljen bejárni a burkot.

Tipp: használjuk ki, hogy egy verem valójában pontok listája.

hull :: [Point] -> [Point]

Test>
[(0, 0), (4, 0), (4, 4), (0, 4)] :: [Point]
Test>
[(0, 0), (4, 0), (4, 4), (0, 4)] :: [Point]
Test>
[(-2, 1), (1, 1), (3, 2), (-2, 3)] :: [Point]
Test>
[(-3, 0), (3, 0), (0, 4)] :: [Point]
Test>
[] :: [Point]
Test>
[] :: [Point]
Test>
[] :: [Point]

Pontozás