• Keine Ergebnisse gefunden

Implementierung von Algorithmen für Grammatik-komprimierte Matrizen in der funktionalen Programmiersprache Haskell

N/A
N/A
Protected

Academic year: 2022

Aktie "Implementierung von Algorithmen für Grammatik-komprimierte Matrizen in der funktionalen Programmiersprache Haskell"

Copied!
65
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

Bachelorarbeit

Implementierung von Algorithmen für Grammatik-komprimierte Matrizen in der funktionalen Programmiersprache

Haskell

Dennis Reichelt und Raphael Willhauk

24. Oktober 2013

eingereicht bei

Professor Dr. Manfred Schmidt-Schauß Künstliche Intelligenz / Softwaretechnologie

Fachbereich Mathematik und Informatik

Institut für Informatik

(2)
(3)

Erklärung

Hiermit bestätigen wir, dass wir die vorliegende Arbeit selbstständig verfasst haben und keine anderen Quellen oder Hilfsmittel als die in dieser Arbeit angegebenen verwendet haben

Frankfurt am Main, den 24.10.2013

______________________ ______________________

Dennis Reichelt Raphael Willhauk

(4)
(5)

Inhaltverzeichnis

1 EINLEITUNG/MOTIVATION 7

1.1 MOTIVATION 7

1.2 AUFGABENSTELLUNG 8

1.3 ÜBERBLICK ÜBER DIE NACHFOLGENDEN KAPITEL 8

1.4 ARBEITSAUFTEILUNG 9

2 GRUNDLAGEN 10

2.1 MATRIZEN 10

2.1.1 DEFINITION EINER MATRIX UND SPEZIELLE MATRIZEN 10

2.1.2 MATRIXOPERATIONEN 10

2.2 GRUNDLAGEN AUS DER LINEAREN ALGEBRA 12

2.2.1 LINEARE GLEICHUNGSSYSTEME 12

2.2.2 DER GAUß-JORDAN-ALGORITHMUS 13

2.2.3 LINEARE ABHÄNGIGKEIT 15

2.3 FUNKTIONALE PROGRAMMIERUNG UND DIE SPRACHE HASKELL 17

2.3.1 FUNKTIONALE PROGRAMMIERUNG 17

2.3.2 HASKELL 18

3 MTDD+ ZUR KOMPRIMIERTEN DARSTELLUNG VON MATRIZEN 21

3.1 MTDD+S 21

3.2 MATRIXOPERATIONEN AUF MTTD+S 24

3.2.1 SKALARMULTIPLIKATION 24

3.2.2 TRANSPOSITION 25

3.2.3 SUMME ALLER MATRIXELEMENTE 25

3.2.4 SPUR EINER MATRIX 26

3.2.5 MATRIXELEMENT BERECHNEN 27

3.2.6 MATRIXMULTIPLIKATION 29

3.2.7 GLEICHHEITSTEST 30

4 IMPLEMENTATIONSDOKUMENTATION 33

4.1 DATENTYPEN 33

4.2 AUSWERTEN VON CIRCUITS 34

4.3 MATRIX-OPERATIONEN 37

4.3.1 SUMME DER EINTRÄGE UND SPUR EINER MATRIX 37

4.3.2 MATRIX TRANSPONIEREN 37

4.3.3 SKALARMULTIPLIKATION 38

4.3.4 MATRIXELEMENT BERECHNEN 38

4.3.5 MATRIXMULTIPLIKATION 39

4.3.6 GLEICHHEITSTEST 42

5 PERFORMANCE-TESTS 51

(6)

5.1 DAS TESTSYSTEM 51

5.2 DATENTYP FÜR NONTERMINAL 51

5.2.1 ERSTE TESTPHASE 51

5.2.2 ZWEITE TESTPHASE 53

5.3 DIE TESTMATRIZEN 53

5.4 CRITERION 53

5.5 TESTS MIT CRITERION 55

5.5.1 MAP-TESTS TEIL 2 56

5.5.2 TRANSPOSITION 59

5.5.3 SKALARMULTIPLIKATION 60

5.5.4 MATRIXELEMENT BERECHNEN 61

5.5.5 GLEICHHEITSTEST 62

5.5.6 ERGEBNISÜBERSICHT 63

6 ZUSAMMENFASSUNG UND AUSBLICK 64

6.1 ZUSAMMENFASSUNG 64

6.2 AUSBLICK 64

7 LITERATURVERZEICHNIS 65

(7)

1 Einleitung/Motivation

1.1 Motivation

Matrizen, also zweidimensionale Zahlenfelder, finden Verwendung in einer Vielzahl von wissenschaftlichen und alltäglichen Bereichen (bspw. digitale Repräsentation von pixelbasierten Bildern, Verarbeitung wissenschaftlicher Messergebnisse). Aus diesem Grund besteht natürlich ein allgemeines Interesse, Matrizen möglichst platzsparend speichern zu können sowie im Idealfall Operationen auf den komprimierten Daten effizient ausführen zu können.

Die Motivation, einen neuen Ansatz der Matrixkompression zu verfolgen besteht darin, dass die bekannten Algorithmen in weiten Teilen auf Matrizen, bei denen der überwiegende Teil der Einträge aus Nullen besteht (sog. dünnbesetzte Matrizen), operieren. Das Prinzip dieser Algorithmen besteht darin, nur diejenigen Einträge zu speichern, welche sich von Null unterscheiden, wobei die Positionen dieser Einträge entweder direkt als Koordinaten gespeichert werden oder sich anhand der benutzten Datenstruktur berechnen lassen.

Beispiel

Darstellung einer 4x4 Matrix als Liste von Listen, in denen Tupel der Form (Spalte, Wert) gespeichert werden:

[

] [

( ) ( ) ( )

( ) ]

Anstelle von 16 Werten speichert die gezeigte Darstellung nur noch acht (je vier Ordinaten und Einträge). Damit reduziert sich der Speicherplatzbedarf der Matrix theoretisch um 50%.

Der Nachteil an dieser Darstellung ist, dass beim Abfragen eines bestimmten Elements in der Liste der jeweiligen Zeile geschaut werden muss, ob ein von Null verschiedener Eintrag vorhanden ist. Sind die Einträge in den Listen sortiert, so ist diese Aufgabe auch mit einer binären Suche lösbar, damit nicht die ganze Liste durchgegangen werden muss. Verändern von Einträgen ist bei dieser Darstellung möglich, indem man das zu verändernde Element sucht und entweder ändert, wenn es gefunden wurde oder neu einfügt, wenn es nicht gefunden wurde.

Die Matrixmultiplikation ist in dieser Darstellung aber relativ schwierig, da jedes Element, welches an dem jeweiligen Schritt beteiligt ist in den Listen gesucht werden muss und die Listen dadurch etliche Male durchsucht werden müssen, sodass die Laufzeit für diese Operation stark ansteigen kann.

Bei sehr schwach besetzten Matrizen kann man statt einer Liste von Listen auch ein Wörterbuch nutzen, bei dem die Schlüssel die Koordinaten des Eintrages repräsentieren. Es gibt auch noch einige weitere darauf aufbauende Darstellungsmethoden, die bekannteste davon ist wohl das Yale-Format und ihre Abwandlungen, auf die wir hier aber nicht weiter eingehen wollen. Alle Methoden haben aber gemeinsam, dass sie bei dicht besetzten Matrizen keinen Kompressionserfolg erzielen und den Speicherplatzbedarf der Matrix bedingt durch die verwendeten Datenstrukturen sogar erhöhen.

Für vollbesetzte Matrizen existieren spezielle Formate wie JPEG, bei dem die Pixelmatrizen mittels einer diskreten Fourier-Transformation komprimiert werden und auch tatsächlich noch einige einfache Operationen auf den komprimierten Grafiken effizient möglich sind.

(8)

Allerdings ist diese Art der Kompression verlustbehaftet, was in meisten Fällen nicht praktikabel ist. Auf Matrizen, die mittels anderer gängiger Kompressionsalgorithmen wie z.B.

der Huffman-Kodierung, Run-Length-Encoding oder Varianten der Lempel-Ziv-Kodierung verlustfrei komprimiert wurden (bspw. GIF, PNG), lassen sich Operationen im Allgemeinen nicht effizient ausführen.

Wir beschäftigen uns im Rahmen dieser Arbeit mit einer grammatikbasierten komprimierten Darstellung von Matrizen, welche die effiziente Durchführung wesentlicher Matrixoperationen auf den komprimierten Daten erlaubt.

1.2 Aufgabenstellung

