N-gram kódolás

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

Ebben a feladatban egy veszteségmentes tömörítési algoritmus részleteit kell megvalósítani. Ahogy majd a folytatásban látni fogjuk, a lényege, hogy megkeressük a leggyakoribb egymás után következő elemeket és kicseréljük ezeket a sorozatokat egy, a bemenetben nem szereplő értékre, amelyet egy előre megadható készletből választunk ki. Mindezt addig alkalmazzuk, amíg találunk többször előforduló, adott hosszúságú részsorozatokat. Az így kapott helyettesítéseket végül egy táblázatba rendezzük és hozzáírjuk a sorozat kódolt változatához. Ezzel, ideális esetben, az eredeti sorozat egy rövidebb változatát kapjuk, amelyet egy egyszerű dekódolóval vissza is tudunk fejteni.

Egy rövid példa az algoritmus szemléletésére, kételemű részsorozatokkal. Tegyük fel, hogy kódolni akarjuk a következő sorozatot:

aaabdaaabac

Ebben a "aa" fordul elő a leggyakrabban, így ehhez választunk valamilyen szimbólumot, pl. a 'Z' értéket. Ekkor a következő helyettesítési táblazatunk jön létre:

Z -> aa

és a részsorozatok megfelelő helyettesítése után ezt kapjuk az eredeti sorozatunkból:

ZabdZabac

Ezután megint ismételhetjük az előbb lépést. Ekkor a táblázat:

Z -> aa
Y -> ab

és a sorozat:

ZYdZYac

Tovább lehet folytatni az egészet a menet közben behelyettesített szimbólumok helyettesítésével is. Ekkor a táblázat:

Z -> aa
Y -> ab
X -> ZY

és a sorozat:

XdXac

Ezzel be is fejeződött a kódolás. A kitömörítést pedig ennek fordítottjával lehet megvalósítani.

Leggyakoribb egymás után következő elemek (3 pont)

Adjunk meg egy függvényt, amely tetszőleges (rendezhető) értékek sorozatához meghatározza a benne előforduló, adott hosszúságú, leggyakoribb részsorozatot! A részsorozattal együtt adja vissza annak tényleges gyakoriságát is. Azonos gyakoriságok esetén a később előforduló részsorozatot adja vissza!

mostCommonConsecutive :: Ord a => Int -> [a] -> ([a],Int)

Test>
([], 0) :: ([()], Int)
Test>
("hi", 1) :: ([Char], Int)
Test>
("ff", 1) :: ([Char], Int)
Test>
("bc", 3) :: ([Char], Int)
Test>
([0, 0], 9) :: ([Integer], Int)
Test>
("abc", 3) :: ([Char], Int)
Test>
("abra", 2) :: ([Char], Int)

Részsorozat összes előfordulásának cseréje (2 pont)

Definiáljunk egy függvényt egy részsorozatot összes előfordulásának cseréjére! A csere nem rekurzív, vagyis a régi részlet helyett az új beillesztendő részsorozatban már nem kell a cserét elvégezni. Amennyiben az eredeti sorozat nem tartalmazza a lecserélendő részsorozatot, úgy változatlanul adjuk vissza.

replace :: Eq a => [a] -> [a] -> [a] -> [a]

Test>
"feebar" :: [Char]
Test>
"foobaz" :: [Char]
Test>
"foobarbaz" :: [Char]
Test>
"foobazbar" :: [Char]
Test>
"fbar" :: [Char]
Test>
"fLOLLOLbar" :: [Char]
Test>
"foobar" :: [Char]

Listák különbsége (1 pont)

Készítsünk egy függvényt, amely két lista kivonását valósítja meg! Ez egy bináris művelet, ahol mindig az elsőként megadott listából vonjuk ki a másodikként megadottat. Az eredménylistában egy elem előfordulásainak száma mindig a kisebbítendő listában levő előfordulásainak és a kivonandó listában levő előfordulásainak különbsége, természetesen negatív érték esetén nulla.

