Informatik III: Aufgabenblock 5 Version (FWM) vom 6. Oktober 2003 Abgabe vor dem 23. Jan. 2004 1 Graphen repräsentieren !#"$ % &('*) In den folgenden beiden Aufgabenblöcken werden Programme und Datenstrukturen zum Umbesteht aus einer Knotenmenge gang mit Graphen entwickelt. Ein gerichteter Graph von Knoten und einer Kantenmenge von Kanten. Auf dem im Folgenden betrachteten Graphen ist eine Funktion definiert, die jeder Kante , einen Wert zuweist. Zur Vereinfachung definieren wir . Im ersten Teil dieser Aufgabe wird eine Datenstruktur entwickelt, in der Graphen gespeichert werden können. Die Eingabe des Graphen erfolgt durch eine Textdatei, die in dem folgenden Format gegeben ist: -+ , . / 0 &1243-565-5789 : : Anzahl : Anzahl 8 % , % +-, der Knoten der Kanten +6, ; vier Zahlen: &124365-5-5<=89 – Kantennummer / >&1?43-5-5657 – Startknoten @&12 3-5-5-5< – Zielknoten A) . – % , Für jede Kante Wie man leicht nachvollziehen kann, beschreibt diese Datei 6 1 2 3 4 5 6 7 8 8 1 1 2 2 3 3 4 5 2 4 3 4 4 5 6 6 10.0 10.0 10.0 10.0 100.0 10.0 10.0 10.0 folgenden Graphen, bei dem zur besseren Übersicht die Kantennummern an den Kanten stehen. Um einen Graphen speichern zu können benötigen wir eine Datenstruktur. Zunächst kann die Eingabedatei direkt in eine Datenstruktur umgesetzt werden, die die Informationen für jede Kante enthält. 1 2 1 1 4 4 8 5 3 2 6 7 3 6 5 Abbildung 1: Ein Beispielgraph 8 Definieren Sie dazu eine struct, die die Informationen für eine Kante enthält, und einen Vektor mit solchen Elementen. Schreiben Sie ein Programm, das eine Eingabedatei liest und eine solche Datenstruktur aufbaut. Denken Sie an die Modularisierung, d.h. Definitionen kommen in eine Header-Datei, die verschiedenen Aufgaben werden in Unterroutinen modularisiert und nach Aufgabenbereichen in verschiedenen Dateien gesammelt. 2 Knoten-Kanten-Inzidenzmatrix aufstellen " 8 12 61? Eine weitere mögliche Dartstellung eines Graphen ist die Knoten-Kanten-Inzidenzmatrix. Diese –Matrix über der Menge enthält eine Zeile für jeden Knoten und eine Spalte für jede Kante. Für diese Matrix gilt ist Startknoten der Kante ist Zielknoten der Kante sonst , 1 1 +, +, Der Graph aus Abbildung ?? wird also durch die Matrix 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 dargestellt. Schreiben sie eine Funktion, die diese Matrix anhand der kantenorientierten Repräsentation des Graphen aufstellt. Die Matrix soll Elemente vom Typ double enthalten. 2 3 Matrix-Multiplikation ) eine Diagonalmatrix, ) obige Knoten-Kanten-Inzidenzmatrix und @A) . Für gegebene rechte Seite soll folgendes lineare Gleichungssystem gelöst werden: (1) Sei Zur Berechnung der Matrix müssen Sie die Matrix Diagonalelementen von und dann mit multiplizieren. transponieren, zeilenweise mit den Verwenden Sie Ihre Matrixmultiplikation aus Aufgabenblock 1. Schreiben Sie zunächst eine Funktion, die die Matrix berechnet. Die Diagonalelemente der Matrix können aus den Kantenattributen berechnet werden, die als Zusatzdaten beim Einlesen des Graphen übergeben , für . wurden. Es gilt: , , , / 4 Gleichungssystem lösen Machen Sie sich noch einmal mit den Routinen zum Lösen eines linearen Gleichungssystems aus Aufgabenblock 2 vertraut. Verwenden Sie die LU-Zerlegung mit Pivotierung und die folgende Rücksubstitution nun in Ihrem Programm zur Lösung des linearen Gleichungssytems (??). Denken Sie daran, dass die LU-Zerlegung ”in-place“ ausgeführt wird. Das heißt, wenn Sie die Matrix später noch einmal benötigen, müssen Sie sie vor der LU-Zerlegung kopieren. 12-12 -1 Warum ist das Gleichungssystem nicht lösbar? Ist es über- oder unterbestimmt? Was "!#!#! passiert , wenn man auf den Vektor anwendet? Was ist der Kern von ? Um das Gleichungssystem zu lösen, müssen Sie eine Komponente des Vektors auf 0 setzen. Dies bedeutet in dem Gleichungssystem, dass sie eine Zeile und eine Spalte weniger benutzen. Versuchen Sie erneut, das Gleichungssystem zu lösen. Stellen Sie eigene Graphen zum Testen & '!#!#! auf. Der Beispielgraph aus Abbildung ??, mit Variable %$ , hat mit und )( die Lösung: u[1] u[2] u[3] u[4] u[5] = = = = = 51 & 1.284 0.8581 ??? 0.7097 0.2903 Verifizieren Sie mit Hilfe Ihres Programms diese Angaben und bestimmen Sie u[3]. 5 Dünn besetzte Matrizen Lassen Sie sich die Matrix mit Ihrer Routine zur Matrixausgabe aus Block 2 ausgeben. 3 8 Die Matrix hat nur wenige Einträge, die von Null verschieden sind. Deren Anzahl lässt sich durch abschätzen. Solche Matrizen nennen wir dünn besetzt. Diese Matrixeigenschaft kann man in Anwendungen auf verschiedene Arten nutzen. Hier wollen wir sie benutzen, um den Rechenaufwand bei der Berechnung der Matrix erheblich zu reduzieren. Lassen Sie sich dazu zunächst mit Ihrem Program die Matrizen für folgende Graphen ausgeben: 1. Ein Graph mit nur einer Kante und zwei Knoten: 2 1 1 1 2 10.0 2. Ein Graph mit nur einer Kante und acht Knoten: 8 1 1 3 6 10.0 3. Noch ein Graph mit nur einer Kante und acht Knoten: 8 1 1 6 2 20.0 4. Eine Kombination der letzten beiden Graphen: 8 2 1 3 6 2 6 2 10.0 20.0 Wie Sie sehen, haben die Matrizen der ersten drei Graphen nur 4 Nichtnulleinträge. Die Matrix des vierten Graphen lässt sich anscheinend durch Aufsummieren der Matrizen des zweiten und dritten Graphen erzeugen. Dies gilt auch allgemein (Warum?). Schreiben Sie eine Funktion, die die Matrix durch Aufsummieren erzeugt. Starten Sie dabei mit einer Matrix, die nur Nullen enthält. Addieren Sie im Folgenden für jede Kante des Graphen die 4 Nichtnulleinträge zu dieser Matrix. Vergleichen Sie das Ergebnis mit der vorher verwendeten Methode, indem Sie die Matrizen und die Lösungen des Gleichungssystems betrachten. Vergleichen Sie auch die benötigte Laufzeit und den Speicherbedarf der beiden Programme (in Abhängigkeit der Anzahl von Kanten und Knoten im Graphen). 4 6 Knotenorientierte Graphen-Repräsentation Im Folgenden wollen wir eine weitere Datenstruktur zur Darstellung von Graphen kennenlernen. Bei einer Reihe von Algorithmen auf Graphen ist es nötig möglichst schnell auf die Menge der zu einem Knoten inzidenten Kanten zugreifen zu können. Dies kann man durch eine knotenorientierte Datenstruktur, die zu jedem Knoten eine Liste der inzidenten Kanten speichert. Da die Anzahl der zu einem Knoten inzidenten Kanten von Knoten zu Knoten unterschiedlich sein kann, sollten Sie diese Datenstruktur als verkettete Liste anlegen. Jedes Element hat einen Eintrag, in dem die Nummer der Kante gespeichert werden kann und einen Zeiger auf das nächste Element in der Liste. typedef struct nachbar { int kante; int knoten; struct nachbar * next; } t_nachbar; Zum Erzeugen eines Elements vom Typ t_nachbar müssen Sie Speicherplatz reservieren. Dazu verwenden Sie die malloc Function: t_nachbar *ersteKante; ... ersteKante = (t_nachbar *) malloc(sizeof(t_nachbar)); ersteKante->next = ... ; ersteKante->kante = ... ; .. Wenn der so reservierte Speicher nicht mehr gebraucht wird, sollte er wieder freigegeben werden. Dazu wird die Funktion free verwendet, die als Argument einen Zeiger auf einen Speicherbereich bekommt, der zuvor mit malloc reserviert worden war, und noch nicht mit free freigegeben worden ist. Programmieren Sie eine Unterroutine, die aus einer gegebenen Kantenstruktur die Knotenstruktur aufbaut. Starten Sie mit einem Vektor von Zeigern auf t_nachbar, für jeden Knoten einen Zeiger. Anfangs sollen die Zeiger 0 sein. Nun werden für jede Kante zwei Elemente von Typ t_nachbar erzeugt, die jeweils in die Listen der beiden an diese Kante grenzenden Knoten eingefügt werden. Testen Sie das Resultat, indem Sie eine Routine schreiben, die die entstandene Datenstruktur ausgeben kann, indem Sie für jeden Knoten die Nachbarknoten auflistet. Vergleichen Sie das Resultat mit dem Graphen. 7 Debugger benutzen Bei dynamischen Datenstrukturen wie verketteten Listen macht man leicht Fehler. Daher ist zur Fehlersuche ein Werkzeug hilfreich. Dies nennt man Debugger, von der englischen Bezeichnung “Bug” für Fehler. Es gibt text-basierte Debugger, hier ist insbesondere das Programm gdb 5 zu nennen, und es gibt graphische Programme, die die Bedienung des gdb einfacher machen. Ein solches graphisches Programm ist ddd. Mit dem ddd kann man sich sehr leicht verkettete Listen und ähnliche Datenstrukturen anzeigen lassen. Testen Sie dies, indem Sie Ihr Programm mit der Kompileroption -g übersetzen (dies ist notwendig, damit der Compiler ausreichend viele Informationen über den Quelltext in dem übersetzten Programm belässt), und den ddd mit Ihrem Programm als Argument starten. Die Bedienung des Debuggers erschließen Sie bitte aus den Hilfe-Texten und der Man-page. Hier nur ein Hinweis zu Vektoren: Zur Anzeige eines Vektors, der zum Beispiel als int a[10] deklariert ist, geben Sie links oben a[0] @ 10 ein, und klicken dann auf “Display”. Allgemein geben sie zunächst das erste Element ein, und dann hinter dem @-Zeichen die Anzahl der anzuzeigenden Elemente. 6