Kapitel 6 Algorithmen auf Arrays Wir behandeln jetzt drei Aufgabenstellungen mit zunehmendem Schwierigkeitsgrad, deren Lösung zu Algorithmen auf Arrays führt: • Suchen eines Wertes in einem Array, • Lösung linearer Gleichungssysteme, • Berechnung kürzester Wege in Graphen. 6.1 Suchen einer Komponente vorgegebenen Wertes Hier handelt es sich um folgendes Basisproblem in Arrays: Gegeben: Ein Array mit n ganzen Zahlen, eine ganze Zahl x. Gesucht: Der Index i einer Komponente mit Wert x (falls vorhanden). 6.1.1 Sequentielle Suche Eine einfache Lösung dieses Problems basiert auf folgender Idee. Durchlaufe die Komponenten des Arrays sequentiell bis die aktuelle Komponente i den Wert x enthält. Gebe dann i aus. Ist x in keiner Komponente, gebe −1 aus. Diese Methode heißt sequentielle Suche. Sie erfordert im schlimmsten Fall n Vergleiche bis der Index i gefunden ist, bzw. festgestellt wird, dass keine Komponente den Wert x hat. Eine Java Methode hierfür ist: Programm 6.1 sequentialSearch Version vom 27. November 2004 115 116 KAPITEL 6. ALGORITHMEN AUF ARRAYS /** * searches for an element in an int array * by sequential search * * @param a the int array * @param x the int to search for * @return the index of the first component containing <code>x</code> * or <code>-1</code> if <code>x</code> is not in the array */ public int sequentialSearch(int[] a, int x){ int k = 0; // variable to hold the index or -1 while (k < a.length) { if (a[k] == x) { return k; // found x } k++; } return -1; } Beim i-ten Wiedereintritt in die for Schleife gilt die Zusicherung (Assertion) i < n und a[0], . . . , a[i − 1] 6= x Beim Austritt aus der Schleife gilt a[i] = x oder i = a.length Ist i = n, so ist x nicht im Array. Diese Überlegungen zeigen die Korrektheit des Algorithmus. Zusicherungen bei Schleifen nennt man auch Schleifeninvarianten. Sie spielen bei Korrektheitsbeweisen eine wichtige Rolle. 6.1.2 Binäre Suche Liegt das Array a in sortierter Form vor (mit n = a.length), gilt also a[0] ≤ a[1] ≤ . . . ≤ a[n−1], so lässt sich die Suche wesentlich beschleunigen durch die Verwendung der binären Suche oder Bisection. Die Idee der binären Suche ist wie folgt: – Wähle den mittleren Index i und vergleiche x und a[i]. – Ist a[i] = x, so ist der Index i gefunden. – Ist a[i] < x, so kann x nur in der “linken Hälfte” von a, d. h. in a[0] . . . a[i − 1] sein. Wende das Verfahren auf die linke Hälfte an. 6.1. SUCHEN EINER KOMPONENTE VORGEGEBENEN WERTES 117 – Ist a[i] > x, so kann x nur in der “rechten Hälfte” von a, d. h. in a[i + 1] . . . a[n − 1] sein. Wende das Verfahren auf die rechte Hälfte an. – Verfahre so weiter bis x gefunden, oder der noch zu durchsuchende Teil, in dem x sein könnte, leer ist. Als Beispiel betrachten wir a mit den Werten 3 5 6 8 10 12 13 16 0 1 2 3 4 5 6 7 und x = 10. Zuerst wird i in der Mitte gewählt, etwa i = 3. Dann ist a[i] = 8 < x = 10, und es wird in der rechten Hälfte von a weitergesucht. 10 12 13 16 4 5 6 7 i wird wieder in der Mitte gewählt, etwa i = 5. Dann ist a[i] = 12 > x = 10, und es wird in der linken Hälfte des verbleibenden Arrays weiter gesucht, also in 10 4 Die einzig verbleibende Wahl von i = 4 findet dann den gesuchten Wert. Wäre jetzt x 6= a[i], so stellte man an dieser Stelle fest dass x nicht im Array enthalten sein kann. Eine Java Methode hierfür ist: Programm 6.2 binarySearch /** * searches for an element in an int array * by binary search * * @param a the int array * @param x the int to search for * @return the index of the first component containing <code>x</code> * or <code>-1</code> if <code>x</code> is not in the array */ public int binarySearch(int[] a, int x){ int k; // variable to hold the index or -1 int i, j; // lower and upper array bounds i = 0; // initial array bounds j = a.length - 1; // initial array bounds while (i <= j) { k = (i + j) / 2; // choose k in the middle if (a[k] == x) { return k; // found x 118 KAPITEL 6. ALGORITHMEN AUF ARRAYS } if (x > a[k]) { i = k + 1; } else { j = k - 1; // update bounds } } return -1; } Vor jedem Eintritt in die Schleife gilt (falls x im Array ist) die Invariante a[i] < x ≤ a[ j] und 0 ≤ i ≤ j ≤ n − 1. Da in jedem Schleifendurchlauf i erhöht oder j erniedrigt wird, terminiert das Programm mit a[k] = x oder i > j. Im ersten Fall wird der richtige Index k zurückgegeben. Im zweiten Fall (also bei i > j) ist der Bereich, in dem x sein könnte, wegen der Schleifeninvariante leer und es wird −1 zurückgegeben. Also arbeitet der Algorithmus korrekt. Bezüglich der nötigen Anzahl von Vergleichen ergibt sich: Satz 6.1 (Aufwand der binären Suche) Für die Anzahl C(n) von Vergleichen bei der binären Suche in einem Array mit n Komponenten gilt1 C(n) ≤ blog nc + 1. Beweis: Der Beweis erfolgt durch vollständige Induktion nach n. Induktionsanfang: Ist n = 1, so ist nur 1 Vergleich erforderlich. Also stimmt die Behauptung wegen blog 1c + 1 = 0 + 1 = 1. Induktionsvoraussetzung: Die Behauptung sei richtig für alle Arrays der Länge < n, n ≥ 2. Schluss auf n: Nach dem ersten Vergleich mit a[k] muss nur in einer der Hälften weitergesucht werden. Beide Hälften haben eine Länge ≤ bn/2c und erfordern daher nach Induktionsvoraussetzung C(bn/2c) ≤ blog(bn/2c)c + 1 Vergleiche. Insgesamt sind dann 1 +C(bn/2c) Vergleiche erforderlich. Einsetzen ergibt: C(n) ≤ 1 +C(bn/2c) ≤ 1 + blog(bn/2c)c + 1 ≤ 1 + log(n/2) + 1 da bn/2c ≤ n/2 = 1 + log(n/2) + log 2 = 1 + log((n/2) · 2) da log(a · b) = log a + log b = 1 + log n 1 log n bedeutet hier (wie stets in der Informatik) den Logarithmus von n zur Basis 2. dae ist a nach oben gerundet, bac ist a nach unten gerundet, also d5,2e = 6 und b5,2c = 5. 119 6.2. LINEARE GLEICHUNGSSYSTEME Also ist C(n) ≤ 1 + log n. Da C(n) ganzzahlig ist, folgt C(n) ≤ b1 + log nc = blog nc + 1. Es sind also wesentlich weniger Vergleiche nötig als bei der sequentiellen Suche. Bei n = 1.000.000 ≈ 220 = 1048576 reichen C(n) = 20 Vergleiche bei der binären Suche aus, während die sequentielle Suche 1.000.000 Vergleiche braucht. 6.2 Lineare Gleichungssysteme 6.2.1 Vektoren und Matrizen Ein n-dimensionaler Vektor (genauer: Spaltenvektor) mit Elementen aus einer Menge I ist ein n-Tupel x1 x2 x = . mit xi ∈ I, i = 1, . . . , n. .. xn Die Menge aller solchen Vektoren wird mit Vn (I) bezeichnet. Meist ist I = R, d. h. Vn (I) = Vn (R). Dafür schreibt man auch kurz Rn . Eine m × n-Matrix mit Elementen aus einer Menge I a11 a12 . . . a21 a22 . . . .. . A= ai1 ai2 . . . .. . am1 am2 . . . ist eine Zusammenfassung a1 j . . . a1n a2 j . . . a2n .. .. . . ai j . . . ain .. . am j . . . amn von m · n Elementen aus I. Mm,n (I) bezeichnet die Menge aller m × n-Matrizen von Elementen aus I. Meist ist I = R oder I ⊆ R. Jede Spalte von A bildet einen Vektor A· j , den sogenannten Spaltenvektor ( j = 1, . . . , n); analog bildet jede Zeile Ai· einen Zeilenvektor (i = 1, . . . , m). Offensichtlich lassen sich Vektoren und Matrizen in Java durch 1- bzw. 2-dimensionale Arrays darstellen. double[] vector = new double[n]; double[][] matrix = new double[m][n]; definieren entsprechende Array-Objekte. Matrizen und Vektoren können wie folgt miteinander multipliziert werden: 120 KAPITEL 6. ALGORITHMEN AUF ARRAYS Multiplikation einer m × n-Matrix A mit einem n-Vektor x: a x + a x + ··· + a x 11 1 12 2 1n n a11 . . . a1n x1 a x + a x + · · · + a .. .. .. = 21 1 22 2 2n xn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . am1 . . . amn xn am1 x1 + am2 x2 + · · · + amn xn Beispiele: 1. Multiplikation einer 2 × 3 Matrix mit einem 3-Vektor. Ergebnis ist ein 2-Vektor. 1 1 2 1 1 · 1 + 2 · 0 + 1 · (−1) 0 · 0 = = 2 0 −1 2 · 1 + 0 · 0 + −1 · (−1) 3 −1 2. Multiplikation einer 1 × 3 Matrix mit einem 3-Vektor. Ergebnis ist eine Zahl. 1 1 2 1 · 0 = 1 · 1 + 2 · 0 + 1 · (−1) = 0 −1 Multiplikation einer m × p-Matrix A mit einer p × n-Matrix B: a11 . . . a1p .. .. b c11 . . . c1n . 11 . . . b1 j . . . b1n . . .. .. = .. .. ai1 . . . aip . . . . .. .. .. b cm1 . . . cmn . p1 . . . b p j . . . b pn . am1 . . . amp mit ci j = ai1 b1 j + ai2 b2 j + · · · + aip b p j p = ∑ aik bk j k=1 Die Formel zur Berechnung von ci j zeigt, dass ci j aus der i-ten Zeile von A und der j-ten Spalte von B gebildet wird. Die illustriert Abbildung 6.1. Beispiele: 1. Multiplikation einer 2 × 3 Matrix mit einer 3 × 3 Matrix. Ergebnis ist eine 2 × 3 Matrix. 2 1 3 1 2 1 −1 0 0 2 0 −1 1 1 0 1 · 2 + 2 · (−1) + 1 · 1 , 1·1+2·0+1·1 , 1·3+0+0 = 2 · 2 + 0 · 0 + (−1) · 1 , 2 · 1 + 0 · 0 + (−1) · 1 , 2 · 3 + 0 + 0 1 2 3 = 3 1 6 mit cij = ai1 b1j + ai2 b2j + · · · + aip bpj = p ' aik bkj k=1 Die LINEARE Formel zurGLEICHUNGSSYSTEME Berechnung von cij zeigt, daß cij aus der i-ten Zeile von A und der j-ten 6.2. 121 Spalte von B gebildet wird. Die illustriert Abbildung 6.1. i · = ij j Abbildung Schemader derMatrixmultiplikation. Matrixmultiplikation. Abbildung6.1: 6.1: Schema Beispiele: 2. Multiplikation einer 1 × 3 Matrix mit einem 3-Vektor. Ergebnis ist eine Zahl. einer 3 × 3 Matrix. Ergebnis ist eine 2 × 3 Matrix. mit 1. Multiplikation einer 2 × 3 Matrix 2 = 1 · 2 + 2 · 0 + 1 · 1 = 3 ( 1, 2, 2 1 1 30 1 2 1 −1 0 01 2 0 −1 1 1 0 3. Multiplikation eines 3 Vektors mit einer 1 × 3 Matrix. Ergebnis ist eine 3 × 3 Matrix. ( 1 · 2 + 2 · (−1) + 1 · 1 , 1·1+2·0+1·1 , 1·3+0+0 = 2 2· 2 + 0 · 0 + (−1) · 1 ,2 ·21 · 12+ · 20 ·20· + 1 (−1) · 1 2 , 4 2 ·23 + 0 + 0 0 1,(2, 1 = 0·1 0·2 0·1 = 0 0 0 1 2 3 = 1·1 1·2 1·1 1 2 1 1 3 1 6 Diese Beispiele zeigen, dass die Matrixmultiplikation die Multiplikation von Matrizen mit Vektoren als Spezialfall enthält. Stellt man sich die n Spalten der p × n-Matrix B als p-Spaltenvektoren B· j , j = 1, . . . , n, vor, so gilt A · B = A · B·1 , . . . , B·n = A · B·1 , . . . , A · B·n . In Java lässt sich die Matrizenmultiplikation folgendermaßen realisieren: double[][] a = new double[m][p]; double[][] b = new double[p][n]; double[][] c = new double[m][n]; int i, j, k; // multiplication for (i = 0; i < m; i++) { // Array für Matrix A // Array für Matrix B // Array für Ergebnismatrix C 122 KAPITEL 6. ALGORITHMEN AUF ARRAYS for (j = 0; j < n; j++) { c[i][j] = 0; for (k = 0; k < p; k++) { c[i][j] += a[i][k] * b[k][j]; }//end for k }//endfor j }//endfor i Wir bauen diese Multiplication jetzt in eine Klasse Matrix für Matrizen ein. Jedes Matrix Objekt hat die drei (privaten) Felder matrix (für die Einträge), numberOfRows und numberOfColumns (für Anzahl der Zeilen und Spalten). In den Methoden und Konstruktoren der Klasse Matrix verwenden wir die Variable mit dem reservierten Namen this. Sie bezeichnet eine implizite Referenz auf das Objekt, das gerade im Konstruktor konstruiert wird bzw. eine Methode für sich aufruft. Sind a, b Objekte der Klasse Matrix, und ruft a die Methode a.multiply(b) für sich auf, so bezeichnet also this im Rumpf von multiply() die Referenz auf das Objekt a. Diese implizite Referenz kann nur innerhalb der Klasse durch die Variable this angesprochen werden. Mehr dazu findet sich in Kapitel 7.3. Programm 6.3 Matrix import java.lang.IllegalArgumentException; import java.lang.ArrayIndexOutOfBoundsException; /** * This is a class for rectangular double matrices */ public class Matrix { /** * the private representation of a matrix is a * 2-dimensional double array */ private double[][] matrix; /** * number of rows * is 0 for empty matrix */ private int numberOfRows; /** * number of columns * is 0 for empty matrix */ private int numberOfColumns; 6.2. LINEARE GLEICHUNGSSYSTEME 123 /** * default constructor, creates empty matrix */ public Matrix() { matrix = null; numberOfRows = 0; numberOfColumns = 0; } /** * constructor, constructs a matrix with prescribed number * of rows and columns and all entries equal to zero * * Throws <code>IllegalArgumentException</code> if * <code>m</code> or <code>n</code> is negative * * @param m the number of rows * @param n thenumber of columns */ public Matrix(int m, int n) throws IllegalArgumentException { if ((m <= 0) || (n <= 0)) { throw new IllegalArgumentException("Anzahl der " + "Zeilen und Spalten muss positiv sein."); } // this is a reference to the matrix to be constructed // we could also say matrix = new double[m][n] etc., // but we use this to make the reference clear this.matrix = new double[m][n]; this.numberOfRows = m; this.numberOfColumns = n; } /** * @return the number of rows of this matrix */ public int getNumberOfRows() { // here this refers to the object that applies the method // i.e., in myMatrix.getNumberofRows(), this refers to myMatrix // we could also say return numberOfRows, // but we use this to make the reference clear return this.numberOfRows; } /** 124 KAPITEL 6. ALGORITHMEN AUF ARRAYS * @return the number of coulumns of this matrix */ public int getNumberOfColumns() { return this.numberOfColumns; } /** * returns the entry at a matrix position * * Throws <code>IllegalArgumentException</code> if * <code>i</code> or <code>j</code> is negative * * Throws <code>ArrayIndexOutOfBoundsException</code> if * <code>i</code> or <code>j</code> is too big * * @param i the row index * @param j the colum index * @return the entry in row i and column j */ public double getEntry(int i, int j) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { if ((i < 0) || (j < 0)) { throw new IllegalArgumentException("Negative Indizes"); } if (this.matrix == null) { throw new IllegalArgumentException("Matrix ist leer."); } if ((i > this.getNumberOfRows()) || (j > this.getNumberOfColumns())) { throw new ArrayIndexOutOfBoundsException( "Index zu groß."); } return this.matrix[i][j]; } /** * sets the entry at position (i,j) of this matrix * * Throws <code>IllegalArgumentException</code> if * <code>i</code> or <code>j</code> is negative or * if this matrix is empty * * Throws <code>ArrayIndexOutOfBoundsException</code> if 6.2. LINEARE GLEICHUNGSSYSTEME 125 * <code>i</code> or <code>j</code> is too big * * @param i the row index * @param j the colum index * @param x the value of the entry */ public void setEntry(int i, int j, double x) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { if ((i < 0) || (j < 0)) { throw new IllegalArgumentException("Negative Indizes"); } if (this.matrix == null) { throw new IllegalArgumentException("Matrix ist leer."); } if ((i > this.getNumberOfRows()) || (j > this.getNumberOfColumns())) { throw new ArrayIndexOutOfBoundsException( "Index zu groß."); } this.matrix[i][j] = x; } /** * multiplies this matrix with another matrix * * Throws <code>IllegalArgumentException</code> if * dimensions are not compatible or * if this matrix is empty * * @param m the other matrix */ public void multiply(Matrix m) throws IllegalArgumentException { if (this.getNumberOfColumns() != m.getNumberOfRows()) { throw new IllegalArgumentException("Falsche " + "Dimensionen"); } if (this.matrix == null) { throw new IllegalArgumentException("Multiplikation " + "mit leerer Matrix."); } // array for result double[][] c = new double[this.getNumberOfRows()] 126 KAPITEL 6. ALGORITHMEN AUF ARRAYS [m.getNumberOfColumns()]; int i, j, k; // multiplication for (i = 0; i < this.getNumberOfRows(); i++) { for (j = 0; j < m.getNumberOfColumns(); j++) { c[i][j] = 0; for (k = 0; k < this.getNumberOfColumns(); k++){ c[i][j] += this.matrix[i][k] * m.getEntry(i, j); }//end for k }//endfor j }//endfor i // store the result in this matrix, // update this column number this.matrix = c; this.numberOfColumns = m.getNumberOfColumns(); } } Wie üblich werden jetzt Matrizen mit Matrix a = new Matrix(10, 15); erzeugt. Mit Anweisungen der Form a.setEntry(2, 1, 3.14); werden Komponenten gesetzt, und zwei Matrizen a und b werden mit a.multiply(b); miteinander multipliziert, wobei dann das Ergebnis in der Matrix a steht. Die algebraische Struktur der Menge der Matrizen (Rechenstruktur!) wird in der Linearen Algebra ausführlich behandelt. Wir werden im weiteren die Kurzschreibweisen A · x bzw. Ax für die Matrix-Vektor Multiplikation, und A · B bzw. AB für die Matrizenmultiplikation verwenden (geeignete “Abmessungen” vorausgesetzt). 127 6.2. LINEARE GLEICHUNGSSYSTEME 6.2.2 Ein Produktionsmodell Ein Betrieb verarbeitet an Produktionsstätte P1 die Rohstoffe R1 . . . , Rn zu Zwischenprodukten Z1 , . . . Z p , die dann an der Produktionsstätte P2 zu Endprodukten E1 , . . . , Em weiterverarbeitet werden. Der Bedarf an Rohstoffen für die Zwischenprodukte ist durch Tabelle 6.1 gegeben, d. h. zur Produktion einer Einheit von Zi werden bi j Einheiten von R j benötigt, j = 1, . . . , n. Tabelle 6.1: Rohstoffbedarf für Zwischenprodukte. R1 b11 R2 b12 ... ... Rn b1n Zi .. . bi1 bi2 ... bin Zp b p1 b p2 ... b pn Z1 .. . Ebenso ist der Bedarf an Zwischenprodukten für die Endprodukte durch Tabelle 6.2 gegeben. Tabelle 6.2: Zwischenproduktbedarf für Endprodukte. Z1 a11 Z2 a12 ... ... Zp a1p Ei .. . ai1 ai2 ... aip Em am1 am2 ... amp E1 .. . Diese beiden Tabellen definieren Matrizen A und B gemäß A := (ai j ) i=1,...,m , j=1,...,p B := (bi j ) i=1,...,p j=1,...,n Dann ergibt sich der Bedarf ci j an Rohstoff R j für die Produktion einer Einheit von Endprodukt Ei zu ci j = ai1 b1 j + ai2 b2 j + · · · + aip b p j . Bezeichnet man die Matrix der ci j als C, also C := (ci j ) i=1,...,m , so ergibt sich C also durch Multiplikaj=1,...,n tion der Matrizen A und B, d. h. C = A·B . C heißt Rohstoffbedarfsmatrix. 128 KAPITEL 6. ALGORITHMEN AUF ARRAYS Diese Daten sollen jetzt zum Einkauf der benötigten Rohstoffe für eine vorgegebene Produktion von yi Einheiten von Endprodukt Ei (i = 1, . . . , m) genutzt werden. Gegeben ist also der Produktionsvektor y= y1 y2 .. . ym Die benötigte Menge z j des Zwischenprodukts Z j an Produktionsstätte P1 ergibt sich zu z j = y1 · a1 j + y2 · a2 j + · · · + ym · am j a1 j a2 j = (y1 , . . . , ym ) . = (a1 j , a2 j , . . . , am j ) .. am j y1 .. . ym Bezeichnet z1 z = ... zp den Bedarfsvektor an Zwischenprodukten, so gilt also z = AT y . 2 Hieraus ergibt sich der Bedarf x j an Rohstoff R j wie folgt: x j = z1 · b1 j + z2 b2 j + · · · + z p · b p j b1 j = (z1 , . . . , z p ) ... = (b1 j , . . . , b p j ) bp j z1 .. = BT · z . ·j . zp Also gilt für den Bedarfsvektor x x1 x = ... = BT · z = BT AT y . xn 2 AT ist die zu A transponierte Matrix AT = (aT ) mit aT = a . Sie ergibt sich aus A durch Spiegeln an der Hauptdiagoji ij ij nalen. 129 6.2. LINEARE GLEICHUNGSSYSTEME Dieser Bedarfsvektor lässt sich natürlich auch direkt über die Rohstoffbedarfsmatrix C = A · B ermitteln: xj = Bedarf an R j für alle Ei zusammen bei Produktionsvektor y = y1 c1 j + y2 c2 j + · · · + ym cm j c1 j c2 j = (y1 , y2 , . . . , ym ) . = (c1 j , . . . , cm j ) .. y1 y2 .. . cm j ym = C·Tj · y . Also ist x = CT · y = BT · AT · y . 3 Jetzt betrachten wir die umgekehrte Fragestellung. Es sind nur bestimmte Mengen von Rohstoffen verfügbar, die durch einen Rohstoffvektor r1 r = ... rn beschrieben werden. Gesucht ist ein Produktionsvektor y1 y = ... , ym der die bezüglich r mögliche Produktion angibt. Die obige Matrizengleichung liefert CT y = r, d. h. das lineare Gleichungssystem c11 y1 + c21 y2 + . . . + cm1 ym = r1 c12 y1 + c22 y2 + . . . + cm2 ym = r2 ... c1n y1 + c2n y2 + . . . + cnm ym = rn mit n Gleichungen und m Unbekannten.4 3 Es gilt allgemein bezüglich der Transponierung (A · B)T = BT AT (Übung). Daher hätte man die obige Gleichung auch aus dieser Regel direkt ableiten können. 4 Es handelt sich hier um ein spezielles lineares Gleichungssystem in nicht üblicher Schreibweise, die daraus resultiert, dass die transponierte Matrix CT verwendet wird. 130 6.2.3 KAPITEL 6. ALGORITHMEN AUF ARRAYS Das Gaußsche Eliminationsverfahren Wir betrachten jetzt lineare Gleichungssysteme in allgemeiner Form (und Schreibweise). Das System a11 x1 + a12 x2 + . . . + a1n xn = b1 a21 x1 + a22 x2 + . . . + a2n xn = b2 .............................. am1 x1 + am2 x2 + . . . + amn xn = bm heißt lineares Gleichungssystem mit m Gleichungen in den n Variablen x1 , . . . , xn . In Matrizenschreibweise schreibt sich dieses System kurz als Ax = b mit a11 . . . a1n , A= am1 . . . amn x1 x = ... , xn b1 b = ... . bm A wird Koeffizientenmatrix des Gleichungssystems genannt, und b heißt rechte Seite des Gleichungssystems. Ein Vektor x1 x = ... xn mit Ax = b heißt Lösung des linearen Gleichungssystems. Die Menge aller Lösungen wird Lösungsraum des Gleichungssystem genannt. Wir werden jetzt einen einfachen Algorithmus zur Lösung linearer Gleichungssysteme entwickeln, das sogenannte Gaußsche Eliminationsverfahren. Dazu benötigen wir zunächst einige Hilfsaussagen über Umformungen des Gleichungssystems, die den Lösungsraum unverändert lassen. Lemma 6.1 Die Addition bzw. Subtraktion des Vielfachen einer Gleichung zu einer anderen ändert den Lösungsraum nicht. Beweis: Addiere das c-fache der k-ten Gleichung zur i-ten. Es entsteht das Gleichungssystem Āx = b̄: a11 x1 +...+ a1n xn = b1 ............................................. (ai1 + c · ak1 )x1 + . . . + (ain + c · akn )xn = bi + c · bk ............................................. a11 x1 +...+ a1n xn = bn Sei x̄ Lösung von Āx = b̄. Dann erfüllt x̄ auch alle Gleichungen von Ax = b (außer evtl. der i-ten), da diese in Āx = b̄ unverändert sind. 131 6.2. LINEARE GLEICHUNGSSYSTEME Wir zeigen nun, dass x̄ auch die i-te Gleichung von Ax = b erfüllt und daher auch eine Lösung von Ax = b ist. Da x̄ eine Lösung von Āx = b̄ ist, gilt (die i-te Gleichung hingeschrieben): (ai1 + cak1 )x̄1 + · · · + (ain + c · akn )x̄n = bi + c · bk . Die linke Seite ist gleich (ai1 x̄1 + · · · + ain x̄n ) + c(ak1 x̄1 + · · · + akn x̄n ) . Nun ist (ak1 x̄1 + · · · + akn x̄n ) = bk , da x̄ die k-te Gleichung von Ax = b löst. Also folgt: bi + c · bk = ai1 x̄1 + · · · + ain x̄n + c · bk oder bi = ai1 x̄1 + · · · + ain x̄n , d. h. x̄ löst auch die i-te Gleichung von Ax = b. Also ist jede Lösung von Ax̄ = b̄ auch eine Lösung von Ax = b. Die Umkehrung folgt entsprechend. Lemma 6.2 Die Multiplikation einer Gleichung mit einer Konstanten 6= 0 ändert den Lösungsraum nicht. Beweis: Der Beweis erfolgt analog zu Lemma 6.1. Die Operationen “Addition des Vielfachen einer Zeile zu einer anderen” und “Multiplikation einer Zeile mit einer Konstanten” heißen auch elementare Umformungen. Lemma 6.3 Das Vertauschen von Zeilen (Umnummerieren der Gleichungen) und Spalten (Umnummerieren der Variablen) ändern den Lösungsraum (bis auf Umnummerieren der Koordinaten) nicht. Beweis: Klar. Satz 6.2 Das Gleichungssystem Ax = b kann durch elementare Umformungen und gegebenenfalls . Zeilen und Spaltenvertauschungen so umgeformt werden, dass die erweiterte Matrix (A .. b) die in Abbildung 6.2 dargestellte Dreiecksform hat, d. h. āii 6= 0 für i = 1, . . . , k, āi j = 0 für i > 1 und i < j . 132 KAPITEL 6. ALGORITHMEN AUF ARRAYS 0 0 ... 0 0 .. . b̄1 .. .. . . .. . .. . b̄k .. . b̄k+1 0 0 ... 0 0 .. . ā11 0 .. . .. . .. . āi j .. . ākk b̄m Abbildung 6.2: Dreiecksform der erweiterten Matrix. Bevor wir Satz 6.2 beweisen, rechnen wir zunächst einige Beispiele durch. Beispiel 6.1 1. m ≤ n x1 + x2 − x3 = 2 x1 + x3 = −1 . Die erweiterte Matrix (A .. b) hat die Form .. 1 1 −1 . 2 . . 1 0 1 .. −1 Die Subtraktion der ersten Zeile von der zweiten ergibt .. 1 1 −1 . 2 , . 0 −1 2 .. −3 wodurch die Dreiecksform erreicht ist. 2. m > n x1 + x2 = 1 x1 − x2 = 0 x1 + 2x2 = 2 . Die erweiterte Matrix (A .. b) hat die Form .. 1 1 . 1 . 1 −1 .. 0 . 1 2 .. 2 . 6.2. LINEARE GLEICHUNGSSYSTEME 133 Die Subtraktion der ersten Zeile von der zweiten und der dritten ergibt .. 1 1 . 1 . 0 −2 .. −1 , .. 0 1 . 1 und die Addition des 12 -fachen der zweiten Zeile zu der dritten .. 1 1 . 1 . 0 −2 .. −1 , . 0 0 .. 1/2 wodurch die Dreiecksform erreicht ist. Beweis: (von Satz 6.2) Der Beweis erfolgt durch die Angabe eines Algorithmus (im Struktogramm) und den Nachweis seiner Korrektheit. Das Struktogramm ist in Abbildung 6.3 angegeben. Die Korrektheit des Algorithmus basiert auf der folgenden Schleifeninvariante: Die erweiterte Matrix hat bei jedem Eintritt in die Schleife die in Abbildung 6.4 dargestellte Form. Dies folgt direkt aus der Tatsache, dass die Addition in den ersten Stellen 1, . . . , i − 1 nur 0 + 0 ergibt und die i-te Spalte beim i-ten Durchlauf unterhalb von Zeile i zu Null gemacht wird. Also verlässt man die Schleife mit k = min{n, m} oder k < min{n, m} und à = 0. Der obige Algorithmus wird auch als Gaußsches Eliminationsverfahren zur Lösung linearer Gleichungssysteme bezeichnet. Seine Nutzung zur Lösung linearer Gleichungssysteme beruht auf dem folgenden Satz: . Satz 6.3 (Lösungskriterien für lineare Gleichungssysteme) Sei Ax = b gegeben, und seien (Ā .. b̄) und k die durch das Gaußsche Eliminationsverfahren gelieferten Größen. Dann gilt: a) Ax = b hat genau dann (mindestens) eine Lösung wenn b̄i = 0 für alle i > k (falls k < m). b) In diesem Falle erhält man alle Lösungen, indem man (für k < n) xk+1 , xk+2 , . . . , xn beliebig wählt und die anderen Variablen x1 , . . . , xk aus der Dreiecksform ā11 x1 + ā12 x2 + · · · + ā1k xk = b̄1 − (ā1,k+1 xk+1 + · · · + ā1,n xn ) ā22 x2 + · · · + ā2k xk = b̄2 − (ā2,k+1 xk+1 + · · · + ā2,n xn ) .. . ākk xk = b̄k − (āk,k+1 xk+1 + · · · + āk,n xn ) durch sukzessives Ausrechnen von xk , xk−1 , . . . , x1 “von unten nach oben” erhält. 127 6.2. LINEARE GLEICHUNGSSYSTEME 134 KAPITEL 6. ALGORITHMEN AUF ARRAYS . . Setze (à ... b̃) := (A. .. b) und i := 1 Setze (à .. b̃) := (A .. b) und i := 1 à #= 0 (Nullmatrix) and i ≤ min{n, m} à 6= 0 (Nullmatrix) and i ≤ min{n, m} Ändere à durch Zeilen- und Spaltenpermutationen bis ãii #= 0 Ändere à durch Zeilenund Spaltenpermutationen bisinãiiZeile 6= 0 gilt. gilt. {Das gewählte Element heißt Pivotelement i.} {Das gewählte Element heißt Pivotelement in Zeile i.} Addiere zu allen Zeilen ! mit ã!i #= 0 ein geeignetes Vielfaches, Addiere zu+allen ` mit ã`i 6= 0 ein so daß ã!i c · ãZeilen ist cgeeignetes =ã − ãã!i . Vielfaches, Dies nennt ii = 0 gilt. {Dann ii `i so dass ã + c · ã = 0 gilt. {Dann ist c = − . Dies nennt man ii `i man Pivotoperation oder Pivotisierung.} ãii Pivotoperation oder Pivotisierung.} i := i + 1 i := i + 1 !! ! !! hh! hh! hhh!!!!! i ≤ min{n, m} hhhh !!! i ≤ min{n, m} hhh !!!! hhhh !!! hhh !!!! true hhhh !! true hhh !!! hhh . Lasse von (Ã.. .. b̃) die Zeile und Spalte mit Index i − 1 weg. Lasse von (à . b̃) die Zeile und Spalte mit Index i − 1 weg. .. Bezeich.. Bezeichne die entstehende Matrix wieder mit ( à . b̃). ne die entstehende Matrix wieder mit (à . b̃). Abbildung 6.3: Gauß-Eliminationsverfahrens. Abbildung 6.3:Struktogramm Struktogramm des des Gauß-Eliminationsverfahrens. #= 0 #= 0. .. #= 0 ãii 0 à b̃ Abbildung 6.4: imBeweis Beweiszuzu Satz Abbildung 6.4:Die DieSchleifeninvariante Schleifeninvariante im Satz 6.2.6.2. 135 6.2. LINEARE GLEICHUNGSSYSTEME c) Dies sind bereits alle Lösungen von Ax = b. Man nennt k auch den Rang der Matrix A. Aussage a) bedeutet dann, dass Ax = b genau dann lösbar . ist, wenn der Rang von A gleich dem Rang der erweiterten Matrix (A .. b) ist. Aussage b) und c) bedeuten, dass der Lösungsraum ein (n − k)-dimensionaler affiner Raum ist. Dies bedeutet grob gesagt folgendes: – Der Lösungsraum des zugehörigen homogenen Systems Ax = 0 ist ein (n − k)-dimensionaler Vektorraum L. – Man erhält alle Lösungen x des inhomogenen Systems Ax = b als Kombination x = x̃ + y, wobei x̃ irgendeine fest gewählte Lösung von Ax = b ist, und y alle Lösungen von Ax = 0 durchläuft. Genauer werden diese Zusammenhänge in der Linearen Algebra in einem allgemeineren Rahmen untersucht. Beispiel 6.2 (Fortsetzung von Beispiel 6.1) 1. Das Gleichungssystem ist lösbar, da k = 2 = m. Man erhält . x1 x2 x3 .. b ................... . . 1 1 −1 .. 2 . 0 −1 2 .. −3 Man kann x3 beliebig wählen (z. B. x3 = c) und erhält x1 + x2 = 2 + c −x2 = −3 − 2c Hieraus ergibt sich x2 = 3 + 2c, und dann aus der ersten Gleichung x1 = −x2 + 2 + c = −(3 + 2c) + 2 + c = −1 − c . −1 − c Also ist der Lösungsraum { 3 + 2c | c ∈ R1 beliebig } ein 1-dimensionaler affiner Raum. c 2. Das Gleichungssystem ist nicht lösbar, da k = 2 < m und b̄3 = 1 2 6= 0. 136 KAPITEL 6. ALGORITHMEN AUF ARRAYS Beweis: (von Satz 6.3) zu a) 1. Die Bedingung b̄i = 0 für i > k ist notwendig für die Lösbarkeit: Der Gauß-Algorithmus lässt wegen Lemma 6.1–6.3 den Lösungsraum unverändert. Ist b̄i 6= 0 für ein i > k, so lautet die i-te Gleichung von Āx = b̄ 0 · x1 + · · · + 0 · xn = bi 6= 0 Dies ist für kein x erfüllt. 2. Die Bedingung b̄i = 0 für alle i > k ist auch hinreichend für die Lösbarkeit: Man kann aufgrund der Bedingung b) eine Lösung wie folgt ausrechnen: Für beliebig, aber fest gewählte Werte von xk+1 , . . . , xn ist d` := ā`,k+1 xk+1 + ā`,k+2 xk+2 · · · + ā`,n xn , ` = 1, . . . , m, eine Konstante. Dann ergibt sich (wegen ākk 6= 0) xk = 1 [b̄k − (āk,k+1 xk+1 + · · · + āk,n x̄n )] . ākk {z } | Konstante dk Sind xk , xk−1 , . . . , x`+1 bereits berechnet, so ergibt sich x` (wegen ā`` 6= 0) als x` = 1 [b̄` − (ā`,`+1 x`+1 + · · · + ā`,k xk ) − d` ] . ā`` Also folgt a) und auch b). zu c) Zeige: Jede Lösung lässt sich gemäß b) gewinnen. Sei x̄1 x̄ = ... x̄n eine Lösung. Wähle dann in b) xk+1 := x̄k+1 , . . . , xn := x̄n . Dann sind (wie die Formel oben für xl zeigt) x1 , x2 , . . . , xk eindeutig bestimmt durch die Wahl von x j = x̄ j ( j = k + 1, . . . , n). Also muss xi = x̄i für i = 1, . . . , k sein. 6.2.4 Wahl des Pivotelements Im Gauß-Algorithmus wurde die Auswahl der Pivotelemente offen—d. h. beliebig—gelassen. Aus numerischen Gründen sind jedoch bestimmte Pivotelemente vorzuziehen. 137 6.2. LINEARE GLEICHUNGSSYSTEME Beispiel 6.3 (Ungünstige Wahl des Pivotelementes) Das lineare Gleichungssystem 0, 000100x1 + x2 = 1 x1 + x2 = 2 soll mit 3-stelliger Arithmetik5 gelöst werden. Die Gauß-Elimination ergibt (Zeile 1 mit 10000 multiplizieren und von Zeile 2 abziehen): x1 (1 − 10000 · 0, 000100) +x2 (1 − 10000) = 2| − 10000 {z } {z } | {z } | =:a =:b =:c In 3-stelliger Arithmetik ergeben sich folgende Werte für a, b, c (; steht für das Runden): a = 1 − 10000 · 0, 000100 = 1 − 1 = 0 b = 1 − 10000 = −9999 = −0, 9999 · 104 ; −0, 100 · 105 = −10000 c = 2 − 10000 = −9998 = −0, 9998 · 104 ; −0, 100 · 105 = −10000 Das transformierte Gleichungssystem in 3-stelliger Arithmetik lautet dann 0, 000100x1 + x2 = 1 −10000x2 = −10000 Hieraus ergibt sich die angenäherte Lösung x1 = 0, 000 x2 = 1, 000 . Die exakte Lösung ist jedoch x1 = 1, 00010 x2 = 0, 99990 , was bedeutet, das die angenäherte Lösung für x1 auf Basis der 3-stelligen Arithmetik unvertretbar schlecht ist. Die Ursache liegt darin, dass a11 = 0, 000100 ein zu kleines Pivotelement im Verhältnis zu a21 = 1 ist. Will man dies vermeiden, so kann man (z. B. durch Zeilenvertauschung) nach einem größeren Pivotelement suchen. Im Beispiel ergibt die Zeilenvertauschung x1 + x2 = 2 0, 000100x1 + x2 = 1 5 Dies bedeutet, dass nur 3 Nachkommastellen in der normalisierten Gleitkommadarstellung (vgl. Kapitel 12) einer Zahl dargestellt werden können. Die darstellbaren Zahlen haben also die Form 0, x1 x2 x3 · 10e mit xi ∈ {0, 1, . . . , 9}, x1 6= 0 und e ganzzahlig. Bei mehr Nachkommastellen wird entsprechend gerundet. 138 KAPITEL 6. ALGORITHMEN AUF ARRAYS Die Gauß-Elimination ergibt x1 (0, 000100 − 0, 000100) +x2 (1 − 0, 000100) = 1 − 0, 000200 {z } | {z } | {z } | =:a =:c =:b mit folgenden Werten für a, b, c in 3-stelliger Arithmetik: a = 0, 000100 − 0, 000100 = 0 b = 1 − 0, 000100 = 0, 9999 = 0, 9999 · 100 ; 0, 100 · 101 = 1 = c 1 − 0, 000200 = 0, 9998 = −0, 9998 · 100 ; 0, 100 · 101 = 1 Das transformierte Gleichungssystem in 3-stelliger Arithmetik lautet dann x1 + x2 = 2 x2 = 1 , woraus sich die angenäherte Lösung x1 = x2 = 1 ergibt, die die exakte Lösung für eine 3-stellige Arithmetik sehr gut approximiert. Dies Beispiel zeigt, dass man das Gaußsche Eliminationsverfahren nicht einfach “naiv” anwenden darf, sondern sich Gedanken über die numerische Genauigkeit machen muss. Dies geschieht durch die Suche nach geeigneten Pivotelementen, die sogenannte Pivotstrategie. Es gibt zwei Standard-Pivotstrategien, die partielle und die totale Pivotsuche. Bei der partiellen Pivotsuche sucht man das Pivotelement in der jeweiligen Spalte nach folgender Regel (vgl. Abbildung 6.5): Permutiere in Schritt i diejenige Zeile (unter den Zeilen ` = i, . . . , m) mit größtem absolutem Wert |a`i | an die i-te Stelle. 20 KAPITEL 1. ALGORITHMEN AUF ARRAYS i i ! .. . Vertauschung |a!i | = max |aji | j=i,...,m Abbildung 1.5: Schema der partiellen Pivotsuche. Abbildung 6.5: Schema der partiellen Pivotsuche. Vergleiche, d. h. quadratisch in m viele. Bei der totalen Pivotsuche sucht man das Pivotelement in der gesamten verbleibenden Restmatrix nach folgender Regel (vgl. Abbildung ??): Permutiere in Schritt i die verbleibenden Zeilen und Spalten bis an Stelle (i, i) der Eintrag mit dem größten Absolutbetrag steht. i 20 6.2. LINEARE GLEICHUNGSSYSTEME KAPITEL 1. ALGORITHMEN AUF ARRAYS 139 Hierdurch ergibt sich in Schritt i deri Gauß-Elimination ein zusätzlicher Suchaufwand von m − i Vergleichen, also insgesamt über alle Schritte m i ! ∑ (m −... i) = (m − 1) + (m − 2) + · · · + 1 i=1 = Vertauschung |a!i | = max |aji | m(m − 1) j=i,...,m 2 1.5: Schema der partiellen Pivotsuche. Vergleiche, d. h.Abbildung quadratisch in m viele. Bei der totalen Pivotsuche sucht man das Pivotelement in der gesamten verbleibenden Restmatrix nach folgender Regel (vgl. Abbildung 6.6): Vergleiche, d. h. quadratisch in m viele. Bei der totalen Pivotsuche sucht man das Pivotelement in der gesamten verbleibenden Restmatrix nach folgender Regel (vgl. Abbildung ??): Permutiere in Schritt i die verbleibenden Zeilen und Spalten bis an Stelle (i, i) der Eintrag Permutiere in Schritt die verbleibenden Zeilen und Spalten bis an Stelle (i, i) der mit dem größteni Absolutbetrag steht. Eintrag mit dem größten Absolutbetrag steht. i i Zeilenpermutation größter Absolutbetrag Spaltenpermutation Abbildung 1.6: Schema der totalen Pivotsuche. Abbildung 6.6: Schema der totalen Pivotsuche. Hierdurch ergibt sich in Schritt i der Gauß Elimination ein zusätzlicher Suchaufwand von (m − i)(n − i) Vergleichen, also insgesamt über alle Schritte Hierdurch ergibt sich in Schritt i der Gauß Elimination ein zusätzlicher Suchaufwand von (m − i)(n − m i) Vergleichen, also insgesamt über alle Schritte ! (m − i)(n − i) = (m − 1)(n − 1) + (m − 2)(n − 2) + · · · + 1 i=1 m n−1 1 2 = (m − 1)(n 1)1+=(m (n −− 1)i) + (n − 2)2 + · − ·· + n − 2)(n (n − 2)) + · · · + 1 ∑ (m≤− i)(n 3 2 i=1 Vergleiche, d. h. kubisch in m viele. Vergleiche. Wenn wir der Einfachheit halber m = n annehmen (quadratische Matrizen), so ergeben In der Numerischen Mathematik werden weitere Methoden zum Erreichen numerischer Gesich und zum Abschätzen des maximalen Fehlers untersucht. nauigkeit (m − 1)2 + (m − 2)2 + · · · + 1 = m m−1 1 (m − ) 3 2 viele Vergleiche, also kubisch in m viele. In der Numerischen Mathematik werden weitere Methoden zum Erreichen numerischer Genauigkeit und zum Abschätzen des maximalen Fehlers untersucht. 140 6.2.5 KAPITEL 6. ALGORITHMEN AUF ARRAYS Eine Java Klasse zur Lösung linearer Gleichungssysteme Wir geben nun ein Java Programm für die Lösung linearer Gleichungssysteme mittels Gauß-Elimination an. Dazu implementieren wir eine Klasse LinearEquation, deren Objekte lineare Gleichungssysteme sind. Zu den public Methoden dieser Klasse gehören solveByTotalPivotSearch() und solveWithoutPivotSearch(), die die entsprechende Variante der Gauß-Elimination des letzten Abschnitts zur Lösung verwenden. Ein Object linEq dieser Klasse kann sich also durch die Anweisung linEq.solveByTotalPivotSearch(); selbst lösen. Die Lösbarkeit wird in dem private Feld solvable abgespeichert, auf das mit der public Methode isSolvable() zugegriffen werden kann. Ist das System lösbar (also linEq.isSolvable() == true), so wird eine Lösung in dem private Feld solution abgespeichert, auf die mit getSolution() zugegriffen werden kann. Eine javadoc Übersicht aller public Konstruktoren und Methoden (es gibt keine public Felder) der Klasse LinearEquation ist in Abbildung 6.7 angegeben, genauere Informationen enthalten Abbildung 6.8 und Abbildung 6.9. Bei der Pivotsuche werden Vertauschungen von Zeilen und Spalten nicht tatsächlich durchgeführt (dies würde zu viel Rechenzeit erfordern), sondern man merkt sich die Indexvertauschungen in zwei Hilfsarrays, nämlich int[] row; int[] col; // Zeilenindex // Spaltenindex Ist durch double[][] a; die Koeffizientenmatrix definiert, so ist a[row[i]][col[j]] also das “aktuelle” Element an der Stelle (i, j); entsprechend sind x[col[j]] und b[row[i]] die aktuellen Elemente für double[] x; double[] b; // Variable // rechte Seite Die Arrays row und col werden beim Aufruf des Konstruktors LinearEquation(double[][], double[]) entsprechend zu row[i] = i und col[j] = j initialisiert. Neben der Klasse LinearEquation werden noch die Hilfsklassen MatrixPosition und DimensionException benötigt. Sie sind am Ende von Programm 6.4 aufgeführt. 6.2. LINEARE GLEICHUNGSSYSTEME Abbildung 6.7: javadoc Summary der Klasse LinearEquation. Programm 6.4 Java Klassen zum Lösen linearer Gleichungssysteme /** * class for systems of linear equations */ public class LinearEquation { /** * will be true if system is solvable 141 142 KAPITEL 6. ALGORITHMEN AUF ARRAYS Abbildung 6.8: Konstruktoren der Klasse LinearEquation. */ private boolean solvable; /** * will be true if solvability has been checked */ private boolean solved; /** * the rank of the coefficient matrix */ private int rank; /** * the matrix of coefficients */ private double[][] coeff; /** * the right hand side */ private double[] rhs; 6.2. LINEARE GLEICHUNGSSYSTEME Abbildung 6.9: Die wichtigsten Methoden der Klasse LinearEquation. /** * encodes row permutations, row i is at position row[i] */ private int[] row; /** * encodes column permutations, column j is at position col[j] */ private int[] col; /** 143 144 KAPITEL 6. ALGORITHMEN AUF ARRAYS * holds the solution vector */ private double[] solution; /** * is true if system is in triangular form */ private boolean triangular; /** * default constructor */ public LinearEquation() { solvable = false; solved = false; triangular = false; rank = 0; solution = null; coeff = null; rhs = null; row = null; col = null; } /** * constructs object with given coeff matrix and rhs * and initializes row and col * * @param a the matrix to which the coeff matrix is set * @param b the rhs to which the rhs is set */ public LinearEquation(double[][] a, double[] b) throws NullPointerException, DimensionException { if (a == null || b == null) { throw new NullPointerException("zugewiesener Array " + "ist null"); } if (a.length != b.length) { throw new DimensionException("unverträgliche " + "Dimension"); } coeff = a; rhs = b; 6.2. LINEARE GLEICHUNGSSYSTEME 145 row = new int[coeff.length]; for (int i = 0; i < coeff.length; i++) row[i] = i; col = new int[coeff[0].length]; for (int j = 0; j < coeff[0].length; j++) col[j] = j; rank = 0; solution = null; solved = false; solvable = false; triangular = false; } /** * tests if system has been tested for solvability * * @return true if a solutioan has already been computed */ public boolean isSolved() { return solved; } /** * brings system into triangular form with choice of pivot method * @exception NullPointerException * @exception DimensionException */ private void triangularForm(int method) throws NullPointerException { if (coeff == null || rhs == null) { throw new NullPointerException(); } int m = coeff.length; int n = coeff[0].length; int i, j, k; // counters int pivotRow = 0; // row index of pivot element int pivotCol = 0; // column index of pivot element double pivot; // value of pivot element // main loop, transformation to triangle form k = -1; // denotes current position on diagonal boolean exitLoop = false; 146 KAPITEL 6. ALGORITHMEN AUF ARRAYS while (! exitLoop) { k++; // pivot search for entry in remaining matrix // (depends on chosen method in switch) // store position in pivotRow, pivotCol MatrixPosition pivotPos = new MatrixPosition(0, 0); MatrixPosition currPos = new MatrixPosition(k, k); switch (method) { case 0 : pivotPos = nonZeroPivotSearch(k); break; case 1 : pivotPos = totalPivotSearch(k); break; } pivotRow = pivotPos.rowPos; pivotCol = pivotPos.colPos; pivot = coeff[row[pivotRow]][col[pivotCol]]; // permute rows and colums to get this entry onto // the diagonal permutePivot(pivotPos, currPos); // // // // // if test conditions for exiting loop after this iteration reasons are: Math.abs(pivot) == 0 k == m - 1 : no more rows k == n - 1 : no more colums ((Math.abs(pivot) == 0) ||( k == m - 1) || (k == n - 1)) { exitLoop = true; } // update rank if (Math.abs(pivot) > 0) { rank++; } // pivoting only if Math.abs(pivot) > 0 // and k < m - 1 if ((Math.abs(pivot) > 0) && (k < m - 1)) { 6.2. LINEARE GLEICHUNGSSYSTEME 147 pivotOperation(k); } }//end while triangular = true; } /** * method for total pivot search * * @param k search starts at entry (k,k) * @return the position of the found pivot element */ private MatrixPosition totalPivotSearch(int k){ double max = 0; int i, j, pivotRow = k, pivotCol = k; double absValue; for (i = k; i < coeff.length; i++) { for (j = k; j < coeff[0].length; j++) { // compute absolute value of // current entry in absValue absValue = Math.abs(coeff[row[i]][col[j]]); // compare absValue with value max // found so far if (max < absValue) { // remember new value and position max = absValue; pivotRow = i; pivotCol = j; }//end if }//end for j }//end for k return new MatrixPosition(pivotRow, pivotCol); } /** * method for trivial pivot search, searches for non-zero entry * * @param k search starts at entry (k,k) * @return the position of the found pivot element */ private MatrixPosition nonZeroPivotSearch(int k){ 148 KAPITEL 6. ALGORITHMEN AUF ARRAYS int i, j; double absValue; for (i = k; i < coeff.length; i++) { for (j = k; j < coeff[0].length; j++) { // compute absolute value of // current entry in absValue absValue = Math.abs(coeff[row[i]][col[j]]); // check if absValue is non-zero if (absValue > 0) { // found a pivot element return new MatrixPosition(i, j); }//end if }//end for j }//end for k return new MatrixPosition(k, k); } /** * permutes two matrix rows and two matrix columns * * @param pos1 the fist position for the permutation * @param pos2 the second position for the permutation */ private void permutePivot(MatrixPosition pos1, MatrixPosition pos2) { int r1 = pos1.rowPos; int c1 = pos1.colPos; int r2 = pos2.rowPos; int c2 = pos2.colPos; int index; index = row[r2]; row[r2] = row[r1]; row[r1] = index; index = col[c2]; col[c2] = col[c1]; col[c1] = index; } /** * performs a pivot operation * * @param k pivoting takes place below (k,k) */ private void pivotOperation(int k) { double pivot = coeff[row[k]][col[k]]; for (int i = k + 1; i < coeff.length; i++) { // compute factor double q = coeff[row[i]][col[k]] / (double) pivot; // modify entry a[i,k], i > k 6.2. LINEARE GLEICHUNGSSYSTEME 149 coeff[row[i]][col[k]] = 0; // modify entries a[i,j], i > k fixed, j = k+1...n-1 for (int j = k + 1; j < coeff[0].length; j++) { coeff[row[i]][col[j]] = coeff[row[i]][col[j]] - coeff[row[k]][col[j]] * q; }//end for j // modify right-hand-side rhs[row[i]] = rhs[row[i]] - rhs[row[k]] * q; }//end for k } /** * solves linear system with the chosen method * @param method the pivot search method */ private void solve(int method) throws NullPointerException{ if (solved) { return; // solution exists } if (! triangular) { triangularForm(method); } if (! isSolvable(method)) { return; } int n = coeff[0].length; double[] x = new double[n]; // set x[rank] = ... = x[n-1] = 0 if (rank < n) { for (int j = rank; j < n ; j++) { x[col[j]] = 0; } }//end if // compute x[rank-1] x[col[rank-1]] = rhs[row[rank-1]] / (double) coeff[row[rank-1]][col[rank-1]]; // compute remaining x[i] backwards 150 KAPITEL 6. ALGORITHMEN AUF ARRAYS for (int i = rank - 2; i >= 0; i--) { x[col[i]] = rhs[row[i]]; for (int j = i + 1; j <= rank-1; j++) { x[col[i]] = x[col[i]] - coeff[row[i]][col[j]] * x[col[j]]; }//end for j x[col[i]] = x[col[i]] / (double) coeff[row[i]][col[i]]; }//end for i solution = x; solved = true; } /** * solves linar system by total pivot search */ public void solveByTotalPivotSearch() throws NullPointerException { solve(1); } /** * solves linar system without pivot search */ public void solveWithoutPivotSearch() throws NullPointerException { solve(0); } /** * checks solvability of linar system with the chosen method * @param method the pivot search method * @return true if linear system in solvable */ private boolean isSolvable(int method) throws NullPointerException { if (solved) { return solvable; } if (! triangular) { triangularForm(method); } for (int i = rank; i < rhs.length; i++) { if (Math.abs(rhs[row[i]]) > 0) { solvable = false; return false; // not solvable 6.2. LINEARE GLEICHUNGSSYSTEME }// end if }// end for solvable = true; return true; } /** * checks if a solved system is solvable * @return true if linear system is solved and solvable */ public boolean isSolvable() { return solvable && solved; } /** * returns the solution * @return <code>double</code> array representing a solution */ public double[] getSolution() { return solution; } /** * returns current matrix (A|b) as String * @return String representing current matrix */ public String equationsToString() throws NullPointerException{ if ((coeff == null) || (rhs == null) || (row == null) || (col == null)) { throw new NullPointerException(); } StringBuffer strBuf = new StringBuffer(); String str = " "; for (int j = 0; j < coeff[0].length; j++) { str = str + col[j] + " "; } strBuf.append(str + "\n"); for (int i = 0; i < coeff.length; i++) { str = "" + row[i] + ":"; for (int j = 0; j < coeff[0].length; j++) { str = str + " " + coeff[row[i]][col[j]]; } str = str + " " + rhs[row[i]]; 151 152 KAPITEL 6. ALGORITHMEN AUF ARRAYS strBuf.append(str + "\n"); } return strBuf.toString(); } /** * returns solution as String * @return string representing solution vector */ public String solutionToString() throws NullPointerException{ if (solution == null) throw new NullPointerException(); StringBuffer strBuf = new StringBuffer(); for (int j = 0; j < solution.length; j++) { strBuf.append("x_" + j + " = " + solution[j] + "\n"); } return strBuf.toString(); } } /** * class for matrix positions */ public class MatrixPosition { int rowPos; int colPos; /** * Constructor * @param i the row position * @param j the column position MatrixPosition(int i, int j) { rowPos = i; colPos = j; } } /** * class for dimension exceptions */ import java.lang.*; // for class Exception 6.2. LINEARE GLEICHUNGSSYSTEME 153 public class DimensionException extends Exception { /** * Constructs a <code>DimensionException</code> * with no detail message. */ public DimensionException() { super(); } /** * Constructs a <code>DimensionException</code> with the * specified detail message. * * @param s the detail message. */ public DimensionException(String s) { super(s); } } Das Applet Gauss.java verwendet diese Klassen zur Implementation eines interaktiven Lösers für lineare Gleichungssysteme. Das Layout ist in Abbildung 6.10 dargestellt. Programm 6.5 Gauss.java /** * Gauss.java * * demonstrates the use of Gauss’ algorithms for solving linear equations * * Assumptions: * Input is per row, row i contains coefficients a_ij and * the righthandside b_j as last number, * missing a_ij are interpreted as 0 * numbers in a row are separated by white space */ import java.awt.*; import java.applet.Applet; import java.util.*; // for class StringTokenizer import java.awt.event.ActionListener; import java.awt.event.ActionEvent; 154 KAPITEL 6. ALGORITHMEN AUF ARRAYS Abbildung 6.10: Applet zur Lösung linearer Gleichungssysteme. public class Gauss extends Applet { private private private private private TextArea input, output; Button startBtn, helpBtn; Choice choiceBtn; Panel p1, p2, p3; String helpStr = "Bitte oben Kooeffizienten a_ij und rechte Seite b_i \n" + "zeilenweise eingeben.\n" 155 6.2. LINEARE GLEICHUNGSSYSTEME + + + + + + "Die letze Zahl der Zeile i ist die rechte Seite b_i,\n" "die Zahlen davor als a_i1, a_i2, ...\n" "Fehlende a_ij in Zeile i werden als 0 interpretiert.\n" "Das eingegebene Gleichungssystem wird mit \n" "der gewählten Pivotsuche gelöst. \nDabei wird die Zeit " "gemessen und ausgegeben.\n"; public LinearEquation linEq = new LinearEquation(); // setup the graphical user interface components public void init() { setLayout(new FlowLayout(FlowLayout.LEFT)); setFont(new Font("Times", Font.PLAIN, 12)); Font courier = new Font("Courier", Font.PLAIN, 12); p1 = new Panel(); p2 = new Panel(); p3 = new Panel(); p1.setLayout(new FlowLayout(FlowLayout.LEFT)); input = new TextArea(" 1 0 1 0 + " 1 1 0 1 + " 1 1 2 0 + " 1 0 0 1 input.setFont(courier); p1.add(input); // put input 4 7 6 5 \n" \n" \n" "); on panel p2.setLayout(new FlowLayout(FlowLayout.LEFT)); helpBtn = new Button(" Hilfe "); helpBtn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ showHelp(); } }); p2.add(helpBtn); choiceBtn = new Choice(); choiceBtn.addItem("ohne Pivotsuche"); choiceBtn.addItem("mit totaler Pivotsuche"); p2.add(choiceBtn); startBtn = new Button(" Start "); 156 KAPITEL 6. ALGORITHMEN AUF ARRAYS startBtn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ runGauss(); } }); p2.add(startBtn); p3.setLayout(new FlowLayout(FlowLayout.LEFT)); output = new TextArea(helpStr, 15, 60); output.setFont(courier); p3.add(output); add(p1); add(p2); add(p3); } /** * reads numbers from input to arrays a and b and to precision */ private void initialize() throws NumberFormatException, NoSuchElementException, DimensionException, NullPointerException { String inputStr = input.getText(); // divide input into lines StringTokenizer inputLines = new StringTokenizer(inputStr, "\n\r"); // get the number of rows if (! inputLines.hasMoreTokens()) { throw new NoSuchElementException(); } int m = inputLines.countTokens(); StringBuffer strBuf = new StringBuffer(); strBuf.append(m + " Gleichungen"); // define array of m lines of tokens StringTokenizer[] line = new StringTokenizer[m]; for (int i = 0; i < m; i++) { line[i] = new StringTokenizer(inputLines.nextToken()); } 6.2. LINEARE GLEICHUNGSSYSTEME // get the number of columns including b int n = 0; for (int i = 0; i < m; i++) { if (line[i].countTokens() < 2) { throw new NoSuchElementException(); } if (line[i].countTokens() - 1 > n) { n = line[i].countTokens() - 1; } } strBuf.append(" und " + n + " Variable\n"); // initialize linEq double[][] a = new double[m][n]; double[] b = new double[m]; for (int i = 0; i < m; i++) { int j; int k = line[i].countTokens() - 1; // that many coeffs on line i for (j = 0; j < k; j++) { a[i][j] = Double.parseDouble( line[i].nextToken()); } for (j = k; j < n; j++) { a[i][j] = 0; } b[i] = Double.parseDouble(line[i].nextToken()); } linEq = new LinearEquation(a, b); // write info in output field output.setText(strBuf.toString()); } /** * displays help text */ public void showHelp() { output.setText(helpStr); } 157 158 KAPITEL 6. ALGORITHMEN AUF ARRAYS /** * solves system of linear equations by Gaussian elimination * * @see <code>LinearEquationSolver</code> */ public void runGauss() { try { output.setText("Rechne ...\n"); initialize(); int choice = choiceBtn.getSelectedIndex(); String choiceStr = choiceBtn.getSelectedItem(); long startTime = System.currentTimeMillis(); switch (choice) { case 0 : linEq.solveWithoutPivotSearch(); break; case 1 : linEq.solveByTotalPivotSearch(); break; } long endTime = System.currentTimeMillis(); long runTime = endTime - startTime; // Construct the output string in a // StringBuffer object StringBuffer outputBuf = new StringBuffer(); outputBuf.append("Benötigte Zeit " + choiceStr + " in Millisekunden: " + Long.toString(runTime) + "\n"); outputBuf.append("solvable = " + linEq.isSolvable() + "\n"); outputBuf.append(linEq.equationsToString() + "\n"); if (linEq.isSolvable()) { outputBuf.append(linEq.solutionToString()); } output.append("\n" + outputBuf.toString()); } catch (NoSuchElementException exp) { output.append("\nJede Zeile muss mindestens " + "2 Zahlen enthalten."); } catch (NumberFormatException exp) { 6.2. LINEARE GLEICHUNGSSYSTEME output.append("\nNur double Zahlen eingeben."); } catch (DimensionException exp) { output.append(exp.toString()); } catch (Exception exp) { // other exceptions output.append(exp.toString()); } } } 159 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 151 6.3160 Kürzeste Wege in gerichteten Graphen KAPITEL 6. ALGORITHMEN AUF ARRAYS 6.3.1 Wege 6.3 Graphen Kürzesteund Wege in gerichteten Graphen Ein gerichteter Graph oder Digraph (directed graph) ist ein Paar G = (V, E) bestehend aus 6.3.1 Graphen und Wege gerichteter Digraph E) bestehend aus –Ein Der Menge Graph V der oder Knoten. Bei(directed uns istgraph) meististVein=Paar {1, G 2, = . . (V, . , n} oder (in Implementationen) V = {0, 1, . . . , n − 1}. – Der Menge V der Knoten. Bei uns ist meist V = {1, 2, . . . , n} oder (in Implementationen) V = 1, . . . , nE − 1}. – Der{0, Menge der (gerichteten) Kanten oder Bögen zwischen Knoten. Bei uns ist E ⊆ V × V \ {(i, i)|i ∈ V }; e = (i, j) bedeutet, daß die Kante e vom Knoten i zum – Der Menge E der (gerichteten) Kanten oder Bögen zwischen Knoten. Bei uns ist E ⊆ V ×V \ Knoten j∈ gerichtet ist. {(i, i)|i V }; e = (i, j) bedeutet, dass die Kante e vom Knoten i zum Knoten j gerichtet ist. Beispiel 6.4 E)mit mit V {1, = 2, {1, 3, 4, 5} und Beispiel 6.4 GG= = (V, (V, E) V= 3,2, 4, 5} und = {(1, 3),(2, (2,3), 3),(2, (2, 4), 1),1), (4,(4, 3), (5, E =E{(1, 2),2), (1,(1, 3), 4),(3, (3,5), 5),(4,(4, 3),4)} (5, 4)} ist ein Graphmit mit55 Knoten Knoten und 8 gerichteten Kanten. Er ist inEr Abbildung 6.11 dargestellt. ist ein Graph und 8 gerichteten Kanten. ist in Abbildung 6.11 dargestellt. 2 4 3 5 1 Abbildung 6.11: Zeichnung Graphen Beispiel Abbildung 6.11: Zeichnung des des Graphen ausaus Beispiel 6.11.6.11. Graphen habenviele vieleAnwendungen. Anwendungen. SieSie eignen sich sich zur Beschreibung von Graphen haben eignen zur Beschreibung von – Verbindungen zwischen Orten in einem Netzwerk (Straßennetz, U-Bahnnetz,. . .), – Verbindungen zwischen Orten in einem Netzwerk (Straßennetz, U-Bahnnetz,. . .), – Hierarchien, – Hierarchien, – Syntax- und Flussdiagrammen, – Syntax- und Flußdiagrammen, – Arbeitsabläufen, – Arbeitsabläufen, und vielem anderen mehr. solchen Anwendungen und In vielem anderen mehr.haben die Kanten (i, j) des Graphen meist eine Bewertung, die je nach Anwendung als Länge oder Entfernung von i nach j (Straßennetz), Kosten (Arbeitsablauf) oder ähnlich In solchen Anwendungen haben die Kanten (i, j) des Graphen meist eine Bewertung, die je interpretiert wird. nach Anwendung als Länge oder Entfernung von i nach j (Straßennetz), Kosten (Arbeitsablauf) oder ähnlich interpretiert wird. 161 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 152 KAPITEL 6. ALGORITHMEN AUF ARRAYS Ein solcher bewerteter Graph ist beschreibbar durch eine Matrix A = (ai j )i, j=1,...,n mit Ein solcher bewerteter Graph ist beschreibbar eine (i, Matrix A= mit (Länge) durch von Kante j) falls (i,(a j)ij∈)i,j=1,...,n E, Bewertung ai j = aij = 0 falls i = j, ∞ sonst. Bewertung (Länge) von Kante (i, j) falls (i, j) ∈ E, 0 falls i = j, ∞ sonst. Wir nennen A die Bewertungsmatrix des Graphen G. Zur Abspeicherung von A im Rechner wird statt 6 ∞Wir einenennen sehr große Zahl M (z. B. M := n des · max 1) verwendet. i j| + A die Bewertungsmatrix Graphen Zur Abspeicherung von A im Rechner (i, j)∈E |aG. wird statt ∞ eine sehr große Zahl M (z. B. M := n · max(i,j)∈E |aij | + 1) verwendet.6 Beispiel 6.5 (Fortsetzung von Beispiel 6.4) Für den Graphen aus Beispiel 6.4 legen wir die in AbBeispiel (Fortsetzung von Beispiel 6.4)Kantenbewertungen Für den Graphen aus Beispiel 6.4 legen wir bildung 6.126.5 links neben den Kanten angegebenen fest. Die zugehörige Matrix A die in Abbildung 6.12 links neben den Kanten angegebenen Kantenbewertungen fest. Die ist rechts daneben angegeben. zugehörige Matrix A ist rechts daneben angegeben. −1 2 −1 2 1 2 3 4 0 3 1 1 5 ⇒ A= 0 −1 ∞ 0 ∞ ∞ −1 ∞ ∞ ∞ 2 ∞ ∞ 2 3 ∞ 0 ∞ 1 0 0 ∞ ∞ 1 0 Abbildung 6.12: Bewerteter Graph aus Beispiel 6.12. Abbildung 6.12: Bewerteter Graph aus Beispiel 6.12. Falls nur der Graph G beschrieben werden soll (d. h. ohne Kantenbewertungen), so reicht auch die Matrix + 1 ohne (i, j) Kantenbewertungen), ∈ E, Falls nur der Graph G beschrieben werden soll (d. h. so reicht auch die A = (aij ) mit aij = 0 sonst. Matrix 1 (i, j) ∈ E, A = (ades ai j = G. In ihr ist durch Zugriff auf aij in koni j ) mit Diese Matrix heißt Adjazenzmatrix Graphen 0 sonst. stanter Zeit (d. h. unabhängig von der Größe des Graphen) feststellbar, ob i und j durch eine Matrix Kante verbunden sind. Dafürdes benötigt man quadratischen (n × n Zeit Diese heißt Adjazenzmatrix Graphen G. jedoch In ihr ist durch ZugriffSpeicherplatz auf ai j in konstanter Matrix). (d. h. unabhängig von der Größe des Graphen) feststellbar, ob i und j durch eine Kante verbunden Es sind auch andere Datenstrukturen zur Speicherung von (n Graphen möglich, z. B. für jeden sind. Dafür benötigt man jedoch quadratischen Speicherplatz × n Matrix). Knoten i eine Liste der Knoten j, die mit i durch eine Kante (i, j) verbunden sind. Diese Es sind auch andere Datenstrukturen zur Speicherung von Graphen möglich, z. B. für jeden Knoten benötigen weniger Platz, erfordern jedoch mehr Zeit, um festzustellen, ob (i, j) ∈ E ist oder i eine Liste der Knoten j, die mit i durch eine Kante (i, j) verbunden sind. Diese benötigen weninicht, da Listen nur sequentiellen Zugriff erlauben. ger Platz, erfordern jedoch mehr Zeit, um festzustellen, ob (i, j) ∈ E ist oder nicht, da Listen nur Bei manchenZugriff Anwendungen sequentiellen erlauben.spielen die Richtungen der Kanten keine Rolle. In diesem Fall ist mit einer Kante (i, j) auch die Kante (j, i) im Graphen, und beide haben dieselbe Bewer- Bei manchen spielen die Richtungen der Kanten keinegerichteten Rolle. In diesem tung. In derAnwendungen Darstellung des Graphen wird dann statt der beiden KantenFall eineist mit einer6 Wir Kante (i, j) auch die Kante ( j, i) im Graphen, und beide haben dieselbe Bewertung. In der werden hier nur diese einfache, aber viel Speicherplatz verbrauchende Datenstruktur verwenden. Eine Darstellung Graphen wirdindann dervon beiden Kantendieeine ungerichtete gezeichnet, oft benutzte des Alternative besteht einemstatt Array Listen.gerichteten Dabei entsprechen Indices der Komponenten den Knoten des Graphen, und die Komponente i enthält eine Liste aller Knoten j, zu denen von i aus eine 6 Wir werden Kante (i, j) existiert. hier nur diese einfache, aber viel Speicherplatz verbrauchende Datenstruktur verwenden. Eine oft benutzte Alternative besteht in einem Array von Listen. Dabei entsprechen die Indices der Komponenten den Knoten des Graphen, und die Komponente i enthält eine Liste aller Knoten j, zu denen von i aus eine Kante (i, j) existiert. 162 KAPITEL 6. ALGORITHMEN AUF ARRAYS s. Man spricht dann von ungerichteten Kanten. Ein Graph mit nur ungerichteten s R s statt also s I Kanten heißt ungerichteter Graph. Im folgenden werden wir die Kantenbewertung als Entfernung interpretieren, bzgl. der wir kürzeste Wege zwischen je zwei Knoten berechnen wollen. Ein Weg von i nach j ist eine endliche Folge von Knoten und Kanten i = i1 , (i1 , i2 ), i2 , (i2 , i3 ), . . . , ik−1 , (ik−1 , ik ), ik = j . i3 - p p p i1 - i2 - p p p p p i` p p p - ik−1 - ik p p p p p p p p p Dabei sind Knotenwiederholungen zugelassen, und auch der triviale Weg, der nur aus einem Knoten besteht (man “geht” dann von i nach i, indem man in i bleibt). Ein Weg heißt elementar, wenn keine Knotenwiederholungen auftreten. Ein Weg von i nach j heißt Zykel, falls i = j gilt. Die Länge eines Weges ist die Summe der Entfernungen der Kanten auf dem Weg. Die Länge des trivialen Weges ist 0. Ein Weg von i nach j heißt kürzester Weg, falls alle anderen Wege von i nach j eine mindestens ebenso große Länge haben. Unser Ziel ist nun die Berechnung der kürzesten Weglängen ui j zwischen je zwei Knoten i, j (mit ui j = ∞ falls kein Weg von i nach j existiert) sowie zugehöriger kürzester Wege. 6.3.2 Zwei konkrete Anwendungen Als erste Anwendung betrachten wir den Ausschnitt aus dem BVG-Netz von Berlin in Abbildung 6.13. Dieses Netz wird in Abbildung 6.14 als bewerteter ungerichteter Graph wiedergegeben, da jede Kante in beiden Richtungen “bereist” werden kann, und die Reisezeit für beide Richtungen gleich ist. Die Bewertung einer Kante ist für beide Richtungen gleich und entspricht der mittleren Reisezeit in Minuten (ohne Umsteigezeiten und Wartezeiten). Die Bewertungsmatrix A dieses Graphen ist in Tabelle 6.3 angegeben. Als kürzester Weg von der Yorkstraße zum Mathematikgebäude ergibt sich Yo −→ Be −→ Zoo −→ ER −→ MA mit der Länge 5 + 5 + 2 + 5 = 17 Minuten. Umsteige- und Wartezeiten können berücksichtigt werden, indem man die Stationen “aufbläht” zu mehreren Knoten, die den verschiedenen Linien entsprechen, und den Kanten dazwischen die Umsteigeund Wartezeiten zuordnet bzw. die Kantenbewertungen um diese Werte vergrößert. Dies ist in Abbildung 6.15 für die Station Zoologischer Garten ausgeführt. Bei Entfernungen als Kantenbewertungen treten nur nicht-negative Zahlen auf. Oft sind jedoch auch negative Kantenbewertungen sinnvoll, wie die folgende Anwendung des Devisentausches zeigt. 163 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 154 KAPITEL 6. ALGORITHMEN AUF ARRAYS S-Friedrichstr. MA-Gebäude Bismarckstr. S1 S-Tiergarten Fußwege ErnstReuter-Pl. Zoologischer Garten Wittenbergpl. U2 U1 Möckernbrücke Bus 119 U9 Yorckstr. U7 Berliner Str. Abbildung 6.13: Ausschnitt aus dem BVG-Netz. Abbildung 6.13: Ausschnitt aus dem BVG-Netz. Tabelle 6.3: Bewertungsmatrix des Graphen zum BVG-Netz. Bi ER 0 2 2 0 Bi ER ∞ 5 0∞ ∞ 2 2∞ ∞ 0 ∞ ∞ 52 7 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞∞ ∞ 2 MA ∞ 5 MA 0 ∞ 10 ∞ 5 11 0 ∞ 10 ∞ ∞ ∞ 11 ∞ Ti ∞ ∞ Ti 10 0∞ 7∞ 310 ∞0 ∞ 7 ∞ ∞3 Fr ∞ ∞ Fr ∞ 7∞ 0∞ ∞∞ ∞7 10 0 ∞ ∞∞ Zoo Be Yo ∞ 7 ∞ 2 ∞ ∞ Zoo∞ Be∞ 11 3 ∞ ∞ 7∞ ∞ 2 ∞ ∞10 0 11 5 ∞∞ 5 3 0 ∞5 ∞ 5 0 ∞ ∞ ∞ ∞ 2 2 0 ∞ 5 15 Mö ∞ ∞ Yo∞ ∞∞ ∞∞ ∞∞ ∞∞ 2 10 0 ∞7 Wi ∞ ∞ Mö Wi ∞ ∞∞ ∞ ∞∞ ∞ ∞2 ∞ ∞∞ ∞ 15 ∞ ∞ 7 ∞0 2 Tabelle 6.3: Bewertungsmatrix des Graphen zum BVG-Netz. Bi ER MA BiTi ERFr Zoo MA TiBe Yo Fr Mö Zoo Wi Be Yo Mö Wi 7 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ 10 ∞ ∞ 5 ∞ ∞ 2 0 5 ∞ ∞ 5 0 2 15 ∞ 2 0 7 ∞ 15 7 0 164 KAPITEL 6. ALGORITHMEN AUF ARRAYS 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 155 Fr 155 7 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN MA 5 Bi 2 Ti ER 11 MA 2 5 Bi 10 Fr 7 3 10 10 Zoo 2 Ti ER 2 11 3 2 Wi 7 10 Mö Zoo 7 2 15 Wi 7 2 7 5 Mö Yo 15 5 2 5 Be Yo Abbildung 6.14: Der Graph 5 für das BVG-Netz. Abbildung 6.14: Der Graph für das BVG-Netz. Ti Be ER 3 für das BVG-Netz. Abbildung 6.14: 2 Der Graph ER 10 Zoo Ti 10 5 2 2 3 Wi Zoo 10 5 10 5 2 Wi Be 5 Abbildung 6.15: Zoologischer Garten mit Umsteige- und Wartezeiten. Be Abbildung 6.15: Zoologischer Garten mit Umsteige- und Wartezeiten. Abbildung 6.15: Zoologischer Garten mit Umsteige- und Wartezeiten. 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 165 Gegeben ist ein gerichteter Graph, bei dem die Knoten Devisenbörsen darstellen und eine Kante (i, j) dem Tausch von Währung i in die “Zielwährung” j entspricht. Die Bewertung der Kante (i, j) gibt den Preis (Kurs) für eine Einheit der Zielwährung j bzgl. der Währung i an. Daher haben beide Kantenrichtungen unterschiedliche Werte. Ein Beispiel ist in Abbildung 6.16 angegeben (Kurse vom November 1998). Abbildung 6.16: Ein Graph für den Devisentausch (Kurse November 2003). In dieser Anwendung möchte man für gegebene “Heimatwährung” i und Zielwährung j eine Umtauschfolge (also einen Weg) bestimmen, so dass das Produkt der Bewertungen entlang des Weges (also der Preis für eine Einheit der Zielwährung) möglichst klein wird. Die Problem lässt sich folgendermaßen auf ein Kürzeste-Wege Problem transformieren. Ersetze die Kantenbewertung ai j der Kante (i, j) durch log ai j . Dann ist (nach den Logarithmengesetzen) das Produkt entlang eines Weges gleich der Summe der Logarithmen der Kanten. Man erhält also ein Kürzestes Wege Problem mit āi j := log ai j als Bewertung der Kanten. Dabei können negative Bewertungen āi j auftreten, nämlich genau dann, wenn ai j < 1 ist. Als Konsequenz negativer Kantenbewertungen können auch Zykel negativer Länge auftreten. Diese machen, wie wir sehen werden, bei der Berechnung kürzester Wege Schwierigkeiten. Beim Devisentausch haben sie jedoch eine besondere Bedeutung: Sie entsprechen einem Gewinn bringenden Zykel, auf dem man durch Umtausch seinen Einsatz vermehren kann. Im Graphen aus Abbildung 6.16 existiert ein solcher Zykel, vgl. Abbildung 6.17. Der Tausch entlang des Zykels ermöglicht den Kauf einer DM für 2,79·0,51·0,61 = 0,9960 DM, also eine Vermehrung des eingesetzten Kapitals um den Faktor 1/0,9960 ' 1,004161. Solche Zykel können tatsächlich bei Devisengeschäften vorkommen, allerdings nur kurzfristig, da sich Kurse dauernd aufgrund von Angebot und Nachfrage ändern. Die Rechner der Devisenhändler bemerken solche Zykel sofort und ordern entsprechend bis die steigende Nachfrage durch das Steigen der Preise den Faktor wieder größer als 1 werden lässt. 166 KAPITEL 6. ALGORITHMEN AUF ARRAYS Abbildung 6.17: Ein gewinnbringender Zykel beim Devisentausch. 6.3.3 Die Bellman Gleichungen Wir werden jetzt eine Methode kennen lernen, mit der man die kürzesten Weglängen (und auch die kürzesten Wege selbst) zwischen je 2 Knoten iterativ berechnen kann (sofern sie existieren). Die Grundidee basiert darauf, die Anzahl der Kanten auf den betrachteten Wegen in jeder Iteration um 1 zu vergrößern. Dazu definieren wir für i, j ∈ V und m ∈ N die Größe Länge eines kürzesten Weges von i nach j (m) mit höchstens m Kanten, falls dieser existiert, ui j := ∞ falls kein solcher Weg existiert. Diese Größe ist wohldefiniert, da es für festes m nur endlich viele Wege mit höchstens m Kanten zwischen i und j gibt, und somit unter diesen endlich vielen auch einen kürzesten. In Beispiel ?? ergeben sich für i = 1 und j = 5 folgende Werte: (1) (2) (3) (4) (5) u15 = ∞, u15 = 3, u15 = 2, u15 = 2, u15 = 2 . Allgemein gilt: Lemma 6.4 Für festes i und j und m ≥ 2 ist (1) (2) (m) (m+1) ui j ≥ ui j ≥ . . . ≥ ui j ≥ ui j . Beweis: Beim Übergang von m auf m + 1 vergrößert sich die Menge der Wege unter denen man einen (m) (m+1) kürzesten Weg ermittelt. Daher gilt ui j ≥ ui j . (m) Die ui j lassen sich durch Rekursionsgleichungen berechnen, die im folgenden Satz 6.4 angegeben sind. Sie besagen anschaulich, dass sich die kürzeste Länge eines Weges von i nach j mit höchstens 167 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN m + 1 Kanten aufspalten lässt in die kürzeste Länge eines Weges mit höchstens m Kanten von i bis zu einem Knoten k und die Länge ak j der Kante (k, j). Die Minimumsbildung in der Rekursionsgleichung dient dem Finden des richtigen Knoten k. Im Beweis dieses Satzes spielt das Prinzip der optimalen Substruktur eine wichtige Rolle. Es besagt in diesem Fall, das ein kürzester Weg zwischen zwei Knoten sich aus kürzesten Teilwegen zusammen setzt. Kurz: jeder Teilweg eines kürzesten Weges ist selber ein kürzester Weg zwischen den Endpunkten des Teilweges. Abbildung 6.18 veranschaulicht dieses Prinzip. Es gilt in verschiedenen Bereichen der Mathematik und gibt immer Anlass zu Algorithmen, die große“ optimale Strukturen aus optima” len Teilstrukturen zusammen setzen. i r s j kürzester Weg von i nach j ! r s kürzester Weg von r nach s Abbildung 6.18: Das Prinzip der optimalen Substruktur bei kürzesten Wegen. (m) Satz 6.4 (Bellman Gleichungen) Die ui j Bellman Gleichungen). (1) ui j = ai j , (m+1) ui j erfüllen die folgenden Rekursionsgleichungen (die sog. (m) = min [uik + ak j ] für m ≥ 1 . k=1,...,n Beweis: Für m = 1 kommen nur Wege mit höchstens einer Kante in Frage. Für i 6= j also gerade die Länge der Kante (i, j), falls diese existiert, bzw. ∞ andernfalls. Für i = j kommt nur der triviale Weg (1) mit 0 Kanten in Frage. Also gilt in allen Fällen ui j = ai j . (m+1) Betrachte nun ui j . Wir zeigen zunächst (m+1) ui j (m) ≥ min [uik + ak j ] . k=1,...,n (m+1) Diese Ungleichung ist trivialerweise erfüllt, falls ui j höchstens m + 1 Kanten existiert. (6.1) = ∞ ist, also kein Weg von i nach j mit Also nehmen wir an, dass ein solcher Weg existiert. Sei dann W ein kürzester Weg von i nach j mit höchstens m + 1 Kanten (dieser existiert, da wegen der Beschränkung auf maximal m + 1 Kanten nur endlich viele Wege in Frage kommen). Wir unterscheiden den Fall, dass W keine bzw. mindestens eine Kante enthält. (1) W habe mindestens eine Kante, etwa 168 KAPITEL 6. ALGORITHMEN AUF ARRAYS - i1 - i2 - p p p p p p - ` - j ip i p p p? W := (m+1) Dann hat W die Länge ui j . (Dabei kann W einen Zykel enthalten oder auch i = j sein, so dass der ganze Weg einen Zykel bildet.) Das Anfangsstück i1 - i2 - p p p W 0 := ip p p p - ` i - p p p? von W ist ein kürzester Weg von i nach ` mit höchstens m Kanten; denn gäbe es einen kürzeren, so würde man durch Anhängen der Kante (`, j) an W 0 einen kürzeren Weg mit höchstens m + 1 Kanten von i nach j erhalten, im Widerspruch zur Annahme, dass W ein kürzester Weg von i nach j ist. (m) Also ist die Länge von W 0 gleich ui` und somit die Länge von W gleich (m+1) (m) (m) = ui` + a` j ≥ min [uik + ak j ] , ui j k=1,...,n da ` bei der Minimumsbildung vorkommt. Also ist Ungleichung (6.1) erfüllt. (2) Hat W keine Kanten, so ist i = j und W besteht nur aus dem einzigen Knoten i = j. Dann ist seine (m+1) (m+1) (m) (1) (m) Länge uii = 0. Aus Lemma 6.4 folgt uii ≤ uii ≤ uii = aii = 0 und somit uii = 0. Also gilt (m+1) uii (m) (m) = uii + aii ≥ min [uik + ak j ] , k=1,...,n da i bei der Minimumsbildung vorkommt. Folglich gilt Ungleichung (6.1) auch in diesem Fall. Jetzt zeigen wir umgekehrt die Ungleichung (m+1) ui j (m) ≤ min [uik + ak j ] . k=1,...,n (6.2) Diese Gleichung ist wieder trivialerweise erfüllt, falls die rechte Seite ∞ ist. Ist sie endlich, so sei r der Index, für den das Minimum angenommen werde, d. h. (m) (m) min [uik + ak j ] = uir + ar j . k=1,...,n Dabei unterscheiden wir die Fälle r 6= j bzw. r = j. (m) (m) (1) Sei r 6= j. Da uir + ar j < ∞, sind uir < ∞ und ar j < ∞. Also existiert ein kürzester Weg W= - j1 - j2 - p p p jq p p p - r i p p p? (m) von i nach r mit höchstens m Kanten und Länge uir und es existiert wegen r 6= j und ar j < ∞ im Graphen die Kante (r, j). Der Weg W kann Zykel enthalten und es kann auch i = r sein (dann hat W entweder 0 Kanten oder der gesamte Weg W bildet einen Zykel). Das folgende Argument gilt für all diese Fälle. Da die Kante (r, j) existiert, ist 169 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN j1 - j2 - p p p W 0 := p p p - r - j jq i - p p p? (m) ein Weg von i nach j mit höchstens m + 1 Kanten und seine Länge ist uir + ar j . Diese kann natürlich nicht kleiner als die kürzeste Länge eines Weges von i nach j mit höchstens m + 1 Kanten sein, d. h. (m) (m+1) uir + ar j ≥ ui j . Also gilt Ungleichung (6.2). (2) r = j. Dann ist (m) (m) (m) (m+1) min [uik + ak j ] = ui j + a j j = ui j ≥ ui j k=1,...,n wegen a j j = 0 und Lemma 6.4. Aus den Ungleichungen (6.1) und (6.2) folgt die Behauptung. 6.3.4 Der Einfluss negativer Zykel Es stellt sich nun die Frage, wie groß man die Anzahl m der Kanten maximal wählen muss, um kürzeste Weglängen zu berechnen. Hier gibt es Schwierigkeiten falls der Graph Zykel negativer Länge (kurz: negative Zykel) enthält. In diesem Fall können die Wege mit höchstens m Kanten einen solchen (m) Zykel (sogar mehrfach) enthalten, so dass das Ergebnis ui j keinem elementaren Weg (also einem ohne Knotenwiederholungen) mehr entspricht, wie das folgende Beispiel zeigt. Beispiel 6.6 (Der Einfluss negativer Zykel) Betrachte den Graph aus Abbildung 6.19. Um die kürzeste Weglänge von 1 nach n zu berechnen, müssen m := n − 1 Kanten berücksichtigt werden. Dann gibt (m) (m) zwar u1n die richtige kürzeste Weglänge an, aber die Werte u1 j sind für j = 2, . . . , n − 2 ungleich der kürzesten Länge eines elementaren Weges von 1 nach j (die 0 beträgt). Zum Beispiel ergibt sich für j=4 n−3 (3) (4) (5) (6) (7) (8) (n−1) u14 = u14 = 0, u14 = u14 = −1, u14 = u14 = −2, . . . , u14 = − . 2 Die Ursache ist natürlich der negative Zykel 2 −→ 3 −→ 2 der Länge −1. −1 ? 1 - 2 0 0 - 3 0 - 4 0 - p p p 0 Abbildung 6.19: Ein Graph mit einem Zykel negativer Länge. - n 170 KAPITEL 6. ALGORITHMEN AUF ARRAYS Wir untersuchen daher zunächst den Fall, dass der gegebene Graph G keine negativen Zykel hat. Dies lässt immer noch negative Kantenbewertungen zu, nur dürfen sich diese nicht entlang eines Zykels zu einer negativen Zahl summieren. Lemma 6.5 Hat G keinen negativen Zykel und gibt es einen Weg von i nach j, so existiert ein kürzester Weg von i nach j, der elementar ist. Beweis: Da G keine negativen Zykel enthält, kann die Wegnahme von Zykel aus einem Weg die Weglänge höchstens verkürzen. Also kann man sich bei der Suche nach kürzesten Wegen auf elementare Wege beschränken. Da es nur endlich viele elementare Wege von i nach j gibt, existiert hierunter auch ein kürzester. Wir definieren nun Länge eines kürzesten elementaren Weges von i nach j falls kein Weg von i nach j existiert, ui j := ∞ falls ein Weg von i nach j existiert. Dann gilt (vgl. Lemma 6.4): Satz 6.5 Hat G keine negativen Zykel, so ist für festes i und j (1) (2) (n−1) ui j ≥ ui j ≥ . . . ≥ ui j = ui j . Die Bellman Gleichungen berechnen also in n − 2 Iterationen die kürzesten Weglängen ui j . (m) Beweis: Da G keine negativen Zykel hat, folgt aus Lemma 6.5, dass ui j nie kleiner als ui j werden kann. Elementare Wege können bei n Knoten höchstens n − 1 Kanten haben. Die Monotonieeigen(m) schaft der ui j aus Lemma 6.4 ergibt dann die Behauptung. 6.3.5 Der Bellman-Ford Algorithmus Satz 6.5 lässt sich direkt in den folgenden Algorithmus umsetzen, der nach seinen Entdeckern BellmanFord Algorithmus genannt wird. Wir geben ihn direkt als Java Methode an, die aus der Entfernungsmatrix A des Graphen die Matrix U der kürzesten Weglängen ermittelt und zurück gibt. Dabei speichert (m) (m+1) u die ui j ab und dient temp zur Berechnung der ui j für festes i. Programm 6.6 bellman public double[][] bellman(double[][] a){ 171 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN int n = a.length; // number of nodes double[][] u = new double[n][n]; // initialize u for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { u[i][j] = a[i][j]; } } // main loop for (int m = 2; m < n; m++){ for (int i = 0; i < n; i++) { double[] tmp = new double[n]; // tmp array // for calculating min{...} in Bellman equation for (int j = 0; j < n; j++) { // save current distance from i to j tmp[j] = u[i][j]; for (int k = 0; k < n; k++) { if (tmp[j] > u[i][k] + a[k][j]) { tmp[j] = u[i][k] + a[k][j]; } }// end for k }// end for j // store tmp in u[i] for ( int j = 0; j < n; j++ ) { u[i][j] = tmp[j]; } }// endfor i }// endfor m return u; } Die Korrektheit des Bellman-Ford Algorithmus folgt direkt aus Satz 6.5 und den Bellman Gleichungen (Satz 6.4). Für den Aufwand überlegt man sich aus der Schachtelung der Schleifen: Satz 6.6 Der Bellman-Ford Algorithmus in der Version von Programm 6.6 benötigt (n − 2) · n · n · n Vergleiche und höchstens (n − 2) · n · (n(1 + n) + 1) Zuweisungen, d. h. seine Laufzeit liegt in der Größenordnung von n4 Operationen. (m) Ist man nur an den ui j interessiert, nicht jedoch an den ui j (d. h. den kürzesten Weglängen mit (m) höchstens m Kanten), so kann man wegen der Monotonieeigenschaft der ui j in u[i,j] direkt jedesmal den Wert von temp[j] abspeichern. Dann gilt nach der m-ten Iteration der äußeren Schleife (m) (m+`) ui j ≥ u[i,j] = ui j ≥ ui j 172 KAPITEL 6. ALGORITHMEN AUF ARRAYS für ein ` mit 0 ≤ ` ≤ n − 1 − m. Dies liegt daran, dass kürzere Weglängen mit einer Kante mehr direkt in u[i,j] abgespeichert werden und für weitere Rechnungen schon benutzt werden. Dies hat im Programm den Vorteil, dass die Zwischenspeicherung in temp überflüssig wird und da(m) durch weniger Zuweisungen nötig sind. Dafür wird in der m-ten Iteration jedoch nicht ui j berechnet, (m) (n−1) sondern ein Wert zwischen ui j und ui j = ui j . Der Algorithmus vereinfacht sich dann zu Programm 6.7 bellmanShort public double[][] bellmanShort(double[][] a){ int n = a.length; // number of nodes double[][] u = new double[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { u[i][j] = a[i][j]; } for ( int m = 2; m < n; m++ ) { for ( int i = 0; i < n; i++ ) { for ( int j = 0; j < n; j++ ) { for ( int k = 0; k < n; k++ ) { if ( u[i][j] > u[i][k] + a[k][j] ) { u[i][j] = u[i][k] + a[k][j]; } } } } } return u; } Dieser Algorithmus hat allerdings immer noch eine Laufzeit von der Größenordnung n4 . Tatsächlich gibt es, wenn man nur an den ui j interessiert ist, andere einfache Algorithmen, deren Laufzeit von der Größenordnung n3 ist, vgl. die Literaturhinweise am Ende des Kapitels. Beispiel 6.7 (Fortsetzung von Beispiel ??) Für den Graphen aus Beispiel ?? sollen die kürzesten Weglängen mit dem Bellman-Ford Algorithmus berechnet werden (in der Version von Programm 6.6). (m) Dabei bezeichnet U (m) die Matrix (ui j )i, j=1,...,n und U 0 −1 ∞ 0 ∞ ∞ U (1) = A = −1 ∞ ∞ ∞ die Matrix (ui j )i, j=1,...,n . Gemäß Satz 6.4 ist 2 ∞ ∞ 2 3 ∞ 0 ∞ 1 0 0 ∞ ∞ 1 0 173 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN (2) Dann ergibt sich u11 aus den Bellman Gleichungen gemäß (2) u11 = (1) min [u1k + ak1 ] k=1,...,n = min[0 + 0, −1 + ∞, 2 + ∞, ∞ − 1, ∞ + ∞] = 0 . Dies entspricht einer Verknüpfung der ersten Zeile von U (1) mit der ersten Spalte von A. Diese Verknüpfung, wir wollen sie mit “⊗” bezeichnen, ist gerade die rechte Seite der Bellman Gleichungen, also 0 ∞ (1) (1) min [uk + ak1 ] . U1· ⊗ A·1 = (0, −1, 2, ∞, ∞) ⊗ ∞ = k=1,...,n −1 ∞ Entsprechend ist (2) u12 = = (2) u13 = = −1 0 (1) ∞ U1· ⊗ A·2 = (0, −1, 2, ∞, ∞) ⊗ ∞ ∞ min[0 − 1, −1 + 0, 2 + ∞, ∞ + ∞, ∞ + ∞] = −1 , 2 2 (1) U1· ⊗ A·3 = (0, −1, 2, ∞, ∞) ⊗ 0 0 ∞ min[0 + 2, −1 + 2, 2 + 0, ∞ + 0, ∞ + ∞] = 1 . (1) Hier wurde gegenüber u13 = 2 eine kürzere Weglänge über 2 Kanten ermittelt. Weiterhin ist ∞ 3 (1) U1· ⊗ A·4 = (0, −1, 2, ∞, ∞) ⊗ ∞ 0 1 min[0 + ∞, −1 + 3, 2 + ∞, ∞ + 0, ∞ + 1] = 2 , ∞ ∞ (1) U1· ⊗ A·5 = (0, −1, 2, ∞, ∞) ⊗ 1 ∞ 0 min[0 + ∞, −1 + ∞, 2 + 1, ∞ + ∞, ∞ + 0] = 3 . (2) u14 = = (2) u15 = = 174 KAPITEL 6. ALGORITHMEN AUF ARRAYS (2) Also ist die erste Zeile von U (2) gleich (0, −1, 1, 2, 3). Die anderen ui j berechnen sich entsprechend, z. B. 0 ∞ (2) (1) ∞ u21 = U2· ⊗ A·1 = (∞, 0, 2, 3, ∞) ⊗ −1 ∞ = min[∞ + 0, 0 + ∞, 2 + ∞, 3 − 1, ∞ + ∞] = 2 . Insgesamt erhält man U (2) = 0 −1 1 2 3 2 0 2 3 3 ∞ ∞ 0 2 1 −1 −2 0 0 1 0 ∞ 1 1 0 und schließlich U =U (4) = 0 −1 1 2 2 2 0 2 3 3 1 0 0 2 1 −1 −2 0 0 1 0 −1 1 1 0 . Das Beispiel zeigt, dass die Bellman Gleichungen eine starke Analogie zur Matrixmultiplikation aufweisen. Bei der Matrixmultiplikation C := A · B ist der Eintrag ci j der Ergebnismatrix C gegeben als n ci j = ∑ [aik · bk j ] . k=1 Hier ist “·” die innere und “+” die äußere Operation. In den Bellman Gleichungen C := A ⊗ B ist der Eintrag ci j der Ergebnismatrix C gegeben als ci j = min [aik + bk j ] , k=1,...,n also mit “+” als innerer und “min” als äußerer Operation. Speziell ist U (1) U (2) U (3) = A = U (1) ⊗ A = U (2) ⊗ A .. . = A⊗A = A⊗A⊗A U (n−1) = U (n−2) ⊗ A = A · · ⊗ A} =: An−1 . | ⊗ ·{z n−1 mal 175 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN Die Analogie zwischen Matrixmultiplikation und Bellman Gleichungen wird besonders deutlich, wenn man sich nur dafür interessiert, ob i und j mit einem Weg mit höchstens m Kanten verbun(m) den sind oder nicht. Dann kann ui j als Boolesche Variable definiert werden, d. h. true es gibt einen Weg von i nach j mit höchstens m Kanten, (m) ui j = false sonst. Ausgehend von A = (ai j ) mit ai j = erhält man (m+1) ui j true falls (i, j) ∈ E false sonst (m) (m) (m) = [ui1 ∧ a1 j ] ∨ [ui2 ∧ a2 j ] ∨ . . . ∨ [uin ∧ an j ] was genau der Matrixmultiplikation entspricht (∧ entspricht ·, ∨ entspricht +). 6.3.6 Die Ermittlung negativer Zykel Für die Berechnung der kürzesten Weglängen ui j hat es sich als notwendig erwiesen, dass der Graph G keine negativen Zykel enthält. Es stellt sich daher die Frage, wie man die Gültigkeit dieser Voraussetzung effizient überprüfen kann. Satz 6.7 (Test auf negative Zykel) Die folgenden Aussagen sind äquivalent: 1. Der Graph G enthält einen negativen Zykel. 2. Der Graph G enthält einen elementaren negativen Zykel. (n) 3. Es gibt einen Knoten i mit uii < 0 Insbesondere kann die Existenz negativer Zykel durch Überprüfen der Diagonalelemente in U (n) festgestellt bzw. ausgeschlossen werden. Beweis: (1) ⇒ (2): Sei Z ein negativer Zykel in G. Ist Z nicht bereits elementar, so durchlaufe man von einem beliebigen Startknoten aus Z solange, bis der erste Knoten zum zweiten Mal erreicht wird. Sei i dieser Knoten. Dann zerfällt Z in den elementaren Zykel Z1 von i nach i und den Rest Z2 , der ebenfalls ein Zykel ist, aber nicht notwendigerweise elementar. Die Länge von Z ist gleich der Summe der Längen von Z1 und Z2 . Also muss Z1 oder Z2 negativ sein. Ist es Z1 , so ist ein elementarer negativer Zykel gefunden. Ist es Z2 , so muss dasselbe Argument ggf. wiederholt werden. Da der Graph G endlich ist, endet man nach einer endlichen Anzahl von Anwendungen dieses Argumentes bei einem negativen elementaren Zykel. (2) ⇒ (3): Sei Z ein elementarer negativer Zykel in G und i ein Knoten in Z. Da Z elementar ist, enthält Z höchstens n Kanten (sonst würde ein Knoten doppelt vorkommen). Also gilt (n) uii ≤ Länge(Z) < 0, 176 KAPITEL 6. ALGORITHMEN AUF ARRAYS da Z einen Weg von i nach i mit höchstens n Kanten bildet. (n) (n) (3) ⇒ (1): Sei uii < 0. Nach Definition von uii existiert dann ein Weg von i nach i (also ein Zykel (n) Z) mit höchstens n Kanten, dessen Länge gleich uii ist. Also ist Z ein negativer Zykel (der trotz der Beschränkung auf n Kanten nicht elementar zu sein braucht). (m) Es existieren also genau dann negative Zykel, sobald ein uii < 0 wird für 1 ≤ m ≤ n. Da dies bei (m) der Berechnung von uii festgestellt werden kann, kann man abbrechen, sobald dies zum erstenmal eintritt, und eine entsprechende Fehlermeldung ausgeben. Der einzige Mehraufwand (neben der Überprüfung) besteht in einer zusätzlichen Iteration. Diese wird notwendig, da bei n Knoten ein elementarer Zykel maximal n Kanten haben kann (vgl. die zweite Richtung des Beweises). 6.3.7 Die Ermittlung kürzester Wege Bisher haben wir nur die kürzesten Weglängen ui j berechnet. Natürlich möchte man auch einen zugehörigen kürzesten Weg ermitteln. Dazu nutzt man die folgende, im Beweis von Satz 6.4 durchgeführte Überlegung aus: Bei der Berech(m+1) (m) (m+1) nung der Bellman Gleichungen ui j = mink [uik + ak j ] entspricht (im Fall ui j < ∞) der Index k0 , für den das Minimum angenommen wird, einem Knoten k0 , so dass (k0 , j) die letzte Kante auf einem kürzesten Weg von i nach j ist. Merkt man sich also im Bellman-Ford Algorithmus jeweils diesen Index, so lässt sich aus dieser Information ein kürzester Weg rekonstruieren. Die Realisierung im Programm erfolgt durch ein n × n Array int[][] tree; mit der Initialisierung tree[i][j] := i -1 falls (i, j) ∈ E . sonst In jedem Durchlauf der äußeren Schleife des Algorithmus wird dann tree[i][j] der Wert k zugewiesen, wenn bei u[i][j] eine Änderung auftritt und das zugehörige Minimum bei k angenommen wird. Der Algorithmus (in der Version von Programm 6.7) wird dann zu7 : Programm 6.8 bellmanTree public double[][] bellmanTree( double[][] a ){ int n = a.length; // number of nodes 7 In dieser Variante wird nur die tree-Matrix zurückgegeben. Natürlich würde man entweder eine geeignete ErgebnisKlasse definieren, die die Rückgabe von u und tree gleichzeitig erlaubt, oder analog zur Lösung linearer Gleichungssysteme in Abschnitt 6.2.5 Graphen als Objekte ansehen, die entsprechende Felder u und tree aktualisieren. 6.3. KÜRZESTE WEGE IN GERICHTETEN GRAPHEN 177 double[][] u = new double[n][n]; for ( int i = 0; i < n; i++ ) for ( int j = 0; j < n; j++ ) u[i][j] = a[i][j]; for ( int m = 2; m < n; m++ ) for ( int i = 0; i < n; i++ ) for ( int j = 0; j < n; j++ ) for ( int k = 0; k < n; k++ ) if ( u[i][j] > u[i][k] + a[k][j] ) { u[i][j] = u[i][k] + a[k][j]; tree[i][j] = k; } return tree; } Das Array tree enthält nach Ausführung der Methode die Information über die kürzesten Wege (falls keine negativen Zykel gefunden wurden). Um dies genauer zu erläutern, brauchen wir den Begriff des gerichteten Baums. Ein gerichteter Baum ist ein Digraph T = (V, E) mit folgenden Eigenschaften: – Es gibt genau einen Knoten r, in dem keine Kante endet (die Wurzel von T ). – Zu jedem Knoten i 6= r gibt es genau einen Weg von der Wurzel r zu i. Dies bedeutet, dass keine zwei Wege in den gleichen Knoten einmünden. Der Graph kann sich ausgehend von der Wurzel also nur verzweigen. Daher kommt auch der Name Baum. Satz 6.8 Hat G keine negativen Zykel, so ist bei Termination von Programm 6.8 für jeden Knoten i der Graph Ti := (V, Ei ) mit der Knotenmenge V = {0, . . . , n − 1} und der Kantenmenge Ei = {(tree[i][j],j) | j = 0, 1, . . . , n − 1; tree[i][j] 6= −1} ein gerichteter Baum mit i als Wurzel. Ein Weg von i nach j in Ti ist ein kürzester Weg von i nach j in G. Ti heißt daher auch KürzesterWege-Baum zum Knoten i. Beweis: Jeder Knoten j 6= i hat in Ti (wenn er von i aus in G erreichbar ist) genau einen Vorgängerknoten, nämlich tree[i][j]. Also können in Ti keine zwei Kanten in einem Knoten zusammentreffen. Daher existiert zu jedem Knoten j, der von i aus in G erreichbar ist, ein eindeutiger Weg von i nach j in Ti . Da G keinen negativen Zykel hat, hat i keinen Vorgängerknoten. Also bildet Ti einen Baum mit Wurzel i. Sei i = i0 , i1 , . . . , i`+1 = j die Folge der Knoten auf dem Weg von i nach j in Ti . Dann ist i` = tree[i][ j], i`−1 = tree[i][i` ], . . . , i = tree[i][i2 ] . 178 KAPITEL 6. ALGORITHMEN AUF ARRAYS - i1 - i2 - p p p i ai,i1 ai1 ,i2 ai2 ,i3 - i` - j ai`−1 ,i` ai` , j Da die Werte im Array tree gerade bei der Minimumsbildung aktualisiert werden, folgt ui j = ui,i` + ai` , j ui,i` = ui,i`−1 + ai`−1 ,i` .. . ui,i2 = ui,i1 + ai1 ,i2 ui,i1 = ai,i1 . Daher ist ui j = ai,i1 +ai1 ,i2 +. . .+ai`−1 , j und somit der Weg in Ti ein kürzester Weg von i nach j in G. Beispiel 6.8 (Fortsetzung von Beispiel ??) Für den Graphen aus Beispiel ?? erhält man −1 1 2 2 3 4 −1 2 2 3 1 −1 5 3 tree = 4 . 4 1 4 −1 3 4 1 4 5 −1 Als E1 ergibt sich aus der ersten Zeile von tree die Kantenmenge E1 = {(1, 2), (2, 3), (2, 4), (3, 5)} , und somit der in Abbildung 6.20 dargestellte Kürzeste-Wege Baum T1 . Die Kanten aus E1 entsprechen folgenden Bellman Gleichungen: (4) u11 = 0 (4) (3) u12 = u11 + a12 (4) (3) u13 = u12 + a23 (4) (3) u14 = u12 + a24 (4) (3) u15 = u13 + a35 6.4 k=1 k=2 k=2 k=3 j=2 j=3 j=4 j=5 Literaturhinweise Suchverfahren in Arrays werden in nahezu allen Büchern über Entwurf und Analyse von Algorithmen behandelt. Besonders ausführlich gehen [Knu98b, Meh88, OW02] hierauf ein. Die Lösung linearer Gleichungssysteme ist Gegenstand aller Bücher über lineare Algebra. Ein empfehlenswertes Buch, das lineare Algebra und Numerik verbindet, ist [GMW91]. Die Berechnung kürzester Wege findet sich sowohl in Büchern über Entwurf und Analyse von Algorithmen (z. B. in [CLRS01, OW02]), als auch in Büchern über Graphenalgorithmen und/oder kombinatorische Optimierung (etwa [Jun94]). Eine sehr gute Darstellung verschiedener kürzeste Wegealgorithmen gibt [Tar83]. 179 6.4. LITERATURHINWEISE −1 2 > 1 3 - 4 2 ? 3 1 - 5 Abbildung 6.20: Der Kürzeste-Wege Baum zum Knoten 1 in Beispiel 6.8.