8 Projekt: Lineare Gleichungssysteme

8.1 Organisation der Daten
8.2 Elemente des Gauß-Algorithmus
8.3 Der eigentliche Algorithmus
8.4 Computer machen (keine) Fehler !(?)
8.5 Nicht-reguläre Systeme
8.6 Ausblick in schwierige(n) Zeiten

Zum vorigen Kapitel Zum Inhaltsverzeichnis


In diesem Kapitel wollen wir einen Algorithmus implementieren, den jeder durchschnittliche Oberstufenschüler beherrschen sollte: den "Gauß-Algorithmus" nämlich, mit dessen Hilfe wir Systeme linearer Gleichungen lösen können.



8.1 Organisation der Daten

Ehe wir überhaupt irgend etwas programmieren können, müssen wir uns erst einmal Gedanken machen, in welcher Form wir eigentlich ein lineares Gleichungssystem im Speicher ablegen wollen. Natürlich werden wir uns auf die Speicherung der Koeffizienten des Gleichungssystems beschränken, wofür sich ein zweidimensionales Array aus Fließkommazahlen, z.B. vom Typ Double, anbietet. Um die programmiertechnischen Schwierigkeiten klein zu halten, sollten wir die Gleichungssysteme in der Größe beschränken: mehr als 10 Gleichungen für 10 Unbekannte erscheint nicht sinnvoll, weil man schon in diesem Fall über 100(!) Koeffizienten einzugeben hätte. Wir stellen also ein zweidimensionales Array zur Verfügung, das die Koeffizienten unseres gewünschten Systems aufnehmen kann:
     const MaxCount = 10;
     var ko : Array [0..MaxCount, 0..MaxCount] of Double;
Die Konstante MaxCount gibt dabei die maximale Anzahl der Gleichungen und der Unbekannten an. Es ist gute Praxis, Bereichsbeschränkungen mit Hilfe solcher Konstanten zu formulieren, weil diese sich dann nachträglich einfach ändern lassen. Da in unserem Programm nur ein Gleichungssystem vorkommen wird, lohnt sich die Vereinbarung eines eigenen Typs kaum, weshalb hier eine direkte Deklaration des Koeffizienten-Arrays gewählt wurde. Um aber später möglichst einfach auch auf ganze Gleichungen zugreifen zu können, ist es sinnvoll, zusätzlich noch eine Typ-Deklaration für einzelne Gleichungen zu vereinbaren:
     type TEquation = Array [0..MaxCount] of Double;
Damit uns das LGS-Projekt nach seinem erfolgreichen Abschluss ein möglichst universell einsetzbares Werkzeug liefert, ist es sinnvoll, die Implementierung des Gauß-Algorithmus vollkommen von der Programmierung der Benutzeroberfläche unseres aktuellen (Test-)Programms zu trennen. Wir erreichen dies, indem wir den Gauß-Algorithmus zusammen mit den benutzten Datenstrukturen in einer eigenen Unit unterbringen. Ist dann in einem anderen Projekt ein Gleichungssystem zu lösen, kann man einfach diese Unit zu dem Projekt hinzufügen und damit von früherer Arbeit profitieren.

Eine Unit ist eine transportables Stück Delphi-Quellcode, das nach einem bestimmten Schema gebaut ist. Ein erster Entwurf unserer Unit könnte z.B. so aussehen:
     unit Unit_LGS;

     interface

       const MaxCount = 10;
       type TEquation = Array [0..MaxCount] of Double;
       var ko : Array [0..MaxCount, 0..MaxCount] of Double;
                EquaCount,
                VarCount : Integer;

     implementation

     end.
Die Unit beginnt mit dem Schlüsselwort unit, gefolgt von einem eindeutigen Unit-Namen, der auch gleichzeitig der Dateiname sein muss. Es folgen zwei Abschnitte:
  1. Unter interface werden alle öffentlichen Deklarationen dieser Unit aufgeführt. Alles, was im interface-Teil der Unit steht, wird veröffentlicht, d.h.: wenn irgend eine andere Unit diese unsere Unit in ihrer uses-Liste aufführt, dann sind die hier aufgeführten Deklarationen von dieser anderen Unit aus "sichtbar" und können daher auch dort genutzt werden.
  2. Unter implementation werden die Quelltexte für im interface-Abschnitt deklarierte Prozeduren und Funktionen aufgeführt. Hier folgt also die eigentliche Implementierung dieser Prozeduren und Funktionen. Zudem kann der implementation-Teil auch noch zusätzliche lokale Deklarationen enthalten. Wichtig ist aber, dass diese Details der Implementierung nur unit-intern bekannt sind! Einem Programm, das unsere Unit benutzt, bleibt der Inhalt des implementation-Teils unbekannt.