Im Rahmen dieser Bachelorarbeit sollen die in [Lo13] definierte Datenstruktur zur komprimierten Darstellung von Matrizen sowie in der gleichen Quelle vorgestellten Algorithmen zur effizienten Verarbeitung der komprimierten Daten unter Benutzung der funktionalen Programmiersprache Haskell implementiert werden.

1.3 Überblick über die nachfolgenden Kapitel

Zunächst gehen wir in Kapitel 2 auf für das Verständnis der Arbeit erforderliches Grundwissen über Matrizen und grundlegende Matrixoperationen sowie Grundlagen aus der linearen Algebra ein und geben einen Überblick über funktionale Programmierung im Allgemeinen sowie die im Rahmen dieser Arbeit verwendeten Programmiersprache Haskell im Speziellen.

Kapitel 3 umfasst die Definition von multi-terminal decision diagrams with addition (MTTD+) zur komprimierten Darstellung von Matrizen sowie einiger Algorithmen, welche eine effiziente Ausführung u.A. der im Grundlagenkapitel vorgestellten Matrixoperationen auf MTTD+s beschreiben. Des Weiteren wird auf Eigenschaften…

Im vierten Kapitel dokumentieren wir im Detail den im Rahmen dieser Arbeit angefertigten Programmcode, welcher Implementierungen der theoretischen Ausführungen aus Kapitel 3 beinhaltet.

Kapitel 5 umfasst Performancetests der von uns implementierten Algorithmen sowie Interpretationen der Testergebnisse. Dabei gehen wir auch auf Abweichungen zwischen den theoretischen Laufzeiten und unseren Messergebnissen ein.

Im sechsten und letzten Kapitel resümieren wir die Ergebnisse unserer Arbeit, fassen die gewonnenen Erkenntnisse zusammen und geben einen Ausblick über mögliche Erweiterungen der in dieser Arbeit behandelten Thematik.

(9)

1.4 Arbeitsaufteilung

Dennis Reichelt:

 Programmierung

o Circuits: Datentyp, evaluateCircuit

o Matrixoperationen: scalarMult, getElement, transpose, entrySum o PerfomanceTest

 Ausarbeitung:

o 1 - Einleitung Motivation: 1.1, 1.2, Arbeitsverteilung 1.4

o 4 - Implementations-Dokumentation (außer 1.3.6: Gleichheitstest) o 5 - Performancetests: Vollständig

Raphael Willhauk:

 Programmierung

o Module: MatrixEquallity, CoefficientMatrix, Validate

 Ausarbeitung:

o 1 - Einleitung: 1.3 Überblick o 2 - Grundlagen: Vollständig o 3 - MTDDs: Vollständig

o 4 - Implementations-Dokumentation: 1.3.6: Gleichheitstest o 6 – Zusammenfassung: 6.2

Von beiden bearbeitet:

 Programmierung

o Module: MatrixMult, MatrixDataType

 Ausarbeitung:

o 6 – Zusammenfassung: 6.1

(10)

2 Grundlagen

2.1 Matrizen

In diesem Abschnitt, der zu großen Teilen auf Inhalten aus [B12] basiert, definieren wir den Begriff der Matrix und stellen einige wichtige Operationen auf Matrizen vor, welche im weiteren Verlauf der Arbeit eine Rolle spielen werden.

2.1.1 Definition einer Matrix und spezielle Matrizen

Eine Matrix ist ein zweidimensionales Zahlenfeld, bestehend aus Zeilen und Spalten (man spricht von einer -Matrix) – sie beinhaltet folglich genau Elemente, wobei diese beliebige Zahlenwerte sein können.

Matrizen benennt man üblicherweise einen einzelnen Großbuchstaben, z.B. etc. Den Eintrag in der -ten Zeile und -ten Spalte der Matrix bezeichnet man mit .

Beispiel

( ) ist eine -Matrix.

ist das Element in der ersten Zeile und dritten Spalte von .

Matrizen lassen sich für diverse Zwecke nutzen, z.B. als kompakte Schreibweise von linearen Gleichungssystemen – darauf werden wir im nächsten Abschnitt genauer eingehen.

Als -te Einheitsmatrix bezeichnet man diejenige -Matrix (jede Einheitsmatrix ist demnach quadratisch), deren Diagonalelemente den Wert und alle übrigen Elemente den Wert besitzen. Üblicherweise bezeichnet man die -te Einheitsmatrix mit .

Beispiel

(

)

2.1.2 Matrixoperationen

2.1.2.1 Matrixtransposition

Die transponierte bzw. gespiegelte Matrix der Matrix erhält man durch Vertauschen der Zeilen und Spalten von - jedes ( ) wird umgewandelt zu .

Beispiel

( ) (

)

(11)

2.1.2.2 Matrixaddition

Man kann zwei Matrizen und prinzipiell nur addieren, wenn ihre Zeilen- und Spaltenanzahl übereinstimmt – alle anderen Konstellationen sind nicht definiert. Die Summenmatrix entsteht durch komponentenweises Addieren der Matrixeinträge von und , d.h. f.a. .

Beispiel

(

) (

) (

)

2.1.2.3 Spur einerMatrix

Als Spur (engl. „trace“) einer quadratischen Matrix bezeichnet man die Summe der Einträge auf ihrer Hauptdiagonalen.

( ) ∑

Beispiel

(

) ( )

2.1.2.4 Skalarmultiplikation

Man multipliziert einen Skalar mit einer Matrix , indem man jeden Eintrag von mit multipliziert. Hierbei gibt es keinerlei Einschränkungen bzgl. der Ausmaße der Matrix.

Beispiel

(

) (

)

2.1.2.5 Matrixmultiplikation

Die Multiplikation zweier Matrizen ist genau dann definiert, wenn die Spaltenanzahl der ersten Matrix der Zeilenanzahl der zweiten Matrix entspricht. Die Multiplikation einer -Matrix mit einer -Matrix resultiert in einer -Matrix .

Für die Einträge von gilt: , d.h. um in Matrix den Eintrag in Zeile und Spalte zu berechnen, multiplizieren wir jeweils die Element aus Zeile der Matrix mit den Element aus Spalte der Matrix (das erste mit dem ersten, das zweite mit dem zweiten usw.) und summieren diese Produkte auf.

Beispiel

( ) ( ) ( )

(12)

Das Element in der zweiten Zeile und ersten Spalte von berechnet sich dabei wie folgt:

( ) ( )

2.2 Grundlagen aus der linearen Algebra

Zunächst gehen wir auf lineare Gleichungssysteme ein, stellen anschließend den Gauß- Jordan-Algorithmus vor, welcher dazu benutzt werden kann, lineare Gleichungssysteme zu lösen, und definieren zuletzt den Begriff der linearen Unabhängigkeit von Vektorfamilien.

Die Ausführungen in diesem Abschnitt basieren zu weiten Teilen auf [Schü08].

2.2.1 Lineare Gleichungssysteme

Als lineares Gleichungssystem (kurz LGS) bezeichnet man eine Menge linearer Gleichungen, welche mehrere unbekannte Variablen enthalten. Es ist nicht dabei erforderlich, dass jede Unbekannte in jeder einzelnen Gleichung vorhanden ist.

Beispiel

LGS mit drei Gleichungen und drei Unbekannten:

Für , und sind alle Gleichungen erfüllt - man spricht dann von einer Lösung des Systems.

Prinzipiell lässt sich jedes LGS mit Gleichungen und Unbekannten in folgende Form bringen:

Lineare Gleichungssysteme ermöglichen es, Zusammenhänge mathematisch zu modellieren und anschließend unbekannte Größen zu berechnen.

Ein LGS nennt man homogen, wenn alle ( ) gleich Null sind, ansonsten nennt man es inhomogen. Während homogene LGS immer mindestens eine Lösung haben, nämlich die triviale Lösung, bei der alle ( ) gleich Null sind, existiert für inhomogene Gleichungssysteme nicht zwangsläufig eine Lösung.

Die Matrix (

) nennt man die Koeffizientenmatrix eines LGS.

Jeder Gleichung ist eine Zeile zugeordnet. Die Spalten entsprechen den im Gleichungssystem vorkommenden Unbekannten und die letzte Spalte der rechten Seite der Gleichungen. Sollte in einer Gleichung eine Variable nicht vorkommen, wird an deren Stelle in der Koeffizientenmatrix einfach eine eingetragen.

(13)

Enthält die Koeffizientenmatrix angehängt an die rechten Seite zusätzlich den Vektor ( ), spricht man von der erweiterten Koeffizientenmatrix. In diesem Fall entspricht die letzte Spalte demnach den jeweiligen rechten Seiten der Gleichungen.

