Gauss–Jordan-eliminá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

A Gauss–Jordan-elimináció többek közt lineáris egyenletrendszerek megoldására alkalmazható módszer. Egy lineáris egyenletrendszert bővített mátrixával adunk meg. Például az alábbi egyenletrendszerhez

3x + 2y +  z = 1
5x + 3y + 4z = 2
 x +  y -  z = 1

a következő mátrix tartozik:

--  x + y + z = b
[ [ 3,  2,  1,  1 ]
, [ 5,  3,  4,  2 ]
, [ 1,  1, -1,  1 ] ]

A feladatban feltesszük, hogy a mátrix annyi sorból áll, ahány egyenletünk van, ez pedig megegyezik az ismeretlenek számával. Ebből következik, hogy az oszlopok száma az egyenletek jobb oldalával kiegészítve pontosan egyel több a sorok számánál. Szintén feltesszük, hogy az egyenletrendszer megoldható.

Az algoritmus lényege, hogy a mátrixot addig transzformáljuk, míg az együtthatókat tartalmazó négyzetes mátrix (a példában az első három oszlop) egységmátrixszá nem alakul, azaz a főátlóban csak egyeseket, azon kívül pedig nullákat tartalmaz. Ebből az alakból már könnyedén leolvasható a változók értéke. Az algoritmus végeredménye az előző példa esetén:

--  x + y + z = b
[ [ 1,  0,  0, -4 ]
, [ 0,  1,  0,  6 ]
, [ 0,  0,  1,  1 ] ]

amely szerint az x = -4, y = 6, és z = 1 megoldást kapjuk.

Ennek az alaknak az elérésére a következő transzformációkat használhatjuk:

Az algoritmust lépésenként fogjuk összeállítani, így a részletes leírás az egyes részfeladatoknál található. Az egyenletrendszer mátrixát lebegőpontos számokat tartalmazó listák listájaként reprezentáljuk a továbbiakban:

type Matrix = [[Float]]

azaz minden egyenlethez tartozik egy lista, amely tartalmazza a benne szereplő együtthatókat, illetve a jobb oldalát. Az egyes részfeladatok teszteléséhez használható a fentebb bemutatott példa:

demo :: Matrix
demo = [ [ 3,  2,  1,  1 ]
       , [ 5,  3,  4,  2 ]
       , [ 1,  1, -1,  1 ] ]

valamint az alábbi, háromismeretlenes rendszert leíró mátrix:

threeEqs :: Matrix
threeEqs = [ [ 1, -1, 2, 3 ]
           , [ 2,  3, 1, 5 ]
           , [ 3,  2, 1, 8 ] ]

és az alábbi, egyszerűbb elemekből álló, kétismeretlenes rendszer is:

twoEqs :: Matrix
twoEqs = [ [ 1, 2, 3 ]
         , [ 4, 5, 6 ] ]

Mátrix szétválasztása (1 pont)

Készítsük el azt a függvényt, amellyel ketté tudjuk bontani a mátrixot! A felbontás során a jobb szélső oszlop értékei egy külön listába kerülnek, így az egyenletek együtthatói pedig egy négyzetes mátrixot alkotnak.

A leválasztott listája lényegében az egyenletek jobb oldalán álló értékek vektora lesz, amelyhez megadunk egy rövidítést:

type Vector = [Float]
split :: Matrix -> (Matrix, Vector)

Test>
([[3.0, 2.0, 1.0], [5.0, 3.0, 4.0], [1.0, 1.0, -1.0]], [1.0, 2.0, 1.0]) :: (Matrix, Vector)
Test>
([[1.0, 2.0], [4.0, 5.0]], [3.0, 6.0]) :: (Matrix, Vector)
Test>
([[1.0, -1.0, 2.0], [2.0, 3.0, 1.0], [3.0, 2.0, 1.0]], [3.0, 5.0, 8.0]) :: (Matrix, Vector)

Részmátrix képzése (1 pont)

Adjunk meg egy függvényt, amellyel elhagyjuk az adott mátrix n darab első sorát és n darab első oszlopát!

crop :: Int -> Matrix -> Matrix

Test>
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] :: Matrix
Test>
[[5.0, 6.0]] :: Matrix
Test>
[] :: Matrix
Test>
[[3.0, 4.0, 2.0], [1.0, -1.0, 1.0]] :: Matrix
Test>
[[-1.0, 1.0]] :: Matrix
Test>
[[1.0, 8.0]] :: Matrix

Adott indexű elemek megcserélése egy listában (2 pont)

Készítsünk egy függvényt, amely adott indexű elemeket megcserél egy listában! Az elemek indexelése nullától kezdődik, hibás index esetén a függvény adjon hibajelzést!

swapItems :: Int -> Int -> [a] -> [a]

Test>
⊥₁ :: [Integer]
⊥₁: swapItems: index out of range
CallStack (from HasCallStack):
  error, called at ./GaussJordan.lhs:157:30 in main:GaussJordan