Kurz gesagt: der interface-Abschnitt enthält die veröffentlichte Schnittstelle zu unserer Unit, der implementation-Teil die geheimen Implementierungs-Details. In unserem Fall enthält der interface-Teil noch keine Prozedur- oder Funktions-Deklaration, weshalb der implementation-Teil im Augenblick auch noch leer ist. Das wird sich aber bald ändern!


Neben der Variablen ko enthält die obige Deklaration noch die Variablen EquaCount und VarCount, die beschreiben, welcher Teil des Koeffizienten-Arrays ko denn nun eigentlich gültige Daten enthalten soll: EquaCount soll die Anzahl der Gleichungen angeben, und VarCount die Anzahl der Variablen des Systems.

Das erste Problem ist, die Koeffizienten unseres jeweiligen konkreten Gleichungssystems in das Koeffizienten-Array ko hinein zu bekommen bzw. nach dem Lösen des Gleichungssystems wieder aus ko auszulesen. Wenn wir dies "gleichungsweise" machen wollen, können wir dazu z.B. die folgenden beiden Prozeduren zur Verfügung stellen:
     procedure SetEquation(nr: Integer; koeff: TEquation);
     procedure GetEquation(nr: Integer; var koeff: TEquation);
Die erste Prozedur soll die Koeffizienten der übergebenen Gleichung koeff als nr-te Gleichung in das Koeffizienten-Array ko schreiben, die zweite soll die Koeffizienten der nr-ten Gleichung aus dem Koeffizienten-Array lesen und sie in der Gleichung koeff an das aufrufende Programm zurückgeben.

Damit ist allerdings noch nicht eindeutig festgelegt, an welcher Stelle der Gleichungsvariablen koeff bzw. des Koeffizienten-Arrays ko welcher Koeffizient unseres Gleichungssystems steht. Um später vergleichbare Programme zu haben und um die Fehlersuche zu vereinfachen, vereinbaren wir die folgenden Konventionen:

Unsere Unit sieht sieht inzwischen nun so aus:
     unit Unit_LGS;

     interface

       const MaxCount = 10;

       type TEquation = Array [0..MaxCount] of Double;

       var ko : Array [0..MaxCount, 0..MaxCount] of Double;
           EquaCount,
           VarCount : Integer;

       procedure SetEquation(nr: Integer; koeff: TEquation);
       procedure GetEquation(nr: Integer; var koeff: TEquation);

     implementation

     procedure SetEquation(nr: Integer; koeff: TEquation);
       begin
       {.....}
       end;

     procedure GetEquation(nr: Integer; var koeff: TEquation);
       begin
       {.....}
       end;

     end;



Aufgabe:

  1. Ein- und Ausgabe

    Schreiben Sie ein Programm, das dem Benutzer gestattet, die Koeffizienten eines Linearen Gleichungssystems in eine StringGrid-Komponente einzutragen. Auf Knopfdruck sollen diese Koeffizienten dann aus dem StringGrid in das Koeffizienten-Array ko übertragen werden; ein zweiter Knopfdruck soll die Koeffizienten auf dem umgekehrten Weg wieder in die StringGrid-Komponente zurückschreiben.

    Implementieren Sie dazu die beiden oben angegebenen Prozeduren SetEquation und GetEquation und benutzen Sie diese. Halten Sie sich dabei unbedingt an die obigen Konventionen.




8.2 Elemente des Gauß-Algorithmus

Zunächst wollen wir uns bewusst machen, was wir eigentlich beim Lösen eines Linearen Gleichungssystems tun: ehe man den Algorithmus in einer Programmiersprache formulieren kann, muss man klären, welche Schritte man da in welcher Reihenfolge "abspult". Dazu dient die folgende


