Griddler

Próbáljuk meg megoldani a feladatot csak a következő segédanyagok felhasználásával: Haskell könyvtárainak dokumentációja, Hoogle, a tárgy honlapja és a BE-AD rendszerbe feltöltött beadandók.

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). Beadni viszont a teljes megoldást kell, vagyis az összes függvény definícióját egyszerre!

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!

A feladat összefoglaló leírása

A feladat egy Griddler rejtvény megoldása. A játékot egy négyzetrácson kell játszani, melynek minden sorához és oszlopához tartoznak számjegyek. Ezeket a számokat a továbbiakban clue-nak fogjuk hívni. Az elnevezés onnan adódik, hogy ezen “kulcsok” alapján kell kitölteni a négyzetrács mezőit.

Minden egyes mezőnek két állapota lehet: üres és teli. Tehát vagy beszínezünk egy mezőt, vagy üresen hagyjuk azt. A clue-k azt jelölik, hogy milyen hosszúak az egyes sorokban az összefüggően beszínezett mezők által alkotott blokkok. Ahány clue van, annyi blokk, és balról jobbra, illetve fentről lefelé olvasva a számjegyeket megkapjuk az egyes blokkok hosszát.

Néhány példa a játéktábla szemléltetésére:

Üres tábla:

354375351833373254282102363454563212254268238211262146241

Az előző tábla megoldása:

354375351833373254282102363454563212254268238211262146241

A tizedik sor kiemelve (ezzel szabadon lehet játszani):

Test>
354375351833373254282102363454563212254268238211262146241

Adatreprezentáció

A játéktábla minden egyes mezőjének állapotát egy-egy karakterrel fogjuk reprezentálni. Egy mező állapota lehet: unknown, empty és full, melyeknek csupán a kezdőbetűiket fogjuk megtartani. Magát az n×m-es táblát egy n hosszú listaként fogjuk kezelni, melynek minden eleme egy m hosszú lista. Egy-egy sornak, illetve oszlopnak a clue-jait egy Int listaként fogjuk ábrázolni. Ezeket a listákat további két listába csoportosítjuk majd, annak megfelelően, hogy sorhoz, vagy oszlophoz tartoznak. Végül ezt a két listát egy rendezett párba foglaljuk (a pár első eleme a sorokhoz tartozó clue-kat, második eleme az oszlopokhoz tartozó clue-kat tartalmazza).

type Cell     = Char
type Table    = [[Cell]]
type ClueLine = [Int]
type Clues    = ([ClueLine], [ClueLine])

A kód olvashatósága érdekében bevezetünk három segédfüggvényt:

unknown = 'u'
empty   = 'e'
full    = 'f'

FIGYELEM: Függvényekre nem tudunk mintát illeszteni, ezért a mintaillesztés során kénytelenek vagyunk az 'u' 'e' 'f' karaktereket vizsgálni.

Definiáljuk a következő konstansokat:

duckClues  = (duckRows, duckCols)    :: Clues
poundClues = (poundRows,  poundCols) :: Clues

duckRows = [[3], [5], [4,3], [7], [5], [3], [5], [1,8],
            [3,3,3], [7,3,2], [5,4,2], [8,2], [10], [2,3], [6]]

duckCols = [[3], [4], [5], [4], [5], [6], [3,2,1], [2,2,5],
            [4,2,6], [8,2,3], [8,2,1,1], [2,6,2,1], [4,6], [2,4], [1]]


poundRows = [[4], [2,1], [1,2], [2,2], [2], [8], [2], [8],
             [2], [2], [2,2,2], [6,3], [2,5,3], [2,2,6], [4,4]]

poundCols = [[2],[4],[2,1],[1,1,2,1],[1,1,4],[11],[12],
             [2,1,1,2],[1,1,1,3],[1,1,1,2],[1,1,1,2],[3,3],[2,3],[3],[2]]

yinYangClues :: Clues
yinYangClues = (yinYangRows, yinYangCols)