Beispiel

Das lineare Gleichungssystem

mit 2 Gleichungen und 4 unbekannten Variablen lässt sich wie folgt als erweiterte Koeffizientenmatrix darstellen:

(

)

2.2.2 Der Gauß-Jordan-Algorithmus

Der nach den Mathematikern Carl Friedrich Gauß und Wilhelm Jordan benannte Algorithmus ermöglicht es, eine erweiterte Koeffizientenmatrix auf die sogenannte reduzierte Zeilenstufenform zu bringen, welche für jede Eingabematrix eindeutig ist. In [Schü8] werden die Eigenschaften einer Matrix in reduzierter Zeilenstufenform wie folgt benannt:

1. Falls eine Zeile nicht nur aus Nullen besteht (Nullzeile), ist die erste von Null verschiedene Zahl eine Eins (solch eine Eins nennen wir führende Eins).

2. Alle Nullzeilen stehen am Ende der Matrix

3. In zwei aufeinanderfolgenden Zeilen, die beide keine Nullzeilen sind, steht die führende Eins der unteren Zeile rechts der führenden Eins der oberen Zeile.

4. Eine Spalte, die eine führende Eins enthält, beinhaltet ansonsten nur Nullen.

Eine genaue Definition der reduzierten Zeilenstufenform ist in [Schr] zu finden.

.

Aus einer erweiterten Koeffizientenmatrix in reduzierter Zeilenstufenform lassen sich durch Rückumwandlung in ein LGS Informationen über dessen Lösungsmenge ablesen:

Besitzt ein LGS genau eine Lösung, lässt sich ebendiese direkt ablesen. Im Falle einer unendlichen Lösungsmenge liefert sie Gleichungen, welche diese Menge klar definieren.

Gibt es für das LGS keine Lösung, so erkennt man dies am Vorhandensein einer falschen Gleichung ( ).

Der Gauß-Jordan-Algorithmus ist eine Erweiterung des gaußschen Eliminationsverfahrens.

Wie in [D] genauer darstellt, bewegt sich die Anzahl der Multiplikationen, welche im Verlauf des Algorithmus bei Eingabe einer -Matrix durchgeführt werden, in der Größenordnung ( ).

Der Algorithmus besteht aus den folgenden Schritten:

1. Wiederhole die folgenden Schritte solange, bis die Matrix leer ist:

1.1. Suche beginnend von links die erste Spalte der Matrix, in der ein von Null verschiedener Eintrag vorkommt.

(14)

1.2. Befindet sich in der in der Schritt gefundenen Spalte der von Null verschiedene Eintrag in einer anderen Zeile als der ersten, vertausche diese beiden Zeilen.

1.3. Dividiere alle Einträge der ersten Zeile durch ihr erstes von Null verschiedenes Element.

1.4. Subtrahiere von allen übrigen Zeilen Vielfache der ersten Zeile derart, dass in der im ersten Schritt ermittelten Spalte außer in der ersten Zeile nur noch Nulleinträge zu finden sind.

1.5. Entferne die erste Zeile und die erste Spalte der Matrix.

2. Betrachte nun wieder die gesamte Matrix. Für jede Zeile, die eine führende Eins hat (wir sagen eine Zeile hat eine führende Eins, wenn auf eine beliebige Anzahl von Nullen irgendwann eine Eins folgt): Ziehe von allen darüber liegenden Zeilen entsprechende Vielfache dieser Zeile ab, sodass über der führenden Eins nur noch Nulleinträge vorhanden sind.

Beispiel

Lineares Gleichungssystem und die dazugehörige erweiterte Koeffizientenmatrix:

(

)

Die erste Spalte mit einem von Null verschiedenen Eintrag ist offensichtlich die erste – da die Zeile mit diesem Eintrag allerdings nicht die erste ist, führen wir eine Zeilenvertauschung durch (Schritt 1.1 und 1.2 des Algorithmus):

(

)

Anschließend teilen wir alle Einträge der ersten Zeile durch , sodass die Zeile eine führende Eins bekommt (Schritt 1.3):

(

)

Innerhalb der ersten Spalte befindet sich abgesehen vom obersten Eintrag noch ein weiterer von Null verschiedener Eintrag – diesen eliminieren wir, indem wir viermal die erste Zeile von der dritten Zeile subtrahieren (Schritt 1.4):

(

)

Nun entfernen wir die erste Zeile und die erste Spalte (Schritt 1.5). Es beginnt eine neue Iteration:

( )

(15)

Der erste von Null verschiedene Eintrag befindet sich bereits ganz oben in der ersten Spalte, sodass keine Vertauschung notwendig ist. Wir teilen die erste Zeile durch ( ):

( )

Alle anderen Einträge der ersten Spalte sind bereits Null – wir löschen also erneut die erste Zeile und erste Spalte:

( )

An dieser Stelle teilen wir lediglich die verbliebene Zeile durch ( ) und werfen anschließend wieder einen Blick auf die gesamte Matrix:

(

)

Nun gilt es noch, alle von Null verschiedenen Einträge über einer führenden Null zu eliminieren (Schritt 2) – zunächst ziehen wir -mal die zweite Spalte von der ersten ab:

(

)

Um die Einträge über der führenden Null der dritten Zeile zu eliminieren, ziehen wir letztendlich -mal die dritte von der ersten und ( )-mal die dritte von der zweiten Zeile ab:

(

)

Wir haben nun die Matrix reduzierte Zeilenstufenform errechnet. Aus dieser lässt sich unmittelbar ablesen, dass alle Gleichungen des LGS erfüllt sind, falls , sowie gilt.

2.2.3 Lineare Abhängigkeit

Man nennt eine Familie von Vektoren eines Vektorraums linear unabhängig, wenn eine den Nullvektor erzeugende Linearkombination dieser Vektoren nur dann möglich ist, falls alle Koeffizienten der Linearkombination gleich Null sind. Andernfalls lässt sich mindestens einer der Vektoren als Linearkombination anderer Vektoren der Familie darstellen – in diesem Fall sagt man, dass die Vektoren voneinander linear abhängig sind.

Formal bedeutet dies, dass eine Familie von Vektoren ⃗⃗⃗⃗ ⃗⃗⃗⃗ ⃗⃗⃗⃗ genau dann linear unabhängig ist, wenn

⃗⃗⃗⃗ ⃗⃗⃗⃗ ⃗⃗⃗⃗

nur dann gilt, wenn alle den Wert Null haben ( ist der Nullvektor). Dies kann man testen, indem man ein homogenes LGS mit als Unbekannten und den Vektoreinträgen als Koeffizienten aufstellt, daraus die erweiterte Koeffizientenmatrix berechnet und im Anschluss den Gauß-Jordan-Algorithmus anwendet.

(16)

Offensichtlich ist die maximale Anzahl linear unabhängiger -dimensionaler Vektoren durch beschränkt – hat man also mehr als Vektoren, ist die Familie der Vektoren definitiv linear abhängig. Gleichermaßen ist es möglich, dass eine Familie von weniger als -dimensionalen Vektoren linear abhängig ist. Ein triviales Beispiel hierfür ist eine Familie von identischen Vektoren, bei denen natürlich alle Kopien dieses Vektors bis auf eine redundant sind.

Beispiel

Wir wollen überprüfen, ob die drei Vektoren ⃗⃗⃗⃗ ( ), ⃗⃗⃗⃗ ( ) und ⃗⃗⃗⃗ (

) linear unabgängig sind.

Falls sie linear unabhängig sind, muss ⃗⃗⃗⃗ ⃗⃗⃗⃗ ⃗⃗⃗⃗ gelten.

Das zugehörige LGS sieht wie folgt aus:

Daraus ergibt sich die erweiterte Koeffizientenmatrix (

). Nach der Umwandlung in die reduzierte Zeilenstufenform mit dem Gauss-Jordan-Algorithmus erhalten wir die Matrix (

).

Es gibt also für die Lösung des LGS nur eine Möglichkeit, denn es gilt und . Somit sind die Vektoren linear unabhängig.

Unterzieht man die Vektoren ⃗⃗⃗⃗⃗ ( ), ⃗⃗⃗⃗⃗ ( ) und ⃗⃗⃗⃗⃗ (

) dem gleichen Verfahren, bekommen man am Ende folgende Matrix in reduzierter Zeilenstufenform: (

).

Daraus können wir die Gleichungen sowie ablesen. Demnach ist es offenbar auch möglich, das LGS zu lösen, ohne alle drei Koeffizienten auf Null zu setzen – vielmehr gibt es unendlich viele Lösungen (eine davon ist ).

