A Burrows-Wheeler-Scott-transzformáció

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 a Burrows-Wheeler-Scott-transzformációt valósítjuk meg. Ennek segítségével véges sorozatoknak egy olyan permutációját tudjuk meghatározni, amelyben nagyobb valószínűséggel fordulnak elő ismétlődő elemekből álló részsorozatok. A transzformáció érdekessége, hogy invertálható, vagyis a kiszámított eredményből pontosan visszaállítható az eredeti sorozat. Ezen kedvező tulajdonságai miatt gyakran alkalmazzák elő- és utófeldolgozási lépésként olyan veszteségmentes tömörítést végző programokban, mint a bzip2(1). De bármelyik más, futamhossz-kódolást használó tömörítés esetében is sikerrel bevethető.

Például a transzformációval a "BANANA" bemenetből a "ANNBAA" kimenetet fogjuk nyerni. Ez, bár nem teszi minden esetben egymás mellé az összes ismétlődő karaktert, egy olyan változata lesz a kiinduló szövegnek, ahol már hosszabb futamok jönnek létre.

Lehetséges forgatások (2 pont)

Készítsünk egy olyan függvényt, amely meghatározza egy véges sorozat összes lehetséges forgatásait! Ez egy n hosszúságú sorozat esetében egy n elemű lista felépítését jelenti, ahol az egymásra következő sorozatok elemei abban térnek el egymástól, hogy az elemeiket balra csúsztatjuk és a bal oldalt így kieső elem a sorozat jobb szélére kerül.

rotations :: [a] -> [[a]]

Test>
[] :: [[()]]
Test>
[[True, False], [False, True]] :: [[Bool]]
Test>
[[1, 2, 3, 4, 5], [2, 3, 4, 5, 1], [3, 4, 5, 1, 2], [4, 5, 1, 2, 3], [5, 1, 2, 3, 4]] :: [[Integer]]
Test>
["BANANA", "ANANAB", "NANABA", "ANABAN", "NABANA", "ABANAN"] :: [[Char]]

Lyndon-szavak ellenőrzése (1 pont)

Adjunk meg egy olyan függvényt, amely el tudja dönteni egy véges sorozatról, hogy Lyndon-szó! Lyndon-szónak nevezzük azokat a véges sorozatokat, amelyekre teljesül az, hogy a sorozat lexikografikusan mindig kisebb vagy egyenlő, mint bármelyik forgatása.

Segítségül: Mivel itt a véges sorozatokat listákkal ábrázoljuk, amelyekre Haskellben alapból adott a lexikografikus rendezés az Ord típusosztályon keresztül, alkalmazhatjuk egyszerűen csak a (<=) operátort az összehasonlításra.

isLyndonWord :: Ord a => [a] -> Bool

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

Lyndon-szavakra bontás (3 pont)

Definiáljunk egy függvényt, amely egy adott véges sorozatot Lyndon-szavakra bont! Ennek során arra kell törekednünk, hogy a sorozat elejétől indulva, mindig a lehető leghosszabb Lyndon-szavakat vágjuk le, majd gyűjtsük össze! Az eredményül keletkező részsorozatokra teljesülnie kell, hogy összefűzésükkel az eredeti sorozatot kapjuk vissza.

lyndonWords :: Ord a => [a] -> [[a]]

Test>
[] :: [[()]]
Test>
[[9, 9]] :: [[Integer]]
Test>
["lyn", "don"] :: [[Char]]
Test>
["B", "ANAN", "A"] :: [[Char]]
Test>
[[0, 1, 1], [0, 0, 0, 1, 1], [0]] :: [[Integer]]
Test>
["S", "COTTIFI", "C", "ATION"] :: [[Char]]

A Lyndon-szavak lehetséges forgatásai (2 pont)

Írjunk egy olyan függvényt, amely egy véges sorozathoz előállítja a belőle képezhető Lyndon-szavak (lyndonWords) összes lehetséges forgatását (rotations), majd ezeket egy összefüggő listába helyezi!