Aufgabe:

  1. Eine Fingerübung auf dem Papier

    Lösen Sie das folgende Gleichungssystem mit Hilfe des Gauß-Algorithmus auf einem Blatt Papier. Protokollieren Sie dabei genau jeden einzelnen Schritt, so dass Sie hinterher genau darüber Rechenschaft ablegen können, "wie man das macht".
         3 x  -  2 y  -    z  =  6
         2 x  +    y  +  3 z  =  5
           x  -    y  +  2 z  = -1
    


Wenn Sie die obige Aufgabe gewissenhaft erledigt haben, dann sollte auf Ihrem Blatt Papier nun eine Serie von Anweisungen stehen wie:
Solche Anweisungen beschreiben die sogenannten "Elementar-Umformungen": dies sind die zulässigen Äquivalenzumformungen für Gleichungssysteme. Das Ziel all dieser Umformungen ist es, das Gleichungssystem in ein äquivalentes umzuformen, also eines, das dieselbe Lösungsmenge hat wie das ursprüngliche, bei dem aber außerhalb der Hauptdiagonalen des Koeffizientenfeldes möglichst nur noch Nullen stehen.

Wir wollen die Implementierung des Algorithmus nun nach der "Bottom-Up-Methode" in Angriff nehmen: dies ist eine Programmierstrategie, bei der man zunächst elementare Teil-Algorithmen zur Verfügung stellt und danach aus diesen einen komplexeren Gesamt-Algorithmus zusammensetzt. Bevor wir uns daher an die Formulierung des Gauß-Algorithmus wagen, müssen wir also zunächst einmal die folgenden Elementar-Umformungen zur Verfügung stellen:
  1. Vertauschen zweier Gleichungen
  2. Multiplikation einer Gleichung mit einer Zahl (ungleich Null)
  3. Subtraktion eines Vielfachen einer Gleichung von einer anderen Gleichung
Dazu könnten die folgenden Prozeduren dienen:
  1. procedure ExchangeEquations(n1, n2: Integer);
  2. procedure DivideEquation(nr: Integer; factor: Double);
  3. procedure SubtEquation(dest: Integer; factor: Double; source: Integer);



Aufgabe:

  1. Elementar-Umformungen

    Ergänzen Sie Ihre Unit Unit_LGS um die obigen Deklarationen für die Elementarumformungen, und implementieren Sie sie!

    Schreiben Sie eine zusätzliche Test-Prozedur "Manipulate", in der Sie die einzelnen Elementar-Umformungs-Prozeduren aufrufen können, um sie zu testen. Wenn Sie sicher sind, dass alle Prozeduren genau das machen, was sie sollen, dann können Sie in "Manipulate" eine solche Folge von Prozedur-Aufrufen programmieren, dass das Gleichnungssystems aus Aufgabe 2 gelöst wird!
    Lösungsvorschlag



8.3 Der eigentliche Algorithmus

Unser Programm kann nun die Koeffizienten eines Gleichungssystems speichern, und es kann die Elementarumforungen durchführen. Nun können wir versuchen, ihm den Gauß-Algorithmus "beizubringen". Wir wollen den Algorithmus mit Hilfe der Elementarumformungen zunächst in einer einfachen, umgangssprachlichen Version formulieren, und ihn dann in einer Prozedur zu implementieren:



Aufgabe:

  1. Schon oft gemacht - aber wie geht das eigentlich?

    Formulieren Sie den Gauß-Algorithmus zum Lösen eines Gleichungssystems umgangssprachlich. Stellen Sie ihn in einem Struktogramm dar. Sie sollen sich dabei nicht auf den oben als Beipiel gewählten Fall "3 Gleichungen mit 3 Unbekannten" beschränken, sondern die Formulierung so wählen, dass sie auch für "n Gleichungen mit n Unbekannten" gilt. Sie können sich aber zunächst auf "quadratische" Systeme beschränken, und darüberhinaus im Augenblick annehmen, dass das System regulär sei, also genau eine Lösung besitzt.
    Lösungsvorschlag


  2. Und nun die Sklaven-Arbeit!!!

    Implementieren Sie in der Unit Unit_LGS eine Prozedur "Diagonalize", welche das aktuell ín der Variablen "ko" gespeicherte Gleichungssystem diagonalisiert.
    Ergänzen Sie Ihr Programm um eine Ergebnis-Anzeige!
    Testen Sie das Programm mit verschiedenen Gleichungssystemen! Erkennen Sie irgendwelche Situationen, in denen sich Probleme ergeben?
    Lösungsvorschlag


  3. Etwas Feinschliff zur Abrundung

    Wenn Sie Ihre Unit Unit_LGS so weit implementiert und sich von der korrekten Arbeitsweise überzeugt haben, dann könnte Ihnen auffallen, dass Sie beim Benutzen dieser Unit gar nicht alle der im interface-Teil deklarierten Prozeduren von außen ansprechen werden: im Grunde brauchen Sie ja nur die Variablen VarCount und EquaCount, die Transfer-Prozeduren SetEquation und GetEquation sowie die Lösungsprozedur Diagonalize! Alle anderen Prozeduren werden eigentlich nur unit-intern gebraucht, müssen also nicht veröffentlicht werden!

    Es ist eine gute Praxis, die Schnittstelle einer Unit so schlank als irgend möglich zu halten. Entfernen Sie also alle überflüssigen Deklarationen aus dem interface-Teil Ihrer Unit Unit_LGS!
    Lösungsvorschlag



