Gedachte Darstellung des Gleichungssystems ist:
Die Koeffizienten aij des Systems aus n Gleichungen mit n Unbekannten liegen im Array A(1..n, 1..n), die rechten Seiten der Gleichung bi sind als (n+1)te Spalte in das Array A() aufgenommen. Die Lösung wird in den Vektor X(1..n) geschrieben. S ist eine Hilfsgröße (vom Typ Single), i, j, k sind Integer.
Bei diesem Algorithmus werden Spalte für Spalte (Zähler ist k) die jeweils unterhalb der Hauptdiagonalen stehenden Glieder eliminiert, indem von der zu behandelnden Gleichung ein geeignetes Vielfaches der k-ten Gleichung abgezogen wird. Es entsteht eine Dreiecksmatrix, die anschließend rückwärts aufgelöst wird.
| 'Beginn Gleichungs-System For k = 1 To n - 1 For i = k + 1 To n S = A(i, k) / A(k, k) A(i, k) = 0 For j = k + 1 To n + 1 A(i, j) = A(i, j) - S * A(k, j) Next Next NextX(n) = A(n, n + 1) / A(n, n) For i = n - 1 To 1 Step -1 S = A(i, n + 1) For j = i + 1 To n S = S - A(i, j) * X(j) Next NextX(i) = S / A(i, i) 'Ende Gleichungs-System |
Es handelt sich hier noch um den "Roh-Algorithmus", denn die Divisionen durch A(k,k) könnten schiefgehen (bei A(k,k)=0). Das würde den Programmabbruch zur Folge haben. An dieser Stelle ist eine Routine zum Auffangen des Fehlers erforderlich, weil eine Meldung allemal besser als ein Fehlerabbruch ist. Der Einbau von Fehlerroutinen gehört beim Hobbyprogrammierer eher zur "Schönheitspflege", im professionellen Bereich ist er natürlich Pflicht.
Wenn das GS nicht lösbar ist, was wir mal außen vor lassen wollen, kann das Verfahren ungenau werden oder, trotz Lösbarkeit versagen, wenn eines der Hauptdiagonalelemente der Matrix A(i,j) zu klein ist oder im Zuge der Elimination zu klein wird oder gar verschwindet.
Das Zauberwort heißt "Pivotsuche".
Man bestimmt vor jeder Eliminationsrunde das größte in Frage kommende Element der Restmatrix für die Division. Damit unter den Gleichungen die Chancen gleichmäßig verteilt sind, wird zuvor "normalisiert". Für die theoretischen Überlegungen dazu lese man die einschlägige Literatur. (Einschlägig heißt hier, daß das Lesen so viel Freude macht, wie eine Tracht Prügel zu empfangen).
Es folgt eine komplette Prozedur, n ist der Grad des Gleichungssystems, A () ist wie im oberen Beispiel belegt (Achtung, der Inhalt von A() wird verändert!), in x() wird die Lösung zurückgeliefert.
| Sub gauss(n%, A() As Double, x() As Double) Dim i%, j%, k%, jmax%, kmax%, merk() As Integer Dim s As Double, max As Double, skal() As Double ReDim merk(n), skal(n) 'Reihenfolge sichern For i = 1 To n merk(i) = i Next 'Normalisierung For i = 1 To n s = 0 For j = 1 To n s = s + Abs(A(i, j)) Next skal(i) = 1 / s Next 'Vorwärtselimination For k = 1 To n - 1 max = skal(k) * Abs(A(k, k)) kmax = k 'Spalte mit max jmax = k 'Zeile mit max For j = k To n 'Pivotzelle suchen For i = k To n If skal(j) * Abs(A(j, i))> max Then jmax = j kmax = i max = skal(j) * Abs(A(j, i)) End If Next Next If jmax <> k Then 'Zeilentausch, wenn nötig For j = k To n + 1 s = A(k, j) A(k, j) = A(jmax, j) A(jmax, j) = s Next s = skal(k) skal(k) = skal(jmax) skal(jmax) = s End If If kmax <> k Then 'Spaltentausch, wenn nötig For i = 1 To n s = A(i, k) A(i, k) = A(i, kmax) A(i, kmax) = s Next j = merk(k) merk(k) = merk(kmax) merk(kmax) = j End If 'eigentliche Elimination For i = k + 1 To n s = A(i, k) / A(k, k) A(i, k) = 0# For j = k + 1 To n + 1 A(i, j) = A(i, j) - s * A(k, j) Next Next Next 'Auflösung rückwärts x(merk(n)) = A(n, n + 1) / A(n, n) For i = n - 1 To 1 Step -1 s = A(i, n + 1) For j = i + 1 To n s = s - A(i, j) * x(merk(j)) Next x(merk(i)) = s / A(i, i) Next End Sub |
Wenn die Koeffizientenmatrix des GS symmetrisch ist, transformiert das Householder-Verfahren diese in tridiagonale Form, die sich dann leicht auflösen läßt. Der Vorteil ist hohe numerische Stabilität des Verfahrens; der Rechenaufwand etwa doppelt so hoch, wie beim Gauß-Verfahren.
| Sub Householder(n%, A() As Double, X() As Double) Dim i%, j%, k% Dim S#, T#, hlp#(20) 'Transformation der Koeffizientenmatrix For j = 1 To n S = 0 For i = j To n S = S + A(i, j) ^ 2 Next If S <> 0 Then If A(j, j) <0 Then hlp(j) = Sqr(S) Else hlp(j) = -Sqr(S) T = 1 / (hlp(j) * A(j, j) - S) A(j, j) = A(j, j) - hlp(j) For k = j + 1 To n + 1 S = 0 For i = j To n S = S + A(i, j) * A(i, k) Next S = S * T For i = j To n A(i, k) = A(i, k) + A(i, j) * S Next Next Else MsgBox "Householder wird abgebrochen" Exit Sub End If Next 'Auflösung X(n) = A(n, n + 1) / hlp(n) For i = n - 1 To 1 Step -1 S = 0 For j = i + 1 To n S = S + A(i, j) * X(j) Next X(i) = (A(i, n + 1) - S) / hlp(i) Next End Sub |
Beispiel für einen komplexeren mathematischen Algorithmus (wer ihn braucht, weiß wozu!), dynamische Variable, Prozeduraufrufe mit Variablenübergabe, Dimensionierung.
Es werden zunächst nur die Variablen für die Matrix und den Rang der Matrix deklariert. Dann folgt die eigentliche Matrixinversion. Sie benötigt eine kleine Tauschroutine, die ebenfalls gezeigt wird.
| Option Explicit Dim MatrixA() As Double, N As Integer Sub MatInv(Mat() As Double, N As Integer) Dim Hlp1() As Double, Hlp2() As Double, Hlp3() As Double ReDim Hlp1(N), Hlp2(N), Hlp3(N) Dim Max As Double, T As Double Dim i As Integer, j As Integer, k As Integer, ix As Integer, iy As Integer 'geht los For i = 1 To N Hlp3(i) = 0 Next For i = 1 To N 'welches ist das größte Element Max = 0 For j = 1 To N If Hlp3(j)<>1 Then For k = 1 To N If Hlp3(k)<>1 And Max<= Abs(Mat(j, k)) Then iy = k ix = j Max = Abs(Mat(j, k)) End If Next End If Next Hlp3(iy) = Hlp3(iy) + 1 'Pivotisierung If ix<>iy Then For j = 1 To N Tausche Mat(ix, j), Mat(iy, j) Next End If Hlp1(i) = ix Hlp2(i) = iy 'Kontrolle auf mögliches Verschwinden eines Hauptachsenwertes If Abs(Mat(iy, iy))<1E-300 Then MsgBox "Abbruch, Inversion nicht möglich!" Exit Sub Else T = Mat(iy, iy) Mat(iy, iy) = 1 For j = 1 To N Mat(iy, j) = Mat(iy, j) / T Next For j = 1 To N If j<>iy Then T = Mat(j, iy) Mat(j, iy) = 0 For k = 1 To N Mat(j, k) = Mat(j, k) - Mat(iy, k) * T Next End If Next End If Next 'Rücktausch For i = 1 To N j = N + 1 - i If Hlp1(j)<>Hlp2(j) Then ix = Hlp1(j) iy = Hlp2(j) For k = 1 To N Tausche Mat(k, ix), Mat(k, iy) Next End If Next End Sub'Hilfsspeicher freigeben ReDim Hlp1(0), Hlp2(0), Hlp3(0) 'Ausgabe .... Private Sub Tausche(A As Double, B As Double) Dim T As Double T = A End SubA = B B = T |
Das war es schon. Und was macht man mit einer Routine allein? Nicht mal testen kann man sie. Deshalb brauchen wir einen Aufruf, der beispielsweise eine Testmatrix mit Zufallszahlen belegt. Ob unsere Matrixinversion richtig rechnet, kann man bei der Gelegenheit ebenfalls testen, denn die Inverse der Inversen muß wieder die Ausgangsmatrix ergeben.
| Sub MatrixAufruf(N As Integer) Dim i As Integer, j As Integer, k As Long Dim MatrixB() As Double, Max As Double, erg As String ReDim MatrixA(N, N), MatrixB(N, N) For i = 1 To N For j = 1 To N MatrixA(i, j) = (Rnd - 0.5) * 1000000 MatrixB(i, j) = MatrixA(i, j) 'erg = erg & Format(MatrixB(i, j), "Scientific") & " " Next 'erg = erg & vbCrLf frmDocument.txtText.Text = i Next 'frmDocument.txtText.Text = erg MatInv MatrixA, N MatInv MatrixA, N Max = -1 For i = 1 To N For j = 1 To N If Abs(MatrixA(i, j) - MatrixB(i, j))>Max Then Max = Abs(MatrixA(i, j) - MatrixB(i, j)) 'erg = erg & Format(MatrixA(i, j) - MatrixB(i, j), "Scientific") & " " Next 'erg = erg & vbCrLf Next End SubfrmDocument.txtText.Text = erg & "Größter Fehler ist " & Max ReDim MatrixB(0, 0) |
Damit sich niemand wundert: Die auskommentierten Zeilen bauen einen String auf, der die Matrix selbst sowie die Matrix der Differenzen anzeigt. Für die Anzeige dient ein Textfeld namens txtText.
Eine wichtige Anwendung der Matrizeninversion ist die Lösung von linearen Gleichungssystemen. Dieses Lösungsverfahren hat ja den immensen Vorteil, sofern man nur die invertierte Matrix kennt, besonders elegant zu sein. Denn wenn Ax = b das Gleichungssystem in vektorieller Schreibweise beschreibt, dann ist x = A-1Ax = A-1b bereits die Lösung. Deshalb:
| Sub invmat(n%, a#(), x#()) Dim i%, j% MatInv n, a For i = 1 To n x(i) = 0 For j = 1 To n x(i) = x(i) + a(i, j) * a(j, n + 1) Next Next End Sub |
Iterative Verfahren zur Lösung linearer GS, von denen es gleich mehrere gibt, (Jacobi, Gauß-Seidel, SOR), beruhen auf der Überlegung, daß man das Gleichungssystem Ax = b (vektoriell geschrieben) schrittweise lösen kann, indem man, immer im Kreis,x(neu) = x(alt) - Korr rechnet, wobei Korr ein Korrekturvektor ist, der irgendwie aus der Differenz von Ax(alt) und b, also dem Fehler gerechnet wird, den der alte x-Vektor im GS erzeugt. Man beginnt mit geratenen Werten und wenn man einen guten Tag hat, dann verschwindet irgendwann das Korrekturglied. Das nennt man dann: "Die Iteration steht". Der absolute Glücksfall.
Die Gauß-Seidel-Iteration ist speziell für schlecht konditionierte GS geeignet. Sie reagiert zudem weniger empfindlich auf fehlerbehaftete Koeffizienten als das Gauß-Verfahren. Der Nachteil ist, daß natürlich die Rechenzeit erheblich größer ist, als bei direkten Algorithmen. Dazu vergleiche man den Test am Ende dieser Seite.
| Sub gaussseidel(n%, A() As Double, X() As Double) Dim xp(50) As Double Dim S As Double, T As Double Dim i%, j%, num As Long 'Beginn For i = 1 To n X(i) = 5 Next Do For i = 1 To n S = 0 For j = 1 To i - 1 S = S + A(i, j) * xp(j) Next T = 0 For j = i + 1 To n T = T + A(i, j) * X(j) Next xp(i) = (A(i, n + 1) - S - T) / A(i, i) Next S = 0 For i = 1 To n S = S + (X(i) - xp(i)) ^ 2 X(i) = xp(i) Next num = num + 1 Loop Until Sqr(S / (n - 1)) <0.000000001 End Sub |
Ein Vergleich der Leistungsfähigkeit der gezeigten Algorithmen ist deshalb nicht sehr sinnvoll, weil diese für unterschiedliche Anforderungen optimiert sind. Aber interessant ist es allemal. Ich wähle hier ein schwach konditioniertes Gleichungssystem, dessen Koeffizientenmatrix, die sog. "Hilbertmatrix" zudem symmetrisch ist.
| Sub hilberttest() Dim n%, i%, j%, S As Double n = CInt(ComboBox1.Text) 'Hilbertmatrix For i = 1 To n For j = 1 To n a(i, j) = 1 / (i + j - 1) Next Next 'rechte Seite, alle xi sind = 1 For i = 1 To n S = 0 For j = 1 To n S = S + a(i, j) Next a(i, n + 1) = S Next End Subgauss n, a, x holen n, a, b householder n, a, x holen n, a, b invmat n, a, x holen n, a, b gaussseidel n, a, x |
Die Koeffizientenmatrix mußte jeweils neu geholt werden, weil die Verfahren das aufrufende Array überschreiben (Bei Arrays gibt es leider keinen ByVal-Aufruf!)
zunächst 12 Gleichungen mit 12 Unbekannten
| Nr. | Gauß | Householder | Mat-Inv | Gauß-Seidel |
| x(1) x(2) x(3) x(4) x(5) x(6) x(7) x(8) x(9) x(10) x(11) x(12) | 0,999999994913475 1,00000062404417 0,999980756840177 1,00025909806798 0,998113734897155 1,00825643413237 0,977035467010419 1,04155259194011 0,95126025095053 1,03573507962894 0,985120426843695 1,00268554425617 | 0,999999981271439 1,00000235714355 0,999926170299648 1,0010035471671 0,99265315218153 1,03225647967565 0,910156747342672 1,16261083542408 0,809349307254756 1,13965007672443 0,941925053242107 1,01046630497711 | 0,999999902345735 1,00000980577897 0,999618638306856 1,00371235609055 0,975866317749023 1,01568412780762 0,658493041992188 1,49558258056641 0,746086120605469 1,11670684814453 0,875782012939453 1,02040243148804 | 0,999992870207552 1,00026940394669 0,997637348470586 1,00746409295183 0,992370964945155 0,997052386525436 1,0045641868918 1,00519101642664 1,00019348251134 0,995085740433649 0,995346812324949 1,00483686666268 |
| Dauer | 1,8 ms | 4,0 ms | 5,7 ms | 105791,2 ms |
Wie es aussieht, ist die Welt in Ordnung. Aber, schon eine Variable mehr verändert die Situation erheblich. Der Versuch mit dreizehn Gleichungen und dreizehn Unbekannten zeigt, daß wir numerisch im Grenzbereich wandeln:
| Nr. | Gauß | Householder | Mat-Inv | Gauß-Seidel |
| x(1) x(2) x(3) x(4) x(5) x(6) x(7) x(8) x(9) x(10) x(11) x(12) x(13) | 0,999999901088455 1,00001526494606 0,999417188455215 1,00964824199888 0,913651150105523 1,46750641279923 -0,629554582462835 4,77757515620127 -4,88366788883986 7,08422633362237 -3,00530887108786 2,51914806053944 0,747343543557318 | 1,00000063647624 0,999899895473124 1,00387061860638 0,935354954008552 1,58225480251897 -2,16737414272584 12,0800145166388 -24,7566980532241 41,2042476314134 -40,646884153115 28,4548815894478 -9,42501394315991 2,73544626110988 | 1,00000015624209 0,999933034647256 1,00056873261929 0,985750913619995 1,27785873413086 0,65203857421875 7,06903076171875 0,9720458984375 10,156005859375 -9,7440185546875 2,6826171875 -4,45159912109375 1,73866271972656 | 0,999994798171067 1,0001798271541 0,998595392430279 1,00365606351211 0,998085749933472 0,996527458325195 1,00042676605075 1,00347757287926 1,0027448974333 0,999347081027708 0,996326354571645 0,996923450959265 1,00371889455586 |
| Dauer | 2,1 ms | 4,7 ms | 7,0 ms | 215240,6 ms |
Nur Gauß-Seidel hat die Fassung bewahrt; keine Kunst, dieses Verfahren ist ja speziell für diese Sorte Gleichungssystem gedacht. Da kann man die größere Rechenzeit schon tolerieren.