lyndonRotations :: Ord a => [a] -> [[a]]

Test>
[] :: [[()]]
Test>
[[9, 9], [9, 9]] :: [[Integer]]
Test>
[[0, 0, 1, 1], [0, 1, 1, 0], [1, 1, 0, 0], [1, 0, 0, 1], [0, 0], [0, 0]] :: [[Integer]]
Test>
["B", "ANAN", "NANA", "ANAN", "NANA", "A"] :: [[Char]]
Test>
["S", "COTTIFI", "OTTIFIC", "TTIFICO", "TIFICOT", "IFICOTT", "FICOTTI", "ICOTTIF", "C", "ATION", "TIONA", "IONAT", "ONATI", "NATIO"] :: [[Char]]

Adott méretre igazítás ismétléssel (1 pont)

Készítsünk egy függvényt, amellyel egy véges sorozatot adott méretűre tudunk nyújtani úgy, hogy a végtelenségig ismételten egymás után fűzzük az elemeit! A paraméterként megadott méret lehet rövidebb, mint a sorozat eredeti hossza, ilyenkor annak az adott hosszúságú kezdőrészletét adjuk vissza.

tile :: Int -> [a] -> [a]

Test>
[] :: [()]
Test>
[] :: [Integer]
Test>
[1, 1, 1, 1, 1, 1, 1, 1] :: [Integer]
Test>
"ANAANAANAA" :: [Char]
Test>
"xXxXxXxXxXxX" :: [Char]
Test>
"BANANABANANABANA" :: [Char]

Lyndon-szavak összehasonlítása (1 pont)

Adjuk meg a Lyndon-szavak összehasonlítására vonatkozó függvényt! Ez hasonló a compare függvényhez, és egy Ordering típusú értékkel tér vissza: EQ, ha a két szó megegyezik, LT, ha az első kisebb, illetve GT, ha nagyobb. Az összehasonlítás alapja, hogy nem egyszerűen csak a két sorozatot vetjük össze lexikografikusan, hanem előtte a rövidebbet mindig kiegészítjük a hosszabb méretére úgy, hogy a végtelenségig ismételve maga után fűzögetjük (tile).

lyndonCompare :: Ord a => [a] -> [a] -> Ordering

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

A részlisták utolsó elemei (1 pont)

Készítsünk egy függvényt, amellyel meg tudjuk adni listába foglalt véges sorozatok utolsó elemeit!

lasts :: [[a]] -> [a]

Test>
[] :: [()]
Test>
[True] :: [Bool]
Test>
[1, 2, 3, 4] :: [Integer]
Test>
"SICN" :: [Char]

A Burrows-Wheeler-Scott-transzformáció (1 pont)

Az előbbiek felhasználásával adjuk meg a Burrows-Wheeler-Scott-transzformációt megvalósító függvényt! A működési elve a következő lesz:

  1. Határozzuk meg a bemenetként kapott véges sorozat Lyndon-szavainak összes lehetséges forgatását (lyndonRotations).

  2. Rendezzük az így kapott listát a Lyndon-szavakra érvényes rendezés (lyndonCompare) szerint.

  3. Vegyük az eredménylistában szereplő sorozatok utolsó elemeit (lasts).

Tekintsük át az egészet egy konkrét példán keresztül! Legyen a bemenet a

"BANANA"

szó. Ezt a következő Lyndon-szavakra tudjuk bontani:

"B", "ANAN", "A"

Ennek a lehetséges forgatásai az alábbiak:

"B" ("B"), "ANAN", "NANA", "ANAN", "NANA" ("ANAN"), "A" ("A")

Rendezzük:

"A", "ANAN", "ANAN", "B", "NANA", "NANA"

Végül kivesszük az utolsó betűket mindegyik szóból:

"A"    -> 'A'
"ANAN" -> 'N'
"ANAN" -> 'N'
"B"    -> 'B'
"NANA" -> 'A'
"NANA" -> 'A'