8.4 Computer machen (keine) Fehler !(?)

Das Lösen linearer Gleichungssysteme ist eine Standardaufgabe, die durchaus nicht trivial ist. Ist schon die Implementierung des Gauß-Algorithmus ein kleiner geistiger Kopfstand, dann ist die Arbeit damit leider noch nicht getan: das Verfahren führt durchaus nicht bei allen Gleichungssystemen zur jeweils korrekten Lösung! Unser einfacher Gauß-Algorithmus erweist sich nämlich bei manchen Systemen als "numerisch nicht stabil". Dies soll heißen, dass bei der Durchführung des Algorithmus bei manchen Gleichungssystemen Fehler in nicht mehr tolerierbarer Größe auftreten können.

Was heißt hier "Fehler"? Nehmen wir nicht gerade deswegen einen "Rechner", weil er eben weniger Fehler beim Rechnen macht als wir? Um die Art der auftretenden Fehler zu verstehen, schauen wir uns mal ein solches "Problembeispiel" im Detail an: das Gleichungssystem
          7 x  +  4 y  -  3 z  =  17
        -14 x  -  8 y  +  5 z  = -36
          3 x  +  1 y  -  2 z  =   3
hat eigentlich die Lösung:
        (x; y; z) = (1; 4; 2) 
wie Sie durch schlichtes Nachrechnen im Kopf (oder notfalls mit Ihrem TR!) beweisen können. Wenn Sie es mit unserem Testprogramm lösen, dann erhalten Sie möglicherweise eine Lösung wie:
         (x; y; z) = (1,2856...; 3,5 ; 2) 
Woran liegt dies? Wenn man der Prozedur Diagonalize beim Lösen des Gleichungssystems mit Hilfe des Debuggers "über die Schulter schaut", dann kann man sehen, dass dieses Verhalten durch kaum zu vermeidende Rundungsfehler verursacht wird. Eine ständige Quelle des Ärgers ist die Subtraktion zweier "fast gleich großer" Zahlen. Sollen die beiden Zahlen wirklich gleich groß sein, dann sollte das Ergebnis Null sein. Ist aber zumindest eine der Zahlen mit einem Rundungsfehler behaftet, dann ergibt sich als Differenz eben nicht Null, sondern dieser Rundungsfehler (bzw. die Differenz der Rundungsfehler). Dies kann aber fatale Folgen haben: beim Gauß-Algorithmus kommt es nämlich an einigen Stellen schon sehr genau drauf an, ob ein Koeffizient Null ist oder nicht!

Mit dem Debugger (oder durch eine entsprechende Änderung im Quelltext) können Sie verifizieren, dass unser Programm das obige Gleichungssystem nach der erfolgreichen Bearbeitung der 1. Spalte in folgendem Zustand hinterlässt:
        1 x  +  0,5714285... y - 0,4285714... z =  2,4285714...
        0 x  + -4,4408..E-16 y - 1            z = -2
        0 x  -  0,7142857... y - 0,7142857... z = -4,2857142...
Der zweite Koeffizient der zweiten Gleichung ist offensichtlich falsch: wenn Sie das System "von Hand" lösen, sehen Sie, dass hier eigentlich eine glatte Null stehen müsste! Diese Erkenntnis macht die Lage jedoch zunächst nur noch schlimmer, denn wenn hier eine Null stände, könnten wir die nun fällige Normierung der zweiten Gleichung überhaupt nicht durchführen! Der falsche Wert hilft uns nun zwar, den Laufzeitfehler "Division durch Null" zu vermeiden, aber gleichzeitig führt er zu einer ziemlich verkehrten "Lösung" unseres Gleichungssystems!