yinYangRows = [[8],[4,4],[2,6],[1,3,2],[3,3],[8],[6],[2,5],
               [1,2,4],[2,5],[4,5],[8]]
yinYangCols = [[4,4],[3,3],[2,2],[2,2,2],[1,3,2,1],[1,4,2],
               [7,3],[3,7],[2,6],[10],[8],[4]]

flowerClues :: Clues
flowerClues = (flowerRows, flowerCols)

flowerRows = [[2,2],[1,1,1],[1,1,1],[1,5,2],[1,2,2,1],[1,5,1],
              [2,1,3],[2,2],[1],[2,1,2],[3],[1]]
flowerCols = [[2],[1,1],[2,1],[1,3,1,1],[1,3,1,1],[3,2,1],[1,3,5],
              [1,3,1,1],[2,1,1],[1,1,1],[1,1],[2]]

Az algoritmus általános leírása

A megoldó algoritmus iteratív típusú lesz. Ez annyit jelent, hogy egy lépést fogunk újra és újra végrehajtani a játéktáblán, egészen addig, amíg meg nem oldjuk a rejtvényt.

Az algoritmus magja egy sormegoldó függvény lesz, amely több kisebb függvényből fog felépülni. Először meghatározzuk egy sornak az összes lehetséges kitöltését, majd ezt összevetjük a sor jelenlegi állapotával, és kiválasztjuk azokat a kitöltéseket, amelyek illeszkednek a sor aktuális állapotára (a sormegoldó függvényt meghívhatjuk részlegesen kitöltött sorokra is). Ezután vesszük a kiválogatott megoldások metszetét, azaz meghatározzuk azokat a cellákat, amelyeket biztosan ismerünk. Ezek a cellák minden lehetséges megoldásban azonos kitöltöttségűek (tehát vagy mindenhol üresek, vagy mindenhol telik). A többi cella továbbra is ismeretlen marad.

Ha megvan a sormegoldó függvényünk, akkor alkalmazzuk azt minden meglévő sorra, majd transzponáljuk a táblát, alkalmazzuk újra a sorokra (tehát az eredeti tábla oszlopaira), végül transzponáljuk vissza a táblát.

Ez lesz az iteráció egy lépése, ezt kell ismételnünk addig, amíg megoldást nem találunk. Akkor számít egy tábla megoldásnak, ha az iteráció két egymást követő lépésében azonos eredményt kapunk.

Megjegyzés: A megadott algoritmus a nem egyértelmű feladványok esetében csak részmegoldást képes adni. Továbbá, bonyolultabb heurisztikák hiányában, az egyértelműen kitölthetőek között is akad az algoritmus által nem teljesen megoldható.

A tábla kirajzolása

A hibakeresés során hasznos lehet egy olyan függvény, amely kirajzolja a konzolra a tábla jelenlegi állapotát.

Cellák kirajzolás

Írjuk meg azt a függvényt, amely az ismeretlen cellák helyére '?', az üres cellák helyére ' ' és a teli cellák helyére '#' szimbólumot ír!


Test>
'?' :: Char
Test>
' ' :: Char
Test>
'#' :: Char

Egy sor kirajzolása

Adjuk meg azt a függvényt, amely kirajzol egy sort! Egy sort úgy rajzolunk ki, hogy kirajzoljuk az összes cellát, valamint a sor elejére és végére írunk egy '|' karaktert. Végül egy newline-al zárjuk a sort.


Test>
"|? #|\n" :: String

A tábla kirajzolása

Az előző két függvény segítségével definiáljuk azt a függvényt, amely kirajzolja az egész táblát!


Test>
[] :: String
Test>
"|# ? #|\n" :: String
Test>
"|# ? #|\n| ## #|\n" :: String
Test>
"| ### |\n| ##### |\n| #### ###|\n| ####### |\n| ##### |\n| ### |\n| ##### |\n|# ######## |\n|### ### ### |\n|####### ### " ++ […, ……] :: String

