Pénzérme forgató játék

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 egy adott játékhoz kapcsolódó függvényeket, és végül egy megoldáskereső algoritmust fogunk megvalósítani.

Egy játék során van c darab pénzérménk, kezdetben minden érmének az írás oldala látszik. A játék lényege, hogy minden körben pontosan f darab érmét megfordítva egy olyan érmesorozatot kapjunk, hogy mindegyiknek a fej oldala legyen felül. A játékról tudjuk, hogy biztosan megoldható, ha c és f relatív prímek és f páratlan.

Az érmék írás oldalát a False logikai érték, a fejet pedig a True reprezentálja. Egy érmesorozat (Coins) valójában logikai értékek sorozata. A State típus a játékmenet egy pillanatnyi állapotát reprezentálja egy rendezett párral, ahol az első elem az érmesorozat, a második elem pedig az egy lépésben átfordítható érmék száma (korábban f).

A megoldáshoz egy kereső algoritmust fogunk implementálni, amelyben a megoldást a játék állapotainak sorozatából (History) rekonstruálhatjuk.

type Coins = [Bool]
type State = (Coins, Int)
type History = [State]

Legnagyobb közös osztó (2 pont)

A megoldhatóság eldöntéséhez szükségünk van annak az ellenőrzésére, hogy két szám relatív prím-e, azaz a legnagyobb közös osztójuk 1.

Az algoritmus hasonló lesz az Euklideszi algoritmushoz. Vegyük az a és b paraméterek abszolútértékét (abs függvény), legyenek ezek a' és b'! A lépések a következők:

  1. Ha a' és b' értéke megegyezik, akkor adjuk vissza a'-t!

  2. Ha a' vagy b' értéke nulla, akkor adjuk vissza kettő közül a nagyobbikat!

  3. Különben vonjuk ki a nagyobb értékből a kisebbet (legyen ez c) és határozzuk meg a c és a kisebbik érték legnagyobb közös osztóját!

Ebben a feladatban csak a fenti leírásnak megfelelő definíciót fogadjuk el!

gcd :: Int -> Int -> Int

Test>
3 :: Int
Test>
1 :: Int
Test>
0 :: Int
Test>
3 :: Int

Megoldhatóság (1 pont)

Az érmés feladvány biztosan megoldható, ha az érmesorozat hossza (c) és az egy körben megfordítandó érmék száma (f) egymással relatív prímek, és az f páratlan. Ezen felül kikötés még, hogy f nem lehet nagyobb c-nél, és mindkét szám pozitív.

A hasSolution függvény első paramétere a c érték, második az f, visszatérési értéke pedig igaz, ha a megadott feltételek teljesülnek, tehát biztosan lesz megoldása a játéknak.

hasSolution :: Int -> Int -> Bool

Test>
True :: Bool
Test>
True :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool

Kezdőállapot (1 pont)

Megadott c és f értékekhez állítsd elő a játék kezdőállapotát, azaz azt a rendezett párt, amely első eleme egy c darab False értéket tartalmazó lista, második eleme pedig az f.

initialState :: Int -> Int -> State

Test>
([False, False, False, False, False], 3) :: State
Test>
([False, False, False, False, False, False, False, False, False, False, False], 5) :: State

Célállapot (1 pont)

Egy megadott állapotról ellenőrizd le, hogy célállapot-e, azaz az állapotban lévő érmesorozat csak True értékből áll.

goalState :: State -> Bool

Test>
True :: Bool
Test>
True :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool

Megengedhető forgatás (2 pont)

Az érmék egy körben való forgatását egy Bool lista, azaz egy Flips típusú érték írja le.

type Flips = [Bool]

Adott érmesorozat mellett a forgatások listájának i. indexű eleme azt jelöli, hogy az érmesorozat i. elemét megfordítjuk-e vagy sem. Például [True, False, True] azokat forgatásokat jelöli, ahol a nullás és a kettes indexű érmét megfordítjuk, de az egyes indexű érmét nem.

Adott s állapot mellett egy forgatásokat leíró flips listának akkor van értelme, ha az s állapotban lévő érmék száma megegyezik a flips hosszával, valamint a flipsben pontosan annyi True található, amennyi az s állapotban lévő f (valóban csak annyi érmét forgatunk, amennyit szabad).

Definiáld a függvényt, mely leellenőrzi a fenti két feltételt!

isLegalFlip :: State -> Flips -> Bool 

Test>
True :: Bool
Test>
True :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool
Test>
False :: Bool

Érmék forgatása (2 pont)

Alkalmazd az állapotra az érmeforgatásokat! Amennyiben az érmefogatás nem megengedhető (isLegalFlip hamisat ad vissza), akkor adj a tesztekben megjelenő szövegű hibaüzenetet az error függvénnyel! Az állapotban lévő f érték a forgatás hatására nem változik.

flipCoins :: State -> Flips -> State

Test>
([True, False, False], 1) :: State
Test>
([False, True, False], 1) :: State
Test>
([False, True, True], 1) :: State
Test>
([False, True, False, True, True], 3) :: State
Test>
([True, True, True, True, True], 3) :: State
Test>
⊥₁ :: State
⊥₁: flipCoins: illegal flip
CallStack (from HasCallStack):
  error, called at ./Flipping.lhs:199:21 in main:Flipping
Test>
⊥₁ :: State
⊥₁: flipCoins: illegal flip
CallStack (from HasCallStack):
  error, called at ./Flipping.lhs:199:21 in main:Flipping