Die Vektoren sind also linear abhängig.

Alternativ kann man aus den Werten der Matrix ablesen: Der Vektor ⃗⃗⃗⃗⃗ ist gleich zweimal dem Vektor ⃗⃗⃗⃗⃗ + 0,5-mal dem Vektor ⃗⃗⃗⃗⃗ .

Wir machen folgende Beobachtungen: Ein Vektor lässt sich nur dann nicht als Linearkombination anderer Vektoren darstellen, falls seine Spalte in der reduzierten Zeilenstufenform eine führende Eins enthält.

Alle anderen Vektoren könnte man entfernen, ohne dass sich der von der Vektorfamilie aufgespannte Vektorraum ändert.

Außerdem zeigt sich, dass es reicht, dem Gauß-Jordan-Algorithmus die Koeffizientenmatrix (nicht die erweiterte!) eines LGS zu übergeben, falls es homogen ist, da sich an der letzten nur aus Nullen bestehenden Spalte im Laufe des Algorithmus niemals etwas ändert. Dies werden wir zukünftig so handhaben, ohne es explizit zu erwähnen.

(17)

Es existiert eine für unsere Arbeit wichtige Analogie zwischen Familien von Vektoren und linearen Gleichungssystemen: Transponiert man die Koeffizientenmatrix eines homogenen LGS, kann man jede Spalte (die nun eine Gleichung repräsentiert) als einen Vektor auffassen. Dann gilt: Ist diese Familie von Vektoren linear unabhängig, so ist die Menge der Gleichungen minimal, d.h. man kann keine Gleichung weglassen, ohne dass sich die Lösungsmenge des LGS ändert. Falls die Familie von Vektoren jedoch linear abhängig ist, existiert mindestens eine Gleichung, die man entfernen kann, ohne dass sich die Lösungsmenge des LGS ändert. Dies ergibt Sinn, da man in dem Fall mindestens einen Vektor als Linearkombination anderer Vektoren und damit gleichbedeutend eine Gleichung als Linearkombination anderer Gleichungen darstellen kann – diese Gleichung liefert demnach keine zusätzlichen Informationen, welche die Lösungsmenge des LGS beeinflussen.

2.3 Funktionale Programmierung und die Sprache Haskell

Wir gehen in diesem Abschnitt auf das funktionale Programmierparadigma sowie die funktionale Programmiersprache Haskell ein. Die Ausführungen dieses Unterkapitels basieren weitestgehend auf [Li11], [Schm12a] sowie [Schm12b].

2.3.1 Funktionale Programmierung

Die funktionale Programmierung ist ein Programmierparadigma, also ein grundlegendes Programmierkonzept, welches man zur Gruppe der deklarativen Programmierparadigmen zählt. Im Gegensatz zur imperativen Programmierung, bei der Folgen von Befehlen bestimmen, was wann wie und womit berechnet werden soll, gibt man bei der funktionalen Programmierung an, was etwas ist, also wie sich die Lösung zusammensetzt. Programme in (rein) funktionalen Programmiersprachen bestehen daher ausschließlich aus Mengen von Funktionen, die sich wie mathematische Funktionen verhalten: Es ist garantiert, dass eine Funktion bei gleicher Eingabe stets die gleiche Ausgabe liefert, ohne dass es Nebeneffekte gibt, was bei imperativen Programmiersprachen durchaus möglich und üblich ist. Diese Tatsache nennt man referenzielle Transparenz - sie begünstigt effektives Debuggen und vereinfacht Programmbeweise erheblich. Erlaubt eine funktionale Programmiersprache auch Seiteneffekte, nennt man sie impure (engl. für unrein).

Ein weiterer wesentlicher Unterschied zu imperativen Programmierung ist, dass die funktionalen Programmierung völlig ohne Schleifen auskommt und stattdessen Rekursion benutzt.

Bekannte Beispiele für funktionale Programmiersprachen sind u.A. Haskell (welche im Rahmen dieser Arbeit verwendet wird), die ML- und Lisp-Sprachfamilien, F# und Erlang.

Man kann zeigen, dass funktionale Programmiersprachen genauso mächtig sind wie imperative Programmiersprachen.

Ihre Ursprünge hat die funktionale Programmierung in der Wissenschaft - sie basiert auf dem in den 1930er Jahren von Alonzo Church und Stephen Kleene eingeführten Lambda-Kalkül, welches formal die Definition von Funktionen und ihren Parametern beschreibt.

Insbesondere ist es daher in funktionalen Programmiersprachen möglich, dass Funktionen wiederum Funktionen als Parameter und/oder als Rückgabewert besitzen - man spricht in diesem Fall von higher-order Funktionen. Ein bekanntes Beispiel hierfür ist die Funktion map, welche als Parameter eine Funktion und eine Liste erhält und die Funktion auf sämtliche Elemente dieser Liste anwendet.

Es wird zwischen strikten und nicht-strikten funktionalen Programmiersprachen unterschieden: Erstere werten Funktionsargumente vor der Funktionsanwendung vollständig aus (call-by-value) und verhalten sich daher in gewisser Weise imperativ, während bei

(18)

letzteren die Ausdrücke unausgewertet in Funktionen eingesetzt werden (call-by-name). Eine alternative nicht-strikte Auswertungsvariante wertet Ausdrücke nur dann aus, wenn ihr Ergebnis bei der Programmausführung zwingend benötigt wird (call-by-need).

Funktionale Programmiersprachen können sowohl statisch als auch dynamisch typisiert sein:

Hat jede Variable bereits zur Compilerzeit einen festen Typ zugewiesen bekommen, spricht man von statischer Typisierung, während bei dynamischer Typisierung der Typ einer Variable erst zur Ausführungszeit überprüft wird. Statische Typisierung hat den Vorteil, dass Typfehler frühzeitig erkannt werden und dadurch typbedingte Laufzeitfehler ausgeschlossen sind.

Außerdem unterscheidet man zwischen starker und schwacher Typisierung: Stark typisierte Sprachen erlaubt Typkonvertierungen auch explizit nur unter mehr oder weniger strengen Bedingungen, während man bei schwach typisierten Sprachen ohne größere Auflagen Typen auch implizit konvertieren kann. Stark typisierte Sprachen sind im Allgemeinen weniger fehleranfällig.

2.3.2 Haskell

Haskell ist eine rein funktionale, nicht-strikte Programmiersprache, die 1990 eingeführt wurde, um für wissenschaftliche Zwecke eine moderne und standardisierte funktionale Programmiersprache zur Verfügung zu haben.

Sie verfügt über ein starkes, statisches Typsystem, welches auch polymorphe Typen erlaubt, d.h. Funktionsparameter und Funktionsausgaben sind nicht zwangsläufig im Voraus auf einen bestimmten Typen festgelegt – innerhalb der Typdeklaration einer Funktion (deren Angabe explizit durch den Programmierer, aber auch implizit automatisch erfolgen kann) können demnach Typvariablen vorkommen. Dabei besteht die Möglichkeit, die Typvariablen auf eine bestimmte Menge von Typen zu beschränken, die gemeinsame Eigenschaften besitzen und daher Instanz einer bestimmten Typklasse sind. Beispiele für Typklassen sind etwa Num, welche Zahlentypen zusammenfasst, oder Eq, welche Typen enthält, die man auf Gleichheit testen kann. Mathematische Operatoren sind bspw. auf Typen der Typklasse Num beschränkt.

Wird eine allgemein gehaltene Funktion auf einen Parameter mit bestimmtem Typ angewandt, werden die Typen automatisch abgeglichen – dies nennt man Typinferenz.

Ein weiteres wesentliches Konzept in Haskells Typsystem sind zusammengesetzte Typen.

Das prominenteste Beispiel hierfür ist der (polymorph definierte) Listentyp [a]. Listen können demnach Werte beliebigen Typs beinhalten (allerdings nicht verschiedene Datentypen innerhalb der gleichen Liste!). Listen sind ein elementarer Bestandteil von Haskell und kommen in praktisch jedem Programm vor.

Haskell arbeitet mit der nicht-strikten Bedarfsauswertung (call-by-need), welche die Vorteile von call-by-value- und call-by-name-Auswertungsstrategien vereinigt. So terminiert bspw. die Funktion first x y = x auch dann, wenn für y ein nicht-terminierender Ausdruck eingesetzt wird – y wird gar nicht erst ausgewertet, da es im Funktionsrumpf nicht vorkommt.