Tehát az eredmény:

ANNBAA
bwst :: Ord a => [a] -> [a]

Test>
[] :: [()]
Test>
[3, 1, 2] :: [Integer]
Test>
"THOSR" :: [Char]
Test>
"ANNBAA" :: [Char]
Test>
"NCIIFTTOICSTAO" :: [Char]

A rendezett sorozat indexei (2 pont)

Írjunk egy függvényt, amellyel egy véges sorozat esetében meg tudjuk határozni az indexek listáját annak megfelelően, hogy az elemek növekvőleg miként lennének rendezettek! Az indexek számozását nullától kezdjük.

Például vegyük a [11,-2,4] sorozatot, amelynek a rendezett változata az lenne, hogy [-2,4,11]. Ezért az eredmény [1,2,0], mert az eredeti sorozat második eleme (-2) lenne a rendezett sorozat első eleme, aztán a harmadik eleme (4) lenne a következő, végül az első (11).

sortedIndices :: Ord a => [a] -> [Int]

Test>
[] :: [Int]
Test>
[1, 2, 0] :: [Int]
Test>
[0, 4, 5, 3, 1, 2] :: [Int]
Test>
[0, 1, 2, 3] :: [Int]
Test>
[4, 3, 0, 1, 2, 5] :: [Int]
Test>
[12, 1, 9, 4, 2, 3, 8, 0, 7, 13, 10, 5, 6, 11] :: [Int]

A visszanyeréshez szükséges táblázat előállítása (1 pont)

A transzformáció invertálásához az előbbi függvények felhasználásával definiáljuk azt a függvényt, amely kiszámít egy táblázatot. Később ezt a táblázatot követve leszünk képesek visszaállítani az eredeti szövegből képzett Lyndon-szavakat. Ehhez mindössze ki kell számítanunk a sorozat elemire, hogy a növekvőleg rendezett változatban melyik pozíción szerepelnének (sortedIndices), és azt összekapcsolni a rendezett változat elemeivel.

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

Test>
[] :: [((), Int)]
Test>
[(-2, 1), (4, 2), (11, 0)] :: [(Integer, Int)]
Test>
[(1, 0), (1, 3), (2, 1), (2, 4), (3, 2), (3, 5)] :: [(Integer, Int)]
Test>
[('A', 0), ('A', 4), ('A', 5), ('B', 3), ('N', 1), ('N', 2)] :: [(Char, Int)]
Test>
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)] :: [(Integer, Int)]
Test>
[('A', 12), ('C', 1), ('C', 9), ('F', 4), ('I', 2), ('I', 3), ('I', 8), ('N', 0), ('O', 7), ('O', 13), ('S', 10), ('T', 5), ('T', 6), ('T', 11)] :: [(Char, Int)]

A Lyndon-szavak felépítése a táblázat alapján (5 pont)

A transzformáció hatásának megfordításához az előző (generateMap) függvénnyel előállított táblázatot kell követnünk ahhoz, hogy fel tudjuk építeni a neki megfelelő Lyndon-szavakat.

Ennek az a lényege, hogy a táblázat egyes sorai egyrészt azt adják meg, hogy melyik elemet kell a következő lépésben beilleszteni az eredménybe, másrészt a hozzá kapcsolt indexen keresztül megadják a táblázatban azt a sort, ahol folytatódik a feldolgozás. Az indexek követésekor azonban előfordulhat, hogy egy olyan pozícióra kerülünk, ahol korábban már jártunk. Ez azt jelenti, hogy sikeresen dekódoltunk egy Lyndon-szót, és az első olyan sorral kell folytatnunk a feldolgozást, ahol eddig még nem jártunk. A feldolgozás mindig kezdődjön az első sorral!

Erre illusztrációként vegyük azt a táblázatot, amelyet a "ANNBAA" szóból hoztuk létre:

'A' 0
'A' 4
'A' 5
'B' 3
'N' 1
'N' 2