Test>
⊥₁ :: [Integer]
⊥₁: swapItems: index out of range
CallStack (from HasCallStack):
  error, called at ./GaussJordan.lhs:157:30 in main:GaussJordan
Test>
"lelho" :: [Char]
Test>
"abc" :: [Char]
Test>
True :: Bool
Test>
⊥₁ :: [[Float]]
⊥₁: swapItems: index out of range
CallStack (from HasCallStack):
  error, called at ./GaussJordan.lhs:157:30 in main:GaussJordan
Test>
[[4.0, 5.0, 6.0], [1.0, 2.0, 3.0]] :: [[Float]]
Test>
[[3.0, 2.0, 1.0, 8.0], [2.0, 3.0, 1.0, 5.0], [1.0, -1.0, 2.0, 3.0]] :: [[Float]]
Test>
[[1.0, 1.0, -1.0, 1.0], [5.0, 3.0, 4.0, 2.0], [3.0, 2.0, 1.0, 1.0]] :: [[Float]]
Test>
[[3.0, 2.0, 1.0, 1.0], [1.0, 1.0, -1.0, 1.0], [5.0, 3.0, 4.0, 2.0]] :: [[Float]]

Oszlopcsere listák listájában (1 pont)

Definiáljuk azt a függvényt, amellyel meg tudjuk cserélni listák listájának két, adott indexű oszlopát! Használjuk a swapItems függvényt!

swapColumns :: Int -> Int -> [[a]] -> [[a]]

Test>
[⊥₁, ⊥₂] :: [[Float]]
⊥₁: swapItems: index out of range
CallStack (from HasCallStack):
  error, called at ./GaussJ
++ […, ……]

⊥₂: swapItems: index out of range
CallStack (from HasCallStack):
  error, called at ./Gau
++ [……]
Test>
[[2.0, 1.0, 3.0], [5.0, 4.0, 6.0]] :: [[Float]]
Test>
[[2.0, 1.0, 3.0], [5.0, 4.0, 6.0]] :: [[Float]]
Test>
[[3.0, 1.0, 2.0, 1.0], [5.0, 4.0, 3.0, 2.0], [1.0, -1.0, 1.0, 1.0]] :: [[Float]]
Test>
[[1.0, 2.0, 1.0, 3.0], [2.0, 3.0, 4.0, 5.0], [1.0, 1.0, -1.0, 1.0]] :: [[Float]]

Maximális abszolútértékű cella indexe (3 pont)

Adjuk meg azt a függvényt, amely kiszámítja egy négyzetes mátrix legnagyobb abszolútértékű elemének indexét (sor, oszlop) alakban! Az elemek indexelése itt is nullától kezdődik, feltételezhetjük, hogy ilyen elemet mindig találunk. Ha az elem nem egyértelmű, akkor a sorfolytonos kiolvasás szerint hátrébb lévő elem indexét vegyük!

maxAbsPosition :: Matrix -> (Int, Int)

Test>
(1, 0) :: (Int, Int)
Test>
(1, 0) :: (Int, Int)
Test>
(2, 0) :: (Int, Int)

Segítség: Az abszolútértéket az abs függvénnyel ki tudjuk számolni.

Egy sor szorzása skalárral és hozzáadása egy másikhoz (3 pont)

Készítsünk egy függvényt, amely meg tudja szorozni a mátrix megadott indexű sorát egy adott skaláris értékkel, majd hozzáadja azt egy másikhoz!

multiplyAccumulate :: Matrix -> Int -> Int -> Float -> Matrix

Test>
[[1.0, 2.0, 3.0], [6.0, 9.0, 12.0]] :: Matrix
Test>
[[1.0, -1.0, 2.0, 3.0], [2.0, 3.0, 1.0, 5.0], [0.0, 5.0, -5.0, -1.0]] :: Matrix
Test>
[[15.0, 10.0, 5.0, 5.0], [5.0, 3.0, 4.0, 2.0], [1.0, 1.0, -1.0, 1.0]] :: Matrix

Skalárral való osztás származtatása (1 pont)

Adjuk meg a multiplyAccumulate függvény segítségével a mátrix egy adott indexű sorának elosztását egy skalárral!

divide :: Matrix -> Int -> Float -> Matrix

Test>
[[1.0, 2.0], [6.0, 8.0]] :: Matrix
Test>
[[1.0, -1.0, 2.0, 3.0], [2.0, 3.0, 1.0, 5.0], [6.0, 4.0, 2.0, 16.0]] :: Matrix
Test>
[[3.0, 2.0, 1.0, 1.0], [0.5, 0.3000002, 0.4000001, 0.20000005], [1.0, 1.0, -1.0, 1.0]] :: Matrix

Az elimináció egy lépése (3 pont)