Megjegyzés: Ha csupán ezzel a függvénnyel írunk ki valamit a konzolra, akkor azt fogjuk tapasztalni, hogy a GHCi bent hagyja a sorvége karaktereket a sztringben. Ezt úgy tudjuk feloldani, hogy az eredményül kapott sztringre meghívjuk a putStrLn függvényt.

A sormegoldó

Ebben a részfeladatban az a célunk, hogy megadjuk az a függvényt, amely meghatározza egy sornak azokat a celláit, amelyeket biztosan ismerünk. Ehhez több segédfüggvényre is szükségünk lesz.

Segítség: A drawTable nevű függvény segítségével tetszőleges játéktáblát kirajzolhatunk itt az internetes felületen (ez a GHCi-ben sajnos nem működik). Tehát ha az egyik tesztesetnek nem felelne meg a függvényünk, akkor csak írjuk be elé, hogy drawTable $, és megjelenik játéktábla ábrája.

Sorok generálása

A megoldáshoz bevezetünk két segédfüggvényt. Ezek csupán arra szolgálnak, hogy előállítsunk egy-egy n hosszú listát üres, illetve teli mezőkből.


Test>
"eeeee" :: [Cell]

Test>
"fffff" :: [Cell]

Egy blokk elhelyezése

Adjuk meg azt a függvényt, amely egyetlen összefüggő, x hosszúságú blokkot elhelyez egy n hosszúságú sorban az összes lehetséges módon! Próbáljuk meg listakifejezéssel megadni!

Megjegyzés: A függvény a következő (emptyLineOptions) függvényhez kíván segítséget nyújtani.


Test>
["feeee", "efeee", "eefee", "eeefe", "eeeef"] :: [[Cell]]
Test>
["fffeeeeeee", "efffeeeeee", "eefffeeeee", "eeefffeeee", "eeeefffeee", "eeeeefffee", "eeeeeefffe", "eeeeeeefff"] :: [[Cell]]
Test>

Egy sor összes lehetséges megoldása

Definiáljuk azt a függvényt, amely meghatározza egy kitöltetlen sor összes lehetséges megoldását! Első paramétere az adott sorhoz tartozó ClueLine, a második pedig a sorban hátralevő kitöltetlen cellák száma.

A függvénynek összesen négy esetet kell lekezelnie:

  1. Nincs több feldolgozandó clue
  2. Pontosan egy darab clue van
  3. Több clue is van, de nem férnek ki a blokkok (*)
  4. Több clue is van, és van olyan elrendezés, amelyben kiférnek a blokkok

Különösebb fejtörést csupán a második és negyedik esetek jelentenek. Az utóbbit kezeljük a következőképpen:

valahány üres ++ x darab teli ++ egy üres ++ maradék

Ahol a valahány üres annyit jelent, hogy kipróbálunk oda minden lehetséges értéket; az x a jelenlegi clue; egy üres mindenképpen kell két blokk közé; és végül a maradék pedig a hátralévő mezők összes lehetséges kitöltése.

A második esetben pedig alkalmazhatjuk a placeOneBlock függvényt.

(*): Ez alatt azt értjük, hogy a mezők száma kisebb, mint a clue-k összege plusz az általuk meghatározott blokkok közti minimális (1) távolságok összege.


Test>
[] :: [[Cell]]
Test>
[[]] :: [[Cell]]
Test>
["eeeeeeeeee"] :: [[Cell]]
Test>
Test>
Test>
Test>

Cellák illeszkedése

A továbbiakban szükségünk lesz egy függvényre, amely megállapítja, hogy két cella illeszkedik-e egymásra. A függvény hívása során fontos a paraméterek sorrendje. A függvény pontosan akkor adjon vissza hamisat, ha:

Minden más esetben legyen igaz.


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

Egy sor illeszkedő megoldásai