Elsőként az első sort vesszük, ahol az 'A' karakter és az 0 index szerepel. Mivel önmagára mutat az index, egyből képezünk belőle egy szót (látott elemek: [0]):

"A"

A feldolgozást a következő még nem látott sorral, a másodikkal, az ('A', 4) párral folytatjuk (látott elemek: [0, 4]).

"A", "A

Ez az ötödik elemre, a ('N', 1) párra mutat (látott elemek: [0, 1, 4]):

"A", "AN

A mutató miatt a feldolgozást a második sorban folytatnánk, de mivel már ott is jártunk, egy újabb szót képezünk:

"A", "AN"

A következő még nem látott sor a harmadik, vagyis az ('A', 5) pár (látott elemek: [0, 1, 2, 4]):

"A", "AN", "A

A neki megfelelő index a hatodik sorra mutat, vagyis a ('N', 2) párra (látott elemek: [0, 1, 2, 4, 5]):

"A", "AN", "AN

Mivel a következőként mutatott pozíció a harmadik, ahol már szintén jártunk, a szót lezárjuk és a feldolgozást a következő még nem látott sorban kezdjük, ez pedig a negyedik, vagyis a ('B', 3) pár (látott elemek: [0, 1, 2, 3, 4, 5]):

"A", "AN", "AN", "B

Itt is az index olyan, amit már láttunk, ezért lezárjuk a szót. És mivel ezzel együtt láttuk az összes elemet a táblázatban, az eredmény is elkészült:

"A", "AN", "AN", "B"
rebuildLyndonWords :: [(a, Int)] -> [[a]]

Test>
[] :: [[()]]
Test>
["FOO"] :: [[Char]]
Test>
["COTT", "S"] :: [[Char]]
Test>
["AR", "B", "FOO"] :: [[Char]]
Test>
["A", "AN", "AN", "B"] :: [[Char]]
Test>
["ATION", "C", "COTTIFI", "S"] :: [[Char]]

Inverz Burrows-Wheeler-Scott-transzformáció (1 pont)

Az előbbi függvényekkel adjuk meg a Burrows-Wheeler-Scott-transzformáció inverzét! A visszaállítás menete legyen a következő:

  1. Állítsuk elő a bemenetként kapott véges sorozatból a neki megfelelő segédtáblázatot (generateMap).

  2. A táblázat felhasználásával építsük újra a sorozathoz tartozó Lyndon-szavakat (rebuilldLyndonWords).

  3. A kapott Lyndon-szavakat rendezzük a rájuk vonatkozó rendezés (lyndonCompare) szerint csökkenő sorrendben. (Vigyázzunk arra, hogy az eredeti rendezés növekvőleg rendez!)

  4. Fűzzük egybe a kapott részsorozatokat.

Mivel inverz, ezért minden kimenetre, amelyet a bwst függvénnyel hoztuk létre, vissza kell tudnunk állítani a bwst eredeti bemenetét. Vagyis a két függvény kompozíciója az identitás függvény kell, legyen.

A példa kedvéért megadjuk, hogy a "BANANA" szóból képzett "ANNBAA" esetében a visszaállítás részleteiben miként fog történni.

Létrehozzuk a "ANNBAA" szónak megfelelő segédtáblázatot, amely az alábbi lesz:

'A' 0
'A' 4
'A' 5
'B' 3
'N' 1
'N' 2

Ebből a következő Lyndon-szavakat építjük fel:

"A", "AN", "AN", "B"

Ezeket aztán csökkenőleg rendezzük:

"B", "AN", "AN", "A"

Végül összefűzzük:

BANANA
ibwst :: Ord a => [a] -> [a]

Test>
[] :: [Char]
Test>
[11, -4, 2] :: [Integer]
Test>
[1, 2, 3, 1, 2, 3] :: [Integer]
Test>
"BANANA" :: [Char]
Test>
"SCOTTIFICATION" :: [Char]

Pontozás (elmélet + gyakorlat)