minus :: Eq a => [a] -> [a] -> [a]

Test>
"test list" :: [Char]
Test>
[1, 2, 3, 7, 8, 9, 10] :: [Integer]
Test>
[] :: [Integer]
Test>
[0, 0, 0, 1] :: [Integer]
Test>
[0, 0, 0, 0] :: [Integer]
Test>
[] :: [Integer]
Test>
[] :: [Integer]

A kódolás egy lépése (1 pont)

A tömörítést végző kódolást lépésekben fogjuk végrehajtani, amelyhez ezért definiálunk számítási állapotokat és ezekre egy átmenetfüggvényt. Az állapotokat a State típussal jelöljük:

type State = ([Int],Dictionary,[Int],Int)

amely egy rendezett négyes:

A szótárnak is egy külön típust adunk meg, amely a következő:

type Dictionary = [(Int,[Int])]

Itt tehát rendezett párok listájáról van szó, ahol az első tag megadja, hogy minek, a második pedig, hogy mit feleltetünk meg.

A step átmenetfüggvény a State által leírt állapot mellett kap még első paraméterként egy egész számot, amely megadja, hogy mekkora hosszúságú részsorozatokkal akarunk kódolni.

A működése a következő:

step :: Int -> State -> State

Test>
([1, 2, 3, 4, 5], [(0, [97, 97])], [0, 97, 98, 100, 0, 97, 98, 97, 99], 4) :: State
Test>
([2, 3, 4, 5], [(1, [97, 98]), (0, [97, 97])], [0, 1, 100, 0, 1, 97, 99], 2) :: State
Test>
([3, 4, 5], [(2, [0, 1]), (1, [97, 98]), (0, [97, 97])], [2, 100, 2, 97, 99], 2) :: State
Test>
([4, 5], [(3, [100, 2]), (2, [0, 1]), (1, [97, 98]), (0, [97, 97])], [2, 3, 97, 99], 1) :: State
Test>
([5], [(4, [97, 99]), (3, [100, 2]), (2, [0, 1]), (1, [97, 98]), (0, [97, 97])], [2, 3, 4], 1) :: State
Test>
([1, 2, 3, 4, 5], [(0, [97, 97])], [0, 97, 98, 100, 0, 97, 98, 97, 99], 4) :: State
Test>
([1, 2, 3, 4, 5], [(0, [97, 97, 98])], [97, 0, 100, 97, 0, 97, 99], 2) :: State
Test>
([1, 2, 3, 4, 5], [(0, [97, 97, 97, 98])], [0, 100, 0, 97, 99], 2) :: State
Test>
([1, 2, 3, 4, 5], [(0, [100, 97, 97, 97, 98])], [97, 97, 97, 98, 0, 97, 99], 1) :: State

Feltételes ismétlés (1 pont)

Készítsünk egy függvényt, amely egy adott f függvényt addig ismétel egy adott x kezdőértékre, amíg a megadott p feltételt igaz! Az eredmény a x így folyamatosan változtatott értéke lesz. Ha eleve nem teljesül rá a feltétel, akkor változatlanul adjuk vissza.

while :: (a -> Bool) -> (a -> a) -> a -> a

Test>
999 :: Integer
Test>
0 :: Integer
Test>
[1, 1, 1, 1, 1, 1, 1, 1, 1] :: [Integer]

Sorozat kódolása (2 pont)

Definiáljuk egy függvényt, amely megadott hossz alapján egy értékhalmaz felhasználásával előállítja annak kódolt alakját! A függvény eredménye egy kódolási összerendeléseket tartalmazó szótár lesz, valamint a kódolt sorozat.

A megadáshoz használjuk a while és step függvényeket. Emlékeztetőül az állapot, amivel a step függvények majd tudnia kell dolgoznia:

Ügyeljünk arra, hogy a kódoláshoz használt értékhalmaz ne tartalmazzon olyan elemet, amely a kódolandó sorozatban is megtalálható (minus)!

A függvény csak kettőnél nagyobb részsorozatokra legyen hajlandó működni, ettől eltérő esetben adjon hibát az error függvény segítségével!

fold :: Int -> [Int] -> [Int] -> (Dictionary, [Int])

Test>
⊥₁ :: (Dictionary, [Int])
⊥₁: fold: n must be greater than 2
CallStack (from HasCallStack):
  error, called at ./NGram.lhs:279:31 in main:NGram
Test>
([(6, [0, 1]), (1, [5, 4]), (0, [5, 5])], [6, 3, 6, 5, 2]) :: (Dictionary, [Int])
Test>
([(0, [5, 5, 5])], [0, 4, 3, 0, 4, 5, 2]) :: (Dictionary, [Int])
Test>
([(0, [5, 5, 5, 4])], [0, 3, 0, 5, 2]) :: (Dictionary, [Int])
Test>
([(67, [66, 4]), (66, [65, 5]), (65, [5, 5])], [67, 3, 67, 5, 2]) :: (Dictionary, [Int])
Test>
([], [42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52]) :: (Dictionary, [Int])

Szerializáció (1 pont)

Írjunk egy függvényt, amely a paraméterül (a fold függvénytől) kapott szótárat és kódot szerializálja! Az eredménylista elemeit a következő sorrendben hozzuk létre:

  1. A leggyakoribb részsorozatok mérete (ezt a szótárból meg lehet mondani).
  2. A szótárban lévő bejegyzések száma.
  3. A szótár kilapított változata, ahol először a benne szereplő pároknál egyenként a pár első, majd második tagja. Ezeket összefűzzük egy listába.
  4. A kódolt törzs.

Ha üres a szótár, akkor a szerializáció eredménye is üres (lista) lesz. (Ld. az utolsó teszteset.)

serialize :: (Dictionary, [Int]) -> [Int]

Test>
[2, 3, 88, 90, 89, 89, 97, 98, 90, 97, 97, 88, 100, 88, 97, 99] :: [Int]
Test>
[3, 1, 90, 97, 97, 98, 97, 90, 100, 97, 90, 97, 99] :: [Int]
Test>
[4, 1, 90, 97, 97, 97, 98, 90, 100, 90, 97, 99] :: [Int]
Test>
[] :: [Int]

Kódolt sorozat kibontása (1 pont)

Készítsünk egy függvényt, amely adott szótár és kódolt sorozat birtokában képes a kódokat kibontani! A kibontás lényegében annyit jelent, hogy vesszük sorra a szótár bejegyzéseit, és a kódolt sorozatban egyesével haladva kicserélgetjük (replace) a pár első elemét a pár második elemére, vagyis a neki megfelelő részsorozatra.

unfold :: Dictionary -> [Int] -> [Int]

Test>
[102, 111, 111, 98, 97, 114] :: [Int]
Test>
[] :: [Int]
Test>
[97, 97, 97, 98, 100, 97, 97, 97, 98, 97, 99] :: [Int]
Test>
[97, 97, 97, 98, 100, 97, 97, 97, 98, 97, 99] :: [Int]

Tömörítés a paraméterek automatikus megválasztásával (3 pont)

Definiáljunk egy olyan függvényt, amely a fold paramétereinek próbálgatásával megkeresi azt, amelyet szerializálva (serialize) a legrövidebb eredményt kapjuk, majd ezt visszaadja!

Emlékeztetőül, a fold függvénynek lényegében az n paraméterét kell szabályozni, amely a leggyakoribb részsorozatok hossza. Ez mehessen 2-től egészen 255-ig. Az értékkészlet legyen mindig a [0..255] lista.