Gleichzeitig wird die mehrfache Auswertung ein und desselben Ausdrucks vermieden – kommt ein Funktionsargument im Rumpf der Funktion mehrfach vor, wertet Haskell den Argumentausdruck nur ein einziges Mal aus und setzt bei allen anderen Vorkommen des Arguments intern einen Zeiger auf den ausgewerteten Ausdruck (sog. Sharing von Unterausdrücken). So wird bei der Funktion quadrat x = x * x ein für x eingesetzter Ausdruck nur einmal ausgewertet, obwohl er zunächst unausgewertet im Funktionsrumpf eingesetzt wird und somit doppelt vorhanden ist.

(19)

Da es in funktionalen Programmiersprachen wie bereits erwähnt keinerlei Schleifen gibt, arbeitet man mit Rekursion. Beispielsweise gilt für die Fakultätsfunktion: Die Fakultät einer Zahl ist gleich mal der Fakultät von ( ). Damit die Funktion terminiert, ist eine Abbruchbedingung notwendig, diese wäre in diesem Fall: Die Fakultät von ist .

In Haskell könnte man diese Funktion wie folgt definieren:

fak :: (Integral a) => a → a fak 1 = 1

fak n = n * (fak (n-1))

Die Typdeklaration der Funktion ist polymorph definiert, wobei die Typvariable a auf ganzzahlige Datentypen beschränkt ist. Im obigen Beispiel wird auch deutlich, dass Haskell Pattern-Matching, also Musterabgleiche von Funktionsargumenten unterstützt. Dabei werden die Funktionsdefinitionen von oben nach unten durchgegangen - sobald die Funktionsargumente erstmals auf das definierte Muster einer Funktionsdefinition passen, wird genau diese ausgeführt. Gerade bei der Benutzung von Listen wird häufig Pattern- Matching verwendet. Eine rekursive Implementierung der length-Funktion (welche die Länge einer Liste berechnet) mit Pattern-Matching könnte z.B. so aussehen (Der Doppelpunkt – cons genannt – ist dabei ein Operator, der einen Wert an eine Liste anhängt).

length :: (Num b) => [a] → b length [] = 0

length (x : xs) = 1 + length xs

Das Muster der ersten Definition von length wird nur von leeren Listen erfüllt, während (x : xs) auf alle Listen passt, die mindestens ein Element enthalten.

Mittels des data-Schlüsselwortes lassen sich in Haskell eigene algebraische Datentypen definieren. Ein Datentyp für einen binären Baum, der an seinen Blättern Int-Werte speichert, könnte bspw. wie folgt aussehen:

data Tree = Node Tree Tree | Leaf Int

Wie wir sehen hat der Datentyp Tree zwei Konstruktoren: Er besteht entweder aus einem inneren Knoten (Node), an dem wiederum zwei Bäume hängen (Node ist also ein rekursiver Konstruktor), oder einem Blatt, welches einen Integer hält.

Auch bei benutzerdefinierten Datentypen ist Polymorphie erlaubt. Wenn die Blätter des eben definierten Baums anstatt Integer-Werten Werte beliebigen Typs halten dürfen, könnte man Tree z.B. so definieren:

data Tree a = Node Tree Tree | Leaf a

Mit dem Schlüsselwort type lassen sich in Haskell Typsynonyme definieren, z.B. ist String lediglich ein Typsynonym für eine Liste von Werten des Typ Char:

type String = [Char]

Typsynonyme verwendet man in erster Linie, um Typdeklarationen aussagekräftiger zu gestalten.

(20)

Zur Modularisierung und Kapselung des Codes gibt es in Haskell ein Modulsystem. Viele elementare Module sind in jeder Haskell-Installation enthalten, z.B. die Module Data.List mit diversen Listenoperationen und Data.Map, welches die Wörterbuch-Datenstruktur mit zugehörigen Funktionen zur Verfügung stellt.

Um (unreine) Ein- und Ausgabeoperationen zu ermöglichen, werden in Haskell Monaden verwendet. Dabei wird durch bestimmte Sprachkonstrukte gewährleistet, dass der pure Programmteil strikt von jeglichen IO-Operationen separiert ist.

(21)

3 Multi-terminal decision diagrams with addition zur komprimierten Darstellung von Matrizen

In diesem Kapitel definieren wir zunächst die als MTTD+ bezeichnete komprimierte Darstellung von Matrizen und stellen anschließend eine Reihe von Algorithmen vor, die grundlegende Matrixoperationen auf MTTD+s ermöglicht. Die Ausführungen in diesem Kapitel orientieren sich weitestgehend an [Lo13].

3.1 MTDD

+

s

Zur komprimierten Darstellung von Matrizen verwenden wir sogenannte multi-terminal decision diagrams with addition (MTTD+), welche im Wesentlichen als eine zweidimensionale Abwandlung kontextfreier Grammatiken mit speziellen Produktionsregeln betrachten werden können.

Ähnlich einer kontextfreien Grammatik, die aus Mengen von Nichtterminalen, Terminalen, Produktionsregeln sowie einem Startsymbol besteht, ist auch ein MTTD+ aufgebaut: es besteht ebenfalls aus einer Menge von Nichtterminalsymbolen (kurz Nichtterminalen), einer Menge von Produktionsregeln (kurz Produktionen) und einem Startsymbol – die Terminalsymbole (kurz Terminale) hingegen sind Elemente der ganzen Zahlen und werden daher in der Definition eines MTTD+ nicht explizit aufgeführt. Insgesamt wird ein MTTD+ also durch ein Tripel ( ) vollständig definiert. Die eigentliche Matrix erhält man angefangen mit dem Startsymbol durch sukzessives Anwenden der Produktionsregeln. Beim Anwenden dieser Regeln gibt es stets nur eine einzige Möglichkeit, ein Nichtterminal zu substituieren. Demnach ist jeder MTTD+ bezüglich der von ihm repräsentierten Matrix eindeutig. Die von G erzeugte Matrix bezeichnen wir als ( ) und das Element in Zeile und Spalte von ( ) als ( ) .

Ein MTTD+ repräsentiert stets eine quadratische Matrix, deren Seitenlänge eine Potenz von Zwei ist – andersartige Matrizen müssen also z.B. mit Nullen aufgefüllt werden, um den Anforderungen einer Komprimierung mittels MTTD+ gerecht zu werden.

Die von einem Nichtterminal erzeugte Matrix nennen wir ( ). Es gilt demnach ( ) ( ), d.h. die von erzeugte Matrix entspricht der Matrix, die beginnend mit ihrem Startsymbol erzeugt wird.

Als Höhe eines MTTD+ bezeichnet man den Exponenten der Zweierpotenz, welche die Seitenlänge der unkomprimierten Matrix angibt. Wir sagen ein Nichtterminal hat die Höhe , wenn ( ) quadratische Matrix der Seitenlänge erzeugt.

Die Menge der Nichtterminale ist bei einem MTTD+ unterteilt in Teilmengen ( ).

In der Teilmenge befinden sich dabei genau diejenigen Nichtterminale, die die Höhe haben. Insbesondere gilt . Für jedes Nichtterminal existiert genau eine Produktion , auf deren linker Seite vorkommt. Wir sagen dann, dass mit korrespondiert.

Man unterscheidet zwischen drei verschiedenen Arten von zulässigen Produktionen:

 (

) ( )

Diese Produktionsart bezeichnen wir im Folgenden als DownStep-Produktion.

(22)

 ( )

Diese Produktionsart bezeichnen wir im Folgenden als Additionsproduktion.

 ( )

Diese Produktionsart bezeichnen wir im Folgenden als Terminalproduktion.

DownStep-Produktionen erzeugen aus einem Nichtterminalsymbol ein -Zahlenfeld, wiederum bestehend aus Nichtterminalen. Diese Art der Produktion wird folgerichtig zum sukzessiven Aufspannen der Matrix verwendet.

Eine Additionsproduktion erzeugt aus einem Nichtterminalsymbol eine Addition zwei weiterer Nichtterminale. Sie führt letztendlich zur komponentenweisen Addition der Matrizen erzeugt durch die beiden Summanden. Die Summenmatrix hat demnach die gleiche Höhe wie die Matrizen, die von den beiden Summanden erzeugt werden.

Terminalproduktionen können offensichtlich nur die Höhe haben – sie erzeugen aus einem Nichtterminal ein Terminalsymbol, welches Element der ganzen Zahlen sein muss.

Zur Vermeidung von unendlich großen Matrizen muss die Grammatik azyklisch sein, d.h. bei der Entwicklung einer Matrix aus einem Nichtterminal darf ebendieses im Entwicklungsverlauf nicht wieder vorkommen.