Adjuk meg azt a függvényt, amely meghatározza egy sornak azon megoldásait, melyek illeszkednek a jelenleg ismert mezőkre! Tehát pontosan azok a megoldások szerepeljenek a végeredményben, melyekre a következő két feltétel teljesül:

  1. Minden egyes cellában, ahol a jelenleg ismert sor mezőjében full van, a megoldásban is full van

  2. Minden egyes cellában, ahol a jelenleg ismert sor mezőjében empty van, a megoldásban is empty van

A lineOptions függvény első paramétere a clue-k listája (ugyanaz, mint az emptyLineOptions függvény első paramétere), második paramétere pedig a jelenleg ismert sor. Tipp: használjuk az isMatching és emptyLineOptions függvényeket!


Test>
["eeeeeeeeee"] :: [[Cell]]
Test>
["ffffffffffeeeee", "effffffffffeeee", "eeffffffffffeee", "eeeffffffffffee", "eeeeffffffffffe", "eeeeeffffffffff"] :: [[Cell]]
Test>
["fffeffffeffffee", "fffeffffeeffffe", "fffeffffeeeffff", "fffeeffffeffffe", "fffeeffffeeffff", "fffeeeffffeffff", "efffeffffeffffe", "efffeffffeeffff", "efffeeffffeffff", "eefffeffffeffff"] :: [[Cell]]

Megjegyzés: A következő két példa az előző feladat utolsó két tesztjéhez kapcsolódnak. Az első azokat a megoldásokat választja ki, amelyeknek a harmadik mezője teli, a másik pedig azokat, amelyeknek az első és utolsó mezője teli.

Test>
Test>

Cellák metszete

Adjuk meg azt a függvényt, amely meghatározza két cella “metszetét”! Egy olyan definíciót kell adnunk, amelyben két teli cella metszete mindig teli, két üres cella metszete mindig üres, és minden egyéb esetben a metszet ismeretlen.


Test>
'f' :: Cell
Test>
'e' :: Cell
Test>
'u' :: Cell

Egy sor megoldásainak metszete

Adjuk meg azt a függvényt, amely tetszőleges nemüres megoldáshalmazból meghatározza a biztos mezőket, azaz veszi minden megoldás cellánkénti metszetét!

Megjegyzés: A bemenő paraméter egy megoldáshalmaz, amelyben ismeretlen hosszú megoldások vannak (persze azonos hosszúak).


Test>
"uuffffffffuueeee" :: [Cell]
Test>
"fffeuuffuueffff" :: [Cell]
Test>
Test>

Egy sor megoldása

Az előbbiekben definiált függvények segítségével határozzuk meg azt függvényt, amely megadja egy tetszőleges sorra a hozzá tartozó megoldások metszetét! Ha ez a halmaz üres, akkor adjuk vissza az eredeti sort.

Ez a függvény annyiban különbözik a combineLineOptions függvénytől, hogy itt már egy konkrét sorra vonatkozóan adjuk meg a metszetet.


Test>
"ffffffffeffefff" :: [Cell]
Test>
"effffeffeffffff" :: [Cell]
Test>
"ffffeffeffffffe" :: [Cell]

Megjegyzés: A következő példában látható demonstrate függvény segítségével kirajzolhatjuk a megoldás menetét. Az első sorban csupán a teli mezőket tüntetjük fel. A következő néhány sorban a lehetséges kitöltési módok láthatók. Végül pedig az utolsó sorban találjuk a megoldást.

Test>

Egy lépés az iterációban

Több segédfüggvény használatával fogjuk definiálni az iteráció egy lépését, azaz azt a függvényt, amely minden sorra és oszlopra meghatározza a biztos mezőket. (Biztos mezők alatt az összes lehetséges megoldás metszetét értjük.)

Az összes sor megoldása

Definiáljuk azt a függvényt, amely egy tetszőleges tábla minden sorára kiszámolja a megoldások metszetét! A bemenete egy olyan hosszú ClueLine lista, ahány sor van és az aktuális tábla.