Vigyázzunk, ha üres lista keletkezik valamelyik tömörítési próba esetén, akkor azokat hagyjuk ki, mert különben üres lista lesz a compress végeredménye!

compress :: [Int] -> [Int]

Test>
[] :: [Int]
Test>
[4, 1, 0, 97, 98, 114, 97, 0, 107, 97, 100, 0] :: [Int]
Test>
[4, 1, 0, 97, 97, 97, 98, 0, 100, 0, 97, 99] :: [Int]
Test>
[5, 1, 0, 116, 111, 32, 98, 101, 0, 32, 111, 114, 32, 110, 111, 116, 32, 0] :: [Int]
Test>
[5, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 2, 2, 2] :: [Int]
Test>
[5, 4, 5, 2, 2, 2, 2, 2, 4, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 4, 4, 5, 5] :: [Int]
Test>
[5, 4, 7, 0, 0, 0, 0, 0, 6, 1, 1, 1, 1, 1, 5, 2, 2, 2, 2, 2, 4, 3, 3, 3, 3, 3, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4] :: [Int]

Deszerializáció (2 pont)

Készítsünk egy függvényt, amellyel képesek vagyunk egész számok sorozatát a serialize formátumnának megfelelően értelmezni és előállítani belőle a szótárt és a kódot! Amennyiben nem tudja kiolvasni, adjon hibát az error függvénnyel!

Emlékeztetőül, a formátum:

  1. A leggyakoribb részsorozatok hossza.
  2. A szótár elemeinek száma.
  3. A szótár kilapított (listába rendezett) változata.
  4. A kódolt sorozat.
deserialize :: [Int] -> (Dictionary, [Int])

Test>
⊥₁ :: (Dictionary, [Int])
⊥₁: deserialize: Invalid format
CallStack (from HasCallStack):
  error, called at ./NGram.lhs:401:19 in main:NGram
Test>
⊥₁ :: (Dictionary, [Int])
⊥₁: deserialize: Invalid format
CallStack (from HasCallStack):
  error, called at ./NGram.lhs:401:19 in main:NGram
Test>
([(3, [4]), (5, [6])], [7, 8, 9, 10]) :: (Dictionary, [Int])
Test>
([(0, [97, 98, 114, 97])], [0, 107, 97, 100, 0]) :: (Dictionary, [Int])
Test>
([(0, [97, 97, 97, 98])], [0, 100, 0, 97, 99]) :: (Dictionary, [Int])
Test>
([(0, [116, 111, 32, 98, 101])], [0, 32, 111, 114, 32, 110, 111, 116, 32, 0]) :: (Dictionary, [Int])
Test>
([(2, [1, 1, 1, 1, 1]), (1, [0, 0, 0, 0, 0])], [2, 2, 2, 2]) :: (Dictionary, [Int])

Kitömörítés (1 pont)

Adjunk meg egy függvényt, amely feldolgozza a bemenetként kapott egész számok sorozatát (deserialize) és kibontja (unfold) belőle az általuk kódolt eredeti sorozatot!

decompress :: [Int] -> [Int]

Test>
⊥₁ :: [Int]
⊥₁: deserialize: Invalid format
CallStack (from HasCallStack):
  error, called at ./NGram.lhs:401:19 in main:NGram
Test>
⊥₁ :: [Int]
⊥₁: deserialize: Invalid format
CallStack (from HasCallStack):
  error, called at ./NGram.lhs:401:19 in main:NGram
Test>
[7, 8, 9, 10] :: [Int]
Test>
[97, 98, 114, 97, 107, 97, 100, 97, 98, 114, 97] :: [Int]
Test>
[97, 97, 97, 98, 100, 97, 97, 97, 98, 97, 99] :: [Int]
Test>
[116, 111, 32, 98, 101, 32, 111, 114, 32, 110, 111, 116, 32, 116, 111, 32, 98, 101] :: [Int]
Test>
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] :: [Int]

Pontozás (elmélet + gyakorlat)