Interessanterweise erhalten wir die richtige Lösung, wenn wir die zweite und die dritte Gleichung miteinander vertauschen, denn dann kommt im Ablauf des Algorithmus keine Division durch eine "Fast-Null" vor! Aber wie kann man erkennen, welche der verbleibenden Gleichungen die geeignetste für den nächsten Schritt ist? Ganz einfach: indem man die Beträge der Koeffizienten der entsprechenden Variablen aus den restlichen Gleichungen miteinander vergleicht und diejenige Gleichung wählt, die den betragsgrößten Koeffizienten an dieser Stelle bietet. Dieses Verfahren ist unter dem Namen "Spaltenpivotisierung" bekannt. Der Division zur Erzielung einer "1" wird also noch eine geschickt gewählte Vertauschung zweier Gleichungen vorgeschaltet. Womit nun auch die schon lange zur Verfügung gestellte Vertauschung zweier Gleichungen endlich einer sinnvollen Verwendung zugeführt werden kann!

Richtig sinnvoll wird dies aber erst, wenn man das LGS zusätzlich einer "Normierung" unterzieht: bevor dieser betragsgrößte Koeffizient gesucht wird, werden alle verbleibenden Gleichungen durch den Betrag ihres jeweiligen "Koeffizientenvektors" dividiert. Durch diese Maßnahme werden die Koeffizienten der einzelnen Gleichungen eigentlich erst miteinander vergleichbar, wie wir das für die Pivotisierung ja brauchen; zuvor können sie ja in durchaus unterschiedlichen Größenordnungen liegen. (Üblicherweise wird die "rechte Seite" der Gleichungen nicht zum "Koeffizientenvektor" dazugerechnet, weshalb sie bei der Betragsbildung unberücksichtigt bleibt. Bei der anschließenden Division darf sie aber natürlich nicht außen vor bleiben, sonst wäre diese ja keine Äquivalenzumformung!)


Aufgabe:

  1. Normierung und Pivotisierung

    Kopieren Sie Ihr LGS-Projekt (um den bisher erreichten Stand zu sichern!). Entwickeln Sie die neue Version weiter, indem Sie die Pivotisierung (mit Normierung) implementieren. Überzeugen Sie sich davon, dass das neue Programm nun das obige Problembeispiel korrekt bearbeitet.
    Lösungsvorschlag



8.5 Nicht-reguläre Systeme

Auch für einen verfeinerten "Gauß-Algorithmus mit Spaltenpivotisierung" bleiben viele Gleichungssysteme problematisch. Versuchen Sie mal, das folgende System mit Ihrem Programm zu lösen:
           2 x  -  3 y  +  5 z  =  1
          -4 x  +  6 y  - 10 z  = -2
          10 x  - 15 y  + 25 z  =  5
Statt das Gleichungssystem zu diagonalisieren, liefert unser Programm einen Gleitkomma-Laufzeitfehler! Wenn Sie sich das System aufmerksam ansehen, dann sehen Sie sicher, dass schon je 2 der 3 Gleichungen linear abhängig sind, und Kenner schließen daraus sofort, dass das System einen zwei-dimensionalen Lösungsraum hat. Aber wie können wir unserem Programm beibringen, diese Situation zuverlässig selbständig zu erkennen und korrekt darauf zu reagieren?