Az algoritmus n. lépése a következő:

  1. Választunk egy vezérelemet (pivot) az együttható mátrixból.

    • Ehhez először válasszuk le az egyenletek jobb oldalát (split).

    • Az együttható mátrixnak csak azt a részét vizsgáljuk, amiből már n oszlopot és n sort elhagytunk balról és fentről, ugyanis ezekkel már végeztünk (crop).

    • A legnagyobb abszolútértékű elemet választjuk (maxAbsPosition). Ezzel az algoritmus úgynevezett teljes főelemkiválasztásos változatát készítjük el, amely a numerikus stabilitását javítja.

  2. A vezérelemet a sorok és oszlopok cseréjével az eredeti mátrix n. sorába és oszlopába visszük (swapItems és swapColumns).

  3. Az n. sort leosztjuk (divide) a vezérelemmel, így az (n, n) pozíción a mátrixban 1 lesz az érték.

  4. Az n. sor annyiszorosát vonjuk ki (multiplyAccumulate) az összes többi sorból, hogy ott az n. oszlopban nulla elemek legyenek. Ezzel az n. lépés után az n. oszlopban csak nullák lesznek, illetve egy 1-es az n. sorban.

  5. Az oszlopcseréket feljegyezzük, mert az eredményben megváltoztatják a változók sorrendjét.

A cseréket az alábbi módon tároljuk:

type Swaps = [(Int, Int)]

ahol a párok első fele tartalmazza, hogy melyik indexre, a második fele pedig, hogy melyik indexről vittünk oszlopot. Az egyes lépések az új cserét a lista elejére fűzik. Az egyszerűség kedvéért azt is feljegyezzük, ha valójában nem történik csere, tehát az i. oszlopot az i. oszlopra cserélve (i, i) elem is bekerül.

A lépés lényegében egy átmenet két állapot között. Egy állapot az alábbi információkat tartalmazza:

Ennek megfelelően az állapot leírására az alábbi rendezett hármast használjuk:

type State = (Int, Matrix, Swaps)

Adjunk meg a hozzá tartozó állapotátmeneteket definiáló függvényt!

step :: State -> State

Test>
(1, [[1.0, 0.79999995, 1.1999998], [0.0, -0.5999999, 0.6000004]], [(1, 0)]) :: State
Test>
(2, [[1.0, 0.0, 0.7777777, 0.6666666], [0.0, 1.0, -0.22222227, -0.33333337], [0.0, 0.0, -0.111110866, -0.6666666]], [(2, 1), (0, 0)]) :: State
Test>
(3, [[1.0000004, 0.0, 0.0, 2.8000007], [-1.0728837e-7, 1.0, 0.0, -0.20000029], [-1.7881396e-7, 0.0, 1.0, -5.364419e-7]], [(2, 2), (1, 1), (0, 0)]) :: State

Teljes elimináció (2 pont)

A teljes elimináció abból áll, hogy egy kezdőállapotból annyiszor ismételjük meg a fenti lépést, ahány egyenlet van. A kezőállapot elemei:

Amint a lépéskor kapott állapotban n eléri az egyenletek számát, az eredmény mátrixot és a cseréket egy párban adjuk vissza. (Így végül az együtthatók helyén egységmátrixot kaptunk.)

eliminate :: Matrix -> (Matrix, Swaps)

Test>
([[1.0, 0.0, 2.0000005], [0.0, 1.0, -1.0000008]], [(1, 1), (1, 0)]) :: (Matrix, Swaps)
Test>
([[1.0, 0.0, 0.0, -4.0000095], [0.0, 1.0, 0.0, 1.0000031], [0.0, 0.0, 1.0, 6.000013]], [(2, 2), (2, 1), (0, 0)]) :: (Matrix, Swaps)
Test>
([[1.0000004, 0.0, 0.0, 2.8000007], [-1.0728837e-7, 1.0, 0.0, -0.20000029], [-1.7881396e-7, 0.0, 1.0, -5.364419e-7]], [(2, 2), (1, 1), (0, 0)]) :: (Matrix, Swaps)

Egyenletrendszer megoldása (2 pont)

Az egyenletrendszer megoldása a következő lépésekkel végezhető el:

  1. Az egyenletrendszer mátrixán elvégezzük az eliminációt (eliminate).

  2. Leválasztjuk az eliminált mátrix jobb szélső oszlopát (split). Ez tartalmazza az értékeket az egyes változókhoz. Az együttható mátrixra nincs szükség a továbbiakban.

  3. Az oszlopcserék miatt a változók értékeit meg kell cserélni (swapItems). Ehhez az elimináció közben gyűjtött oszlopcsere listát kell balról végigjárni, és a cseréket az eredmény listán elvégezni.

solve :: Matrix -> Vector

Test>
[-1.0000008, 2.0000005] :: Vector
Test>
[-4.0000095, 6.000013, 1.0000031] :: Vector
Test>
[2.8000007, -0.20000029, -5.364419e-7] :: Vector

Pontozás (elmélet + gyakorlat)