Test>
["uuuuuuuuuuuuuuu", "uuuuuuuuuuuuuuu", "uuuuuuuuuuuuuuu", "uuuuuuuuuuuuuuu", "uuuuuuuuuuuuuu" ++ […, ……], "uuuuuuuuuuuuu" ++ […, ……], "uuuuuuuuuuuu" ++ […, ……], "uuuuuuufffu" ++ […, ……], "uuuuuuuuuu" ++ […, ……], "uffffffuu" ++ […, ……], "uufffuuu" ++ […, ……], "uuuufff" ++ […, ……], "uuuuuf" ++ […, ……], "uuuuu" ++ […, ……], "uuuuu" ++ [……]] :: Table
Test>

Egy tábla megoldása

Adjuk meg azt a függvényt, amely egy tetszőleges tábla minden sorára és oszlopára kiszámolja a megoldások metszetét (segítség az algoritmus általános leírásánál)!


Test>
["ueeeeeeuuffuuuu", "ueeeeeeuffffuuu", "ueeeeeeufffuuuu", "ueeeeeeufffuuuu", "ueeeeeeuufffuu" ++ […, ……], "ueeeeeeuefffu" ++ […, ……], "uuueeeeuffff" ++ […, ……], "uuuuuufffff" ++ […, ……], "uuuuuufuee" ++ […, ……], "uffffffuf" ++ […, ……], "uuffffef" ++ […, ……], "uuuufff" ++ […, ……], "uuuuuf" ++ […, ……], "ueueu" ++ […, ……], "ueee" ++ […, ……]] :: Table
Test>
["uuuuffffeeuu", "uuufeefffuuu", "uuuueuffffuu", "fuuuuufeefuu", "uuuufufeeffu", "uuuuffffuffu", "uuuuuufffffu", "uuuuuueffffu", "fuuufuefffuu", "uuuuueffffuu", "uuffefffffuu", "uuuuffffuuuu"] :: Table

Megjegyzés: A függvény eredményét a drawTable segítségével is ellenőrizhetjük.

Test>
Test>

A rejtvény megoldása

A rejtvényt megoldó függvényhez szükségünk lesz további két segédfüggvényre.

Üres tábla

Definiáljuk azt a függvényt, amely létrehoz egy n×m-es ismeretlen mezőkkel telített táblát!


Test>
["uuuuu", "uuuuu", "uuuuu", "uuuuu", "uuuuu"] :: Table

Az iteráció egy lépésének végrehajtása

Definiáljuk azt a rekurzív függvényt, amely végrehajt egy lépést a táblán, majd attól függően, hogy az új tábla megegyezik-e az előző lépésbelivel vagy megtesz még egy lépést (ha különböznek), vagy pedig visszadja az új táblát (ha megegyeznek)!


Test>
["eeeeeeeeefffeee", "eeeeeeeefffffee", "eeeeeeeffffefff", "eeeeeeefffffffe", "eeeeeeeefffffe" ++ […, ……], "eeeeeeeeefffee" ++ […, ……], "eeeeeeeeffff" ++ […, ……], "feeeeefffff" ++ […, ……], "fffeefffee" ++ […, ……], "fffffffef" ++ […, ……], "efffffef" ++ […, ……], "effffff" ++ […, ……], "eeffff" ++ […, ……], "eeeef" ++ […, ……], "eeee" ++ […, ……]] :: Table
Test>
["eeeffeffeeee", "eefeefeefeee", "eefeefeefeee", "efefffffeffe", "feeffeffeeef", "feefffffeeef", "effeefeefffe", "eeeffeffeeee", "eeeeeefeeeee", "eeeffefeffee", "eeeeefffeeee", "eeeeeefeeeee"] :: Table

Megjegyzés: A függvény által adott eredmények a rendelkezésre álló feladványainkból grafikusan megjelenítve:

Test>
Test>
Test>
Test>

A megoldó függvény

Adjuk meg azt a függvényt, amely adott clue-k alapján megold egy Griddler rejtvényt!


Test>
Test>