Ein MTTD+ , welches über keinerlei Additionsproduktionen verfügt, nennt man einfach MTDD. Kommen in hingegen keinerlei DownStep-Produktionen vor, entspricht einem mathematischem Additionsterm, der bei Auswertung zu einem Skalar führt – man spricht dann von einem +-Circuit.

Die Größe eines MTTD+ definieren wir abweichend zu [Lo13] der Einfachheit halber als , also der Summe aller Produktionsregeln von . Für eine genauere Analyse des Speicherplatzbedarfs ist es notwendig, die speicherinterne Darstellung sowohl der Nichtterminale als auch der Terminale zu berücksichtigen.

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } { } { }

 { (

) (

) (

) } Es gilt ( ) (

).

MTTD+s können in günstigen Fällen enorme Kompressionsraten erzielen: So lässt sich im Optimalfall eine Matrix mit Seitenlänge (welche folglich Elemente beinhaltet) durch einen MTTD+ der Größe ( ) repräsentieren (eine Produktion für jede Höhenstufe).

Allgemein kann man sagen, dass die Kompressionsrate durch häufiges Auftreten gleichartiger oder ähnlicher Muster in der Matrix begünstigt wird.

Falls die Muster innerhalb der Matrix jedoch ungünstig positioniert sind, behindert dies eine allzu effektive Kompression. Man betrachte folgende Grafik:

(23)

Die blauen und roten Viererblöcke stellen jeweils gleichartige Muster dar, die an verschiedenen Stellen auftreten. Die schwarzen Punkte sind beliebige Werte. Weil sich die blauen Blöcke innerhalb des Rasters befinden, können alle von ihnen durch die gleiche Produktion repräsentiert werden. Die roten Blöcke hingegen liegen auf den Rastergrenzen und können daher nicht effektiv zusammengefasst werden, obwohl es sich auch bei ihnen um ein gleichartiges Muster handelt.

Matrizen, in denen wenig bis keine sich wiederholenden Muster auftreten, lassen sich hingegen nur wenig bis gar nicht komprimieren – es kann sogar passieren, dass eine als MTTD+ repräsentierte Matrix einen größeren Speicherplatzbedarf aufweist als die unkomprimierte Matrix. Im absoluten worst-case-Szenario sind in einer Matrix nur unterschiedliche Elemente enthalten und auch mittels Additionsproduktionen lässt sich keine Platzeinsparung erreichen. Dann hat ein MTTD+, der eine Matrix mit Seitenlänge repräsentiert, die Größe ∑ ( ). Dies entspricht in etwa einer Vervierfachung der Größe der unkomprimierten Matrix.

(24)

3.2 Matrixoperationen auf MTTD

+

s

Nachdem wir MTTD+s als komprimierte Darstellung von Matrizen definiert haben, werfen wir nun einen Blick auf eine Reihe von Algorithmen, die grundlegende Matrixoperationen auf MTTD+s ermöglichen. Es ist zu bemerken, dass diese Algorithmen keine Dekomprimierung der Matrizen erfordern.

3.2.1 Skalarmultiplikation

Dieser triviale Algorithmus erzeugt bei Eingabe eines Skalars und eines MTDD+ einen MTDD+ , sodass gilt ( ) ( ). ( ) ist somit das Skalarprodukt aus und ( ).

Um zu erzeugen, müssen wir lediglich die Produktionsmenge von geringfügig verändern: Jede Produktionsregel der Form (also Produktionen, die auf Terminale abbilden – diese kommen nur in Höhe 0 vor) ersetzen wir durch eine Regel . Dies führt offensichtlich zur Berechnung des oben beschriebenen Skalarprodukts. An Produktionen der Form ( ) ist keine Änderung notwendig, da sie nur Einfluss auf die Positionierung der Terminalsymbole innerhalb der Matrix, nicht aber auf die Wertigkeit der Terminale haben. Ebenso müssen Additionsproduktionen, also Regeln der Form , nicht modifiziert werden – es gilt natürlich ( ) ( ) ( ) und dadurch auch für ein beliebige Terminalsymbole innerhalb von ( ) . Wegen des Distributivgesetzes gilt außerdem ( ) - somit reicht es auch in diesem Fall aus, nur die Terminalproduktionen zu ändern.

Die Berechnung von lässt sich in Zeit ( ) durchführen. Wir müssen sie für jede Terminalproduktion, also Regel der Form durchführen und die Anzahl solcher Regeln ist durch beschränkt, was zu einer Laufzeit von ( ) für diesen Algorithmus führt.

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } (vereinfacht)

 { ( ) }

Es gilt ( ) ( ). Wir multiplizieren mit dem Skalar . Anwenden des Algorithmus ergibt einen MTTD+ ( ) mit

 { }

 { ( ) } Wir erhalten ( ) (

) ( )

(25)

3.2.2 Transposition

Bei Eingabe eines MTDD+ berechnet dieser ebenfalls sehr simple Algorithmus einen MTDD+ , wobei ( ) der Transposition der Matrix ( ) entspricht. Da nur Produktionen der Form ( ) die Positionierung der Elemente innerhalb der Matrix bestimmen, müssen auch nur solche Regeln modifiziert werden. Es genügt, bei jeder solcher Regel Zeilen und Spalten zu vertauschen, d.h. jede Regel dieser Form wird umgewandelt zu ( ).

Dies ist korrekt, denn offensichtlich gilt

( ) ( ( ) ( )

( ) ( )) ( ( ) ( ) ( ) ( ) ).

Da es maximal |G| Regeln der obigen Form gibt (genau genommen weniger, denn es muss mindestens eine Regel geben, die auf ein Terminal abbildet), ist die Laufzeit durch O(|G|) beschränkt.

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } (vereinfacht)

 { ( ) ( ) }

Es gilt ( ) ( ).

Anwenden des Algorithmus ergibt einen MTTD+ ( ) mit

 { }

 { ( ) ( ) } Wir erhalten ( ) ( ) ( )

3.2.3 Summe aller Matrixelemente

Auch zur Berechnung der Summe aller Matrixelemente ist nur die Modifikation von Regeln der Form ( ) notwendig. Da die Ausgabe dieses Algorithmus kein zweidimensionales Zahlenfeld, sondern ein Skalar sein soll und da die Summe aller Elemente einer Matrix A gleich der Summe der Summen ihrer Teilmatrizen A1 bis A4 ist, liegt es nahe, diese Regeln durch Regeln der Form zu ersetzen.

Additionsproduktionen erfüllen offensichtlich auch für diesen Algorithmus ihren Zweck, ohne eine Änderung erfahren zu müssen.

Durch diese Umformungen erhalten wir einen +-Circuit. Um letztendlich die gewünschte Summe zu berechnen, fängt man wie gewohnt mit der Startproduktion an und entwickelt

(26)

iterativ alle Nichtterminale, bis schließlich alle Nichtterminale zu Terminalen ausgewertet wurden. Die Auswertung des so entstehenden Additionsterms liefert das Ergebnis.

Wir nehmen vereinfachend an, dass eine einzelne Addition in konstanter Zeit durchführbar ist und dass zu mehrfach vorkommenden Nichtterminalen gehörende Summen nur einmal ausgewertet wird. Wir modifizieren maximal |G| Regeln und führen maximal ebenso viele Additionen durch - somit ergibt sich eine Laufzeit der Größenordnung O(|G|).

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } (vereinfacht)

 { (

) }

Es gilt ( ) ( ).

Anwenden des Algorithmus ergibt einen +-Circuit ( ) mit

 { }

 { } Wir erhalten ( )

3.2.4 Spur einer Matrix

Der Algorithmus zur Berechnung der Spur einer Matrix ist dem zur Berechnung der Summe aller Matrixelemente ähnlich – da uns hier allerdings nur die Summe der Elemente auf der Hauptdiagonalen interessiert, werden Regeln der Form ( ) umgewandelt zu . Andersartige Produktionen müssen aus demselben Grund wie beim Algorithmus zur Summe aller Matrixelemente nicht modifiziert werden und auch die Berechnung des Ergebnisses funktioniert auf die gleiche Art und Weise.

Ein nennenswerter Unterschied ist allerdings, dass zur Berechnung der Spur eigentlich nicht alle Produktionen benötigt werden – da wir aber im Voraus keine Informationen darüber haben, welche Produktionen einen Wert erzeugen, der Hauptdiagonalen liegt, und welche nicht, müssen wir wohl oder übel alle Produktionen der oben genannten Form umwandeln.

Bei Anwendung ähnlicher Argumentation wie in 1.2.3 ergibt sich für diesen Algorithmus ebenfalls eine lineare Laufzeit bzgl. der Größe von G.

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } (vereinfacht)

 { (

) } Es gilt ( ) (

).