Suchen Sie als erstes mit Hilfe des Debuggers den genauen Fehlerort. Setzen Sie dazu einen "Breakpoint" an den Anfang der Prozedur "Diagonalize": in Delphi markiert ein Klick vor eine Quelltextzeile einen Breakpoint, die Zeile erscheint dann mit einem roten Balken hinterlegt. Gehen Sie dann (mit der F7-Taste [Einzelne Anweisung ausführen] und F8-Taste [Ganze Prozedur ausführen] schrittweise im Programm weiter, bis Sie den Fehlerort genau lokalisiert haben.

Schauen Sie sich nun den Zustand des Gleichungssystems direkt vor dem Auftreten des Fehlers genau an! Dann sollten Sie eine gute Idee haben, wie unser Gauß-Algorithmus zu erweitern ist, damit er nicht an dieser Situation scheitert. Es wird dabei nötig sein, dass Sie entscheiden, wann eine "kleine Zahl" als "Null" zählen soll - und das ist ein recht ernsthaftes Problem. Die problematischen Zahlen entstehen bei der Subtraktion der Gleichungen, weshalb es sinnvoll ist, eben bei dieser Subtraktion schon darauf zu achten, ob hier eventuell zwei fast gleichgroße Zahlen voneinander subtrahiert werden. Sollte dies der Fall sein, kann als Ergebnis gleich die Null angenommen werden. Sie müssen sich "nur noch" ausdenken, wie Sie entscheiden wollen, wann die beiden Zahlen als "hinreichend gleichgroß" gelten sollen!


Aufgabe:

  1. Die Luxus-Version, die "alles" kann

    Kopieren Sie Ihr LGS-Projekt (um den bisher erreichten Stand zu sichern!). Entwickeln Sie die neue Version weiter, indem Sie den Gauß-Algorithmus so erweitern, dass er auch für Systeme mit nicht genau einer Lösung das Gleichungssystem stabil diagonalisiert. Sichern Sie dabei die Subtraktion zweier Gleichungen so ab, dass bei fast gleichgroßen Koeffizienten eine "echte" Null als Ergebnis verwendet wird.
    Überzeugen Sie sich davon, dass das neue Programm nun das obige Problembeispiel korrekt bearbeitet.
    Lösungsvorschlag



8.6 Ausblick in schwierige(n) Zeiten

Nachdem Sie sich bis hier her durch dieses Kapitel durchgekämpft haben, haben Sie nun das Gefühl, "das Problem der Gleichungs-Systeme" damit gelöst zu haben? Immerhin kann unsere letzte Version beliebige Gleichungssysteme aus bis zu 10 Gleichungen mit bis zu 10 Variablen diagonalisieren, wobei die Grenze "10" auch leicht auf "100" (oder gar "1000") erhöht werden kann. Wenn Sie jedoch die bisherigen Erfahrungen kritisch überdenken, sollte in Ihnen auch der Verdacht aufkeimen, dass es möglicherweise doch noch Systeme gibt, bei denen sich selbst unsere letzte Programmversion verrechnet. Hoffentlich haben Sie diesen Verdacht, denn sonst hätten Sie ein wesentliches Lernziel dieses Kapitels nicht erreicht!

Vom Standpunkt der Numerischen Mathematik sind lineare Gleichungssysteme ein sehr ergiebiges Thema, speziell im Hinblick auf Fragen der Rechengenauigkeit. Ganze Kapitel von Hochschul-Vorlesungen zur Numerischen Mathematik sind dem Thema gewidmet, für spezielle Sorten von "schlecht konditionierten Systemen" optimale Lösungsalgorithmen zu finden. Es gibt leider keinen universellen Algorithmus, der immer das beste mögliche Ergebnis produziert.

Ganz allgemein können sich durch die Vielzahl der hintereinandergeschalteten Rechenschritte selbst kleine Fehler zu einer ganz erstaunlichen Größe "aufschaukeln". Der eigentliche Grund für ihr Auftreten ist die traurige Tatsache, dass Fließkommazahlen im Rechner stets nur mit begrenzter Genauigkeit gespeichert werden können: die Anzahl der geltenden Dezimalstellen ist also durch den jeweils benutzten Datentyp begrenzt: beim Typ Single wären dies etwa 7 bis 8 geltende Stellen, beim Typ Double immerhin 13 bis 14. Trotzdem ist das Problem prinzipieller Natur, und die Verwendung noch "größerer" Datentypen löst es nicht wirklich, sondern mindert nur sein Ausmaß im Einzelfall.
Wir können aus diesem Dilemma etwas sehr Wesentliches lernen:

Selbst wenn die verwendeten Algorithmen korrekt sind, kann der Einsatz eines Rechners noch nicht die Richtigkeit der produzierten Ergebnisse garantieren!

Diese beunruhigende Tatsache sollte man sich gelegentlich ins Gedächtnis rufen, wenn mal wieder ein ungebildeter Zeitgenosse mit der Autorität des Computers argumentiert!




Zum vorigen Kapitel Zum Inhaltsverzeichnis