Összes lehetséges forgatás (3 pont)

Generáljuk le adott (pozitív egész) c és f esetén az összes lehetséges forgatást, azaz az olyan c hosszúságú logikai értékeket tartalmazó listákat, melyek mindegyike pontosan f darab True értéket tartalmaznak!

Segítség a rekurzív megoldáshoz (nem kötelező ez alapján definiálni):

generateFlips :: Int -> Int -> [Flips]

Test>
[[True, False, False], [False, True, False], [False, False, True]] :: [Flips]
Test>
[[False, False, False, False]] :: [Flips]
Test>
[[True, True, True, False, False], [True, True, False, True, False], [True, True, False, False, True], [True, False, True, True, False], [True, False, True, False, True], [True, False, False, True, True], [False, True, True, True, False], [False, True, True, False, True], [False, True, False, True, True], [False, False, True, True, True]] :: [Flips]

Az összes következő állapot (2 pont)

Adott állapot esetén adjuk vissza egy listában az összes lehetséges következő állapotot! Az előzőleg megírt függvények segítségével le tudjuk generálni az összes lehetséges forgatást, és az összes forgatást alkalmazzuk az adott állapotra.

nextStates :: State -> [State]

Test>
[([True, False, False], 1), ([False, True, False], 1), ([False, False, True], 1)] :: [State]
Test>
[([True, True, False], 1), ([False, False, False], 1), ([False, True, True], 1)] :: [State]
Test>
[([True, True, True, False, False], 3), ([True, True, False, True, False], 3), ([True, True, False, False, True], 3), ([True, False, True, True, False], 3), ([True, False, True, False, True], 3), ([True, False, False, True, True], 3), ([False, True, True, True, False], 3), ([False, True, True, False, True], 3), ([False, True, False, True, True], 3), ([False, False, True, True, True], 3)] :: [State]

Megoldás keresése (3 pont)

A megoldást a feladvány állapotainak sorozatával (előzmény, History típus) ábrázoljuk. A megoldás keresésekor egyszerre több úton is elindulunk, és addig forgatunk, amíg el nem jutunk a célba (minden érmének a fej oldala van felül). A megoldhatóságot itt nem kell ellenőrizni.

A megoldások keresése a következő:

  1. Ha az előzmények listája üres, nincs megoldás, adjunk vissza egy üres listát.

  2. Ha van legalább egy elem az előzmények listájában, akkor két eset lehetséges:

1. Ha az első előzmény első állapota célállapot (`goalState`),
   álljunk meg a kereséssel, és adjuk vissza ezt az első útvonalat
   (ez lesz a legkevesebb lépésből álló állapotsorozat a megoldáshoz).

2. Vegyük a lista elejéről a legelső útvonalat, és
   a többi útvonal mögé fűzzük a legelső útvonal összes
   rákövetkezőjét. Folytassuk a keresést az így kapott új
   útvonal-listával (`solution`).

    A rákövetkezőket úgy kapjuk meg, hogy a legelső útvonal
    fejelemére alkalmazzuk az összes lehetséges forgatást (`nextStates`)
    és az így kapott állapotokat egyenként fejelemként hozzáfűzzük az
    eredeti útvonalhoz.
solution :: [History] -> History

Test>
[([True, True, True], 1), ([True, True, False], 1), ([True, False, False], 1), ([False, False, False], 1)] :: History
Test>
[([True, True, True, True, True], 3), ([False, False, True, True, False], 3), ([True, True, True, False, False], 3), ([False, False, False, False, False], 3)] :: History
Test>
[([True, True, True, True, True, True, True], 3), ([False, True, True, True, True, False, False], 3), ([True, True, True, False, False, False, False], 3), ([False, False, False, False, False, False, False], 3)] :: History

Legrövidebb megoldás (2 pont)

A megoldás kereséséhez először ellenőrizzük, hogy a rendre megadott c és f esetén biztosan megoldható-e a feladvány (hasSolution)! Ha nem, adjuk vissza a tesztben is megjelenő hibaüzenetet az error függvénnyel!

Amennyiben megoldható, paraméterezzük fel a solution függvényt! Állapítsuk meg a kezdeti állapotot a paraméterül kapott c és f értékekből (initialState). Ezután készítsünk belőle egy előzményt, mely csupán ezt az egy állapotot tartalmazza! A solutionnek ebből az egy előzményből álló sorozatot adjuk át paraméterként.

Az eredmény az állapotokban adott érmesorozatok lesznek.

solve :: Int -> Int -> [Coins]

Test>
[[True, True, True], [True, True, False], [True, False, False], [False, False, False]] :: [Coins]
Test>
[[True, True, True, True, True], [False, False, True, True, False], [True, True, True, False, False], [False, False, False, False, False]] :: [Coins]
Test>
[[True, True, True, True, True, True, True], [False, True, True, True, True, False, False], [True, True, True, False, False, False, False], [False, False, False, False, False, False, False]] :: [Coins]
Test>
⊥₁ :: [Coins]
⊥₁: solve: no solution
CallStack (from HasCallStack):
  error, called at ./Flipping.lhs:329:33 in main:Flipping
Test>
⊥₁ :: [Coins]
⊥₁: solve: no solution
CallStack (from HasCallStack):
  error, called at ./Flipping.lhs:329:33 in main:Flipping

Pontozás