(27)

Anwenden des Algorithmus ergibt einen +-Circuit ( ) mit

 { }

 { } Wir erhalten ( )

3.2.5 Matrixelement berechnen

Dieser Algorithmus liest aus einer als MTTD+ komprimierten Matrix ( ) den Eintrag in Zeile und Spalte aus (wir fangen bei der Nummerierung der Zeilen und Spalten mit Null an).

Um dies zu bewerkstelligen, wird in einen +-Circuit mit der gleichen Menge an Nichtterminalen und dem gleichen Startsymbol umgewandelt, sodass ( ) ( ) gilt.

Auswerten von führt schlussendlich zu dem gesuchten Wert.

Wir müssen im Wesentlichen die Produktionsmenge von derart umwandeln, dass sie mit einem +-Circuit kompatibel ist und gleichzeitig dafür sorgen, dass die Auswertung der umgewandelten Produktionsregeln den gesuchten Wert ergibt.

Da es sich nicht um eine Matrix, sondern einen einzelnen Wert handelt, den wir bestimmen wollen, liegt es auf der Hand, dass DownStep-Produktionen, also Produktionen der Form (

), grundlegend in ihrer Struktur modifiziert werden müssen.

Die Grundidee ist, diejenigen DownStep-Produktionen, die auf dem Weg vom Startsymbol zum gesuchten Matrixelement durchlaufen werden, derart zu modifizieren, dass sie jeweils nur das Nichtterminalsymbol erzeugen, welches die Teilmatrix repräsentiert, in der das gesuchte Matrixelement liegt (dabei benutzen wir eine Art Hilfsproduktion, die lediglich von einem Nichtterminal auf ein weiteres Nichtterminal führt).

Wir müssen also für jede dieser DownStep-Produktionen bestimmen, in welcher der vier erzeugten Teilmatrizen ( ), ( ), ( ), und ( ) sich das gesuchte Element befindet - dies ist jeweils abhängig von der Höhenstufe, in der sich die Produktion befindet. Das bedeutet, dass wir für jede Höhenstufe individuell ausrechnen müssen, um welche Teilmatrix es sich jeweils handelt.

Es ist noch anzumerken, dass wir im Voraus nicht wissen, welche DownStep-Produktionen bei der Erzeugung des Elements durchlaufen werden und welche nicht, sodass wir der Einfachheit halber direkt alle (entsprechend ihrer Höhenstufe) modifizieren. Dies ist ohnehin notwendig, da ein +-Circuit keinerlei DownStep-Produktionen besitzen darf.

Konkret berechnen wir für jede Höhenstufe die Indizes und desjenigen Nichtterminals, welches die gesuchte Teilmatrix erzeugt (z.B. deuten und auf das Nichtterminal ) und modifizieren dementsprechend die DownStep-Produktionen.

Dafür benutzen wir Zahlen und , die jeweils für den Zeilen- und Spaltenindex des gesuchten Elements in der aktuell bearbeiteten Teilmatrix stehen – diese Werte sind wie wir sehen werden veränderlich. Wir fangen bei Höhenstufe an und arbeiten uns Schritt für Schritt bis zu Höhenstufe durch. Klar: Anfangs gilt und .

Wir wissen: Jede Produktion der Höhenstufe erzeugt eine Matrix der Seitenlänge , deren Zeilen und Spalten von bis durchnummeriert sind. Man stelle sich diese Matrix in vier gleichgroße Teile aufgeteilt vor – es gilt nun anhand der Indizes und herauszufinden, in welcher Teilmatrix sich das gesuchte Element befindet: Falls bzw.

(28)

, dann befindet sich das gesuchte Element in der oberen bzw. linken Hälfte der Matrix – andernfalls ist es in der unteren bzw. rechten Hälfte zu verorten. Formal bedeutet dies:

{

{

Nun müssen wir noch die Indizes des gesuchten Elements für die nächsttiefere Höhenstufe anpassen – diese sind relativ zur Teilmatrix, auf die sie sich beziehen:

Auf diese Weise lassen sich sukzessive für jede Höhenstufe (bis Höhenstufe 1) die Indizes der gesuchten Teilmatrix bestimmen. Als letzten Schritt wandeln wir nun schlussendlich die DownStep-Produktionen um:

Eine Regel der Form (

), wobei Höhe besitzt, wird zu .

Eine Modifikation der Additions- sowie der Terminalproduktionsproduktionen ist nicht erforderlich, da sie bereits so wie sie sind unserem Zweck genügen.

Die Argumentation zur Laufzeit ist abermals ähnlich zu der in 1.2.3. – auch hier haben wir lineare Laufzeit, d.h. ( ).

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } (vereinfacht)

 { ( ) ( ) ( ) ( ) ( ) }

Es gilt ( )

(

)

.

Wir wollen unter Benutzung des Algorithmus den Wert in Zeile 4 und Spalte 6 bestimmen (weiß eingefärbt).

Es gilt und . Es ergibt sich sowie , da sowohl 4 als auch 6 größer gleich sind. Demnach müssen wir in der unteren rechten Teilmatrix

weitersuchen (hellgrau unterlegt). Durch Anwenden der oben genannten Formel bekommen wir und . Daraus ergibt sich und , was wiederum der oberen rechten Teilmatrix entspricht (dunkelgrau unterlegt). Wir erhalten und – diesmal ist das gesuchte Element in der oberen linken Teilmatrix zu verorten. Nun sind wir fertig und modifizieren die DownStep-Produktionen entsprechend.

(29)

Es ergibt sich ein +-Circuit ( ) mit

 { } (vereinfacht)

 { } Auswerten liefert ( )

3.2.6 Matrixmultiplikation

Ziel ist es, aus zwei MTTD+s ( ) und ( ) einen MTTD+ zu konstruieren, der das Matrixprodukt der beiden Matrizen ( ) und ( ) repräsentiert.

Dazu erzeugen wir für jedes Nichtterminal und jedes Nichtterminal (wobei und die gleiche Höhe haben) ein Nichtterminal ( ) für , sodass (( )) ( ) ( ) gilt – das bedeutet insbesondere ( ) (( )) ( ) ( ) ( ) ( ), womit klar ist, dass der Algorithmus zum gewünschten Ergebnis führt.

Konkret sind vier Fälle zu berücksichtigen:

1. und haben beide die Höhe und ihre zugehörigen Produktionen sind Terminalproduktionen, d.h. und , wobei . Dann gilt trivialerweise ( ) , wobei .

2. Die Regel für ist eine Additionsregel, hat also die Form . Dann gilt ( ) ( ) ( ). Dies ist korrekt, weil auch bei Matrixmultiplikationen das Distributivgesetz gilt: (( )) ( ) ( ) ( ( ) ( )) ( ) ( ) ( ) ( ) ( ) (( )) (( )).

3. Die Regel für ist eine Additionsregel, hat also die Form . Dann gilt ( ) ( ) ( ). Die Argumentation ist vollständig analog zu Fall 2.

4. Die Regeln für A und B sind beides DownStep-Produktionen: (

) und B → (

). Hier ist es lediglich erforderlich, die Regeln der Matrixmultiplikation („Zeile mal Spalte“) zu beachten. Demnach muss gelten:

( ) (

)

Da wir die Produktion so natürlich nicht speichern können, müssen wir für jeden eine Teilmatrix repräsentierenden Term eine eigene Produktion einführen. Letztendlich ergibt sich ( ) (

), wobei ( ) ( ) für { }

Die Produktionsmenge von besteht somit aus allen auf diese Weise gebildeten Produktionen. Aus ebendieser lässt sich natürlich auch die Menge der Nichtterminale herleiten, indem man alle linken Seiten der Produktionen zusammenfasst. Das Startsymbol von ist (( )).

(30)

Die Anzahl der zu erstellenden Regeln ist durch ( ) beschränkt, woraus sich die Laufzeit dieses Algorithmus sowie die Größe des resultierenden MTTD+ ergibt.

Wir verzichten auf dieser Stelle an ein Beispiel, da, falls man im Beispiel alle möglichen Fälle behandeln will, bereits die Multiplikation zweier -Matrizen zu einer derart großen Zahl an Produktionsregeln führt, dass es unmöglich ist, das Beispiel übersichtlich zu gestalten.

3.2.7 Gleichheitstest

MTTD+s bieten durchaus die Möglichkeit, zwei identische Matrizen auf unterschiedliche Art und Weise zu komprimieren. Dieser Algorithmus hat den Zweck, zwei mittels MTTD+

komprimierte Matrizen einem Gleichheitstest zu unterziehen und je nach Ergebnis ‚wahr‘

oder ‚falsch‘ auszugeben.

Um zwei Matrizen ( ) und ( ) auf Gleichheit zu testen (wobei und Nichtterminale gleicher Höhe sind und zur Menge einem MTTD+ gehören), baut der Algorithmus im Wesentlichen ein lineares Gleichheitssystem auf, welches im Berechnungsverlauf (bei dem die Grammatik top-down durchlaufen wird) immer wieder erweitert, modifiziert, reduziert und schlussendlich durch Einsetzen von bestimmten Werten für die unbekannten Variablen auf Korrektheit getestet wird.

Genauer speichert der Algorithmus auf jeder Höhenstufe ein LGS von bis zu Gleichungen (wobei durch die Anzahl der Unbekannten im LGS beschränkt ist) der Form

, wobei die Variablen bis Nichtterminalen der Höhe entsprechen.

Das anfängliche Gleichungssystem der Höhe besteht aus einer einzigen Gleichung (was freilich äquivalent zu ist). und sind hier als Variablen zu verstehen – haben diese Variablen den „richtigen Wert“, d.h. führen und ultimativ auf identische Matrizen, geht die Gleichung auf. Mit dieser Initialen Gleichung als Grundlage schlussfolgern wir anhand der Produktionen von G weitere Gleichungen und bauen dadurch herabsteigend von Höhenstufe für jede Höhenstufe ein Gleichungssystem auf. Auf Höhe Null eingekommen setzen wir für alle Nichtterminale, welche nun nur noch mit Terminalproduktionen korrespondieren können, die entsprechenden Terminale ein und überprüfen, ob alle Gleichungen aufgehen.

Um das Gleichungssystem für die nächstniedrigere Stufe zu bestimmen, führen wir nacheinander folgende Schritte durch (aktuelle Höhenstufe ist – Ziel ist es, das Gleichungssystem der Höhenstufe zu berechnen):

1. Standardisieren sämtlicher Gleichungen: Alle Gleichungen sind in die Standardform

zu überführen. Dies ist notwendig, da die Gleichungen nach Durchführung von Schritt 3 bzw. 4 nicht mehr der Standardform genügen – diese ist allerdings für Schritt 2 erforderlich, weswegen alle Gleichungen wieder in ein einheitliches Format überführt werden müssen.

2. Entfernen redundanter Gleichungen: Die maximale Anzahl nicht-redundanter Gleichungen ist wie in Kapitel 1.2.3 beschrieben durch die Anzahl der in den Gleichungen vorkommenden Variablen beschränkt. Da allerdings durch Schritt 3 und 4 die Zahl der Gleichungen die Zahl der vorkommenden Variablen übersteigen kann, ist es gerade bei sehr großen Matrizen unerlässlich, regelmäßig die redundanten Gleichungen zu entfernen. Dies tun wir mithilfe des Gauß-Jordan-Algorithmus.

3. Mit Additionsproduktionen korrespondierende Nichtterminale substituieren: Für jedes im Gleichungssystem vorkommende Nichtterminal der Höhe , welches mit einer

(31)

Regel der Form korrespondiert: Ersetze jedes Vorkommen der Variable im LGS durch den Term . Dies ist solange zu wiederholen, bis kein im Gleichungssystem vorkommendes Nichtterminal der Höhe mehr mit einer Additionsproduktion korrespondiert. Da ( ) ( ) ( ) gilt, ändern diese Substitutionen nicht die Aussage der Gleichungen, führen uns aber offensichtlich unserem Ziel näher, die Höhe der in den Gleichungen vorkommenden Nichtterminale zu verringern.

4. Mit DownStep-Produktionen korrespondierende Nichtterminale substituieren: An dieser Stelle korrespondieren ausschließlich Produktionen der Form (

) mit den in den Gleichungen vorkommenden Nichtterminalen der Höhe . Ersetze jedes dieser Nichtterminale durch die rechte Seite seiner korrespondierenden Produktion – wir erhalten dadurch Gleichungen, deren Variablen ausschließlich aus -Matrizen bestehen, welche ihrerseits Nichtterminale der Höhe enthalten.

Schlussendlich zerlegen wir jede Gleichung in vier Gleichungen – eine Gleichung nur mit denjenigen Elementen, welche in den -Matrizen oben links vorkommen, eine mit den Elementen oben rechts, eine mit den Elementen unten links und eine mit den Elementen unten rechts. Auch hier bleiben die Aussagen der ursprünglichen Gleichungen erhalten, sie werden lediglich jeweils in vier Teilgleichungen zerlegt.

Auf Höhenstufe Null angekommen führen wir erneut die Schritte 1-3 durch und setzen anschließend für alle Nichtterminale diejenigen Terminale ein, die ihre korrespondierenden Terminalproduktionen erzeugen, und überprüfen, ob alle Gleichungen aufgehen. Ist dies der Fall, dann ist auch die initiale Gleichung erfüllt, d.h. es gilt ( ) ( ), andernfalls gilt ( ) ( ).

Der dominierende Faktor der Laufzeit dieses Algorithmus ist der zum Entfernen redundanter Gleichungen verwendete Gauß-Jordan-Algorithmus, welcher kubische Laufzeit in der Anzahl der Anzahl der Variablen im LGS besitzt. Diese ist durch die Anzahl der Nichtterminale von beschränkt, welche identisch mit der Anzahl der Produktionen von ist. Wir führen den Gauß-Jordan-Algorithmus maximal einmal je Höhenstufe aus - die Anzahl der Höhenstufen ist ebenfalls durch die Anzahl der Produktionen beschränkt, denn es kann nicht mehr Höhenstufen als Produktionen geben. Daraus ergibt sich die (allerdings sehr grobe) Laufzeitschranke ( ).

Beispiel

Wir betrachten den MTTD+ ( ) mit

 { } { }

 ist irrelevant

 { ( ) ( ) } Es gilt ( ) ( ) (

). Diesen Sachverhalt verifizieren wir unter Benutzung des Algorithmus:

Die erste Gleichung lautet . Diese Gleichung ist standardisiert, Schritt 1 entfällt also. In Schritt 2 stellen wir fest, dass diese Gleichung nicht redundant ist. Da mit einer Additionsproduktion korrespondiert, ersetzen wir in Schritt 3 durch seine Summanden. Wir bekommen die Gleichung ( ) Es existieren nun nur noch Nichtterminale, die mit DownStep-Produktionen korrespondieren.

(32)

Wir substituieren diese zunächst - ( ) (( ) ( )) – und teilen die Gleichung nun nach dem beschriebenen Muster in vier Teilgleichungen auf (Schritt 4):

( ) ( ) ( ) ( )

Wir haben nun erreicht, dass alle Nichtterminale im Vergleich zur Eingabegleichung um eine Höhenstufe reduziert sind - insbesondere sind wir auf Höhenstufe 0 angekommen. Zunächst werden alle Gleichungen standardisiert:

In Schritt 2 wird festgestellt, dass nur die ersten beiden Gleichungen essentiell sind, die anderen beiden sind redundant (dies ist offensichtlich, da sie identisch zu den ersten beiden sind).

Referenzen

ÄHNLICHE DOKUMENTE

Schiebt man die Blöcke von A in jeder Zeile von A zyklisch um eine Position nach links und die von B in jeder Spalte nach oben, so erhält jeder wieder zwei passende Blöcke.

Schiebt man die Blöcke von A in jeder Zeile von A zyklisch um eine Position nach links und die von B in jeder Spalte nach oben, so erhält jeder wieder zwei passende Blöcke.

Trotz der Geburt in einem anderen Land ähnelt das Muster der Bildungsferne (Idealtyp 1 und 1a) dem von Herrn Krause, der ebenfalls als angestellter Landarbeiter aufwächst, von

4.) Die Luftverschmutzung ist hoch und der Lärm der Umgebung groß. 5.) Das führt zu Spannungen in unserer Familie. 6.) Wir hoffen, die Lösung durch eine Anzeige in der

Die Anzahl der Zeilen und Spalten wird auch als Dimension einer Matrix

Allgemein ist zu sagen, dass man alle Pfade des XSTree1 a elementweise (da ja der Pfad aus einer Liste besteht) besucht, dabei die Repeats (die verglichen werden sollen)

Zusammenfassend kann erkannt werden, dass eine Transformation, beziehungsweise auch eine Standardreduktion, durch folgenden Typ repräsentiert

Die Eingabe für den Algorithmus von Plandowski besteht aus zwei SLPs in Beispiel 3.13 sehen wir, dass wir in Wirklichkeit nur einen SLP eingeben und zwei Nichtterminale..