Computerorientierte
Mathematik II Sommersemester 2000 Joswig / Jahn / Liebchen / Schwartz 2. Programmieraufgabe |
|
Zunächst einmal: Die zweite Programmieraufgabe ist viel einfacher als sie aussieht. Laßt Euch durch die vermeintlich hohe Komplexität des erwarteten Programms nicht abschrecken, wir haben die Aufgabe für Euch in kleine überschaubare Häppchen unterteilt.
Daher vergeßt beim Programmierens eines Häppchens mal all das andere, was noch wartet, und konzentriert Euch ganz auf den aktuellen Programmteil.
Wir sind uns bewußt, daß das Vorgeben von Interfaces Eure Kreativität einschränkt, aber andererseits ist das vielleicht ganz gut.
Der Kern Eures Programms wird eine Graphenklasse sein, mit der man Wege im Graphen suchen kann. Da Ihr mit Adjazenzlisten arbeiten sollt, benötigt Ihr natürlich eine schöne Listenklasse. Wie später klar wird, ist es notwendig, daß Ihr Knoten nach verschiedenen Kriterien sortieren könnt, also muß Eure Listenklasse eine Sortiermethode haben.
Daten müssen natürlich aus Dateien eingelesen werden. Allerdings liegen die Daten in einer etwas unbequemen Form vor: Als Fliesen (tiles), die irgendwo (durcheinander) in der Ebene liegen und ein bis vier "Ausgänge" haben. Das deutet, daß Ihr in Eurem Graphen nebeneinander liegende Fliesen, die Knoten entsprechen, mit Kanten verbinden müßt, wenn sie sich berührende Ausgänge haben.
Ist der Graph aufgebaut kann man in ihm einfach einen Weg zwischen
zwei in der Datei benannten speziellen Knoten finden (wenn es einen gibt).
Zur graphischen Ausgabe von Graph und Weg steht Euch eine
von uns geschriebene Klasse zur Verfügung, die Ihr einfach in Euren
Frame
reinpacken könnt.
Ihr sollt dann einen Listener für Mausklicks installieren, mit dem der Benutzer Kanten im Graphen einfügen und löschen kann. Die Logik dabei ist einfach: Gibt es an der angeklickten Stelle schon eine Kante, wird sie gelöscht, ansonsten eingefügt. Berechnet anschließend gleich einen neuen Weg und zeigt ihn an.
Als besonderer Service sind bereits alle Exception-Klassen vorhanden.
Wir stellen Euch ein paar Klassen und interfaces zur Verfügung, die
im labyrinth
-Package zusammengefaßt sind. Um es zu benutzen,
müßt Ihr es zunächst in Euren Bereich ansprechbar machen mit
ln -s ~co2-004/labyrinth ~/prog2/labyrinthMal vorausgesetzt, daß Eurer Verzeichnis für das zweite Programm
prog2
heißt und schon existiert. Es wird ein Verzeichnis
~/prog2/labyrinth
angelegt (genauer gesagt: ein
Link), in dem Ihr die nötigen Dateien findet.
Ersetzt ggf. prog2
durch den Verzeichnisnamen Eures
Programmverzeichnisses, z.B. durch Programme/Programm2
.
Damit obiges Kommando funktioniert, muß das Verzeichnis schon existieren.
In jeder Eurer Dateien müßt Ihr als erste Zeile ein
import labyrinth.*;stehen haben, damit der Java-Compiler die gestellten Klassen/Interfaces finden kann. Eine Liste der gestellten Dateien ist verfügbar, außerdem umfangreiche Dokumentation.
Es ist sinnvoll, wenn Ihr in folgender Reihenfolge der Implementierung vorgeht:
SortableListWithPointer
-Interface
genügt. Da SortableListWithPointer
von ListWithPointer
abgeleitet ist, muß Eure Klasse auch diesem Interface entsprechen.
Ihr braucht jedenfalls nur eine Listenklasse implementieren!
Die meisten der benötigten Methoden sind Euch so (oder ähnlich) bereits vorgemacht worden und sollten daher kein Problem darstellen.
Der Clou an der Listenklasse ist, daß sie sich per sort
selbst sortieren kann. Dafür sollt Ihr Mergesort auf Listen
implementieren. (Tip: Achtet darauf, daß Eure Referenz auf die
jeweiligen ersten Listenelemente immer stimmt.) Damit nach
verschiedenen Kriterien sortiert werden kann, wird an
sort
eine Instanz eines Comparer
s gegeben,
deren einzige Methode isGreaterThan
zum Vergleich
benutzt wird.
Die Korrektheit Eures Mergesorts testet bitte mit
folgendem Hauptprogramm (einfach in die Listenklasse einfügen).
Statt MyList
müßt Ihr natürlich den Namen Eurer Klasse eintragen.
/** * Main for testing the sort algorithm. * * @param argv ignored. */ public static void main(String argv[]) { // Test list implementation for lists with increasing length. try { for(int length = 0; length <= 40; length++) { MyList L = new MyList(); // Use your list implementation here! for(int i = 0; i < length; i++) L.insertFront(new Integer((int)Math.round(Math.random() * 100 - 50))); System.out.println("length=" + length); Comparer cmp; // Sort in ascending order. cmp = new Comparer() { public boolean isGreaterThan(Object o1, Object o2) throws CompareException { return ((Integer)o1).intValue() > ((Integer)o2).intValue(); } }; L.sort(cmp); System.out.print("ascending: " + L + " "); if (L.isSorted(cmp)) System.out.println("OK"); else System.out.println("failed"); // Sort in descending order. cmp = new Comparer() { public boolean isGreaterThan(Object o1, Object o2) throws CompareException { return ((Integer)o1).intValue() < ((Integer)o2).intValue(); } }; L.sort(cmp); System.out.print("descending: " + L + " "); if (L.isSorted(cmp)) System.out.println("OK"); else System.out.println("failed"); } } catch(CompareException e) { System.err.println("Got " + e.getMessage() + ". This should never happen."); } }
Tip: Es ist dringend anzuraten, immer am Ende der Methode
Für die Adjazenzlisten (Listen von Knoten!) verwendet Ihr
natürlich Eure Listenklasse. Dabei werden für jeden Knoten die Knoten
gespeichert, zu denen er eine Kante besitzt. Das bedeutet, daß jede
Kante durch insgesamt zwei Einträge in Adjazenzlisten
repräsentiert wird: der eine Endknoten in der Liste des anderen und
umgekehrt. Beachtet das insbesondere bei der Implementation von
Die Methode
Die Knoten beinhalten außerdem noch die Information, welche Sorte
Fliese denn (gerade) ihnen jeweils entspricht. Die dazu notwendige
Klasse
Spalte und Zeile sind ganze Zahlen (auch negative), der Fliesentyp ist
eine ganze Zahl von 1 bis 15. An jeweils genau einer Fliese der Datei
steht als fünftes Token
Anmerkung: Da der Graph ungerichtet ist, ist es egal, ob ein Weg von
Weitere Labyrinthe sind auch verfügbar.
Beispiele:
Wohl erlaubt ist, daß das Labyrinth nicht zusammenhängend ist, solange keine
offenen Wegstücke vorhanden sind:
Die Zuordnung der Fliesen läßt sich aus folgender Tabelle
entnehmen:
Ihr müßt (und sollt!) die Zuordnung von Fliesentyp zu verfügbaren
Richtungen nicht selbst vornehmen, dafür gibt es in
Die Benutzung von "leeren" Knoten für "leere" Felder ist verboten.
Beim Einlesen fügt Ihr zunächst nur die Knoten in den Graphen ein (mit
Koordinaten und Fliesentyp). Erst in einem weiteren Schritt werden in
der Methode
Um zu entscheiden, ob an einem Knoten in einer bestimmten Richtung
(die die Fliese angibt), eine Kante eingefügt werden soll
-- oder
ob gar eine unzulässige Datei vorliegt muß
man den direkt in dieser Richtung benachbarten Knoten anschauen und
prüfen, ob seine Fliese eine entgegenkommende Richtung
vorsieht.
Die Fliesen liegen jedoch im allgemeinen völlig durcheinander in der
Knotenliste Eures Graphen, so daß man den Nachbar anhand seiner
Koordinaten erstmal suchen müßte, was uns mit O(#Anzahl Knoten)
zu lange dauert.
Deshalb sollt Ihr eine etwas fortgeschrittene Methode einsetzen, bei der zuerst alle
waagerechten Kanten und dann alle senkrechten Kanten eingefügt werden:
In jeder Zeile ist es jetzt einfach zu überprüfen, ob zwei hintereinanderliegende
Fliesen zueinander passen. Wenn ja, fügt eine Kante zwischen ihren Knoten ein,
wenn nein, gebt eine Fehlermeldung aus. Beachtet, daß Ihr den ersten und letzten Knoten
jeder Zeile gesondert betrachten müßt, da es ja sein könnte, daß er verbotene
"in der Luft hängende" Ausgänge nach außen hat.
Beachtet auch, daß bei "Löchern" in einer Zeile die Knoten links und rechts eines
Loches zwar hintereinander in der Liste liegen, aber keinesfalls verbunden
werden dürfen.
Beachtet: Innerhalb von
Mit einem einfachen
So, das war der Teil der ersten Woche. Und nicht vergessen: Es sieht schlimmer aus als es ist.
Ein Weg im Graphen zwischen den als Start und Ziel ausgezeichneten
Knoten -- so einer überhaupt existiert --
läßt sich nun sehr leicht rekursiv über Tiefensuche (DFS - Depth First
Search) finden. Es genügt uns, wenn
Der
Da ein neuer mit
Ihr sollt eine Menüzeile vorsehen, in deren "File"- (oder
"Datei"-)Menü sich ein Punkt zum Laden eines neuen Labyrinths
befindet (über einen
Zum Abfragen der Mausklicks könnt Ihr ganz analog zum bekannten
Bei geeignetem Mausklick wird dann das
Jetzt gilt es herauszufinden, ob sich an dieser Stelle bereits
eine Kante befindet, und wenn ja, welche. Beachtet, daß Ihr
ggf. neue Knoten erzeugen müßt, wenn der Benutzer eine Kante
irgendwo haben möchte, wo noch nicht ein oder gar beide
Endknoten existieren. Ebenso sollt Ihr einen Knoten löschen,
wenn er keine zu ihm inzidende Kante mehr besitzt (also seine
Adjazenzliste leer ist). Das gilt nicht für den Start- bzw. Endknoten,
diese müssen immer existieren.
Das angezeigte Labyrinth soll stets im obigen
Sinne zulässig sein, es darf keine "in der Luft
hängende" Wegstücke geben. Spätestens hier wird Euch auffallen,
wenn Eure Routinen zum Einfügen und Löschen von Kanten nicht
dafür sorgen, daß die Fliesen der Knoten immer auf dem neuesten
Stand sind.
Wer Lust hat, kann jetzt noch eine Speicherroutine einbauen und
hat einen praktischen Labyrintheditor, um den ihn jeder beneiden
wird.
Wir wünschen Euch viel Spaß und vor allem viele lehrreiche
Momente mit dieser Programmieraufgabe.
sort
die Methode isSorted
zu fragen,
ob das Sortieren funktioniert hat!
Die Graphenklasse
Für den (ungerichteten!) Graph sind zwei Interfaces zu implementieren: Node
für Knoten und LabyrinthGraph
für den eigentlichen Graphen.
insertEdge
.
computePath
laßt Ihr zunächst (in der ersten Woche) leer:
public Path computePath(Node n1, Node n2) { return null; }
Tile
ist
netterweise von uns für Euch schon geschrieben worden.
Der Zwei-Phasen-Graph
Im Leben Eures Graphen sind zwei Phasen zu unterscheiden. In der ersten wird er
eingelesen und aufgebaut, in der zweiten benutzt. Diese Unterscheidung ist
wichtig, was die Behandlung des Fliesentypen angeht:
insertEdge
den Fliesentyp nicht
verändern. insertEdge
und deleteEdge
den Fliesentyp immer
anpassen.
Einlesen der Graphen
Das Dateiformat ist sehr einfach:
Pro Fliese ist eine Zeile vorgesehen, die aus vier bzw. fünf (durch
mind. ein Leerzeichen getrennte) Tokens besteht:
tile
Spalte Zeile Fliesentyp [s
|d
]
s
bzw. d
, was
anzeigt, daß diese Fliese der Anfang (s
) bzw. das Ende
(d
) des zu berechnenden Weges sein soll.
Beispiel:
tile 50 50 9
tile 51 51 6 d
tile 50 51 3
tile 51 50 12 s
s
zu d
oder umgekehrt berechnet wird.
Zulässige Labyrinthe
Eine solche Menge von Fliesen nennen wir zulässig, wenn je
zwei aneinander grenzende Fliesen entweder beide eine Verbindung über
"Ausgänge" zueinander haben, oder beide keine haben. "In der Luft
hängende" Fliesenausgänge darf es nicht geben (und müssen von Euch
erkannt werden, s.u.) Ebenso ist erforderlich daß genau ein Knoten als
Startknoten und genau ein anderer als Zielknoten gekennzeichnet ist.
erlaubt verboten
Die Fliesentypen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tile
schon
vorgefertigt die Methoden getDirection
,
getDirections
,
setDirection
und setDirections
,
mit denen Ihr abfragen/setzen könnt, ob es einen "Ausgang" in einer
bestimmten Richtung gibt.
Einfügen der Kanten
addEdgesAndCheckTiles
,
die am Ende von readFromFile
aufgerufen werden soll, die Kanten erstellt.
sortNodesRowThenColumn
die Knotenliste des Graphen
so sortiert, daß alle Knoten einer Zeile von links nach rechts
hintereinander liegen. Erreicht wird dies durch Euren Mergesort, dem
ein entsprechender Comparer
gegeben
wird, der eine lexikographische Ordnung auf den Knoten vorgibt, bei
der der Zeilenindex Vorrang hat vor dem Spaltenindex. sortNodesColumnThenRow
).
addEdgesAndCheckTiles
darf nicht findNode
.
Zum Testen schenken wir Euch ein toString
für den Graphen:
/**
* Convert whole graph to textual representation.
*
* @return string.
*/
public String toString()
{
StringBuffer sb = new StringBuffer();
Node n1, n2;
ListWithPointer nodes = getNodes(), adjacentNodes;
for(nodes.reset(); !nodes.atEnd(); nodes.advance())
{
n1 = (Node)nodes.getCurrent();
sb.append(n1 + " adjacent nodes: ");
adjacentNodes = n1.getAdjacentNodes();
for(adjacentNodes.reset(); !adjacentNodes.atEnd(); adjacentNodes.advance())
{
n2 = (Node)adjacentNodes.getCurrent();
sb.append(n2 + " ");
}
sb.append("\n");
}
return sb.toString();
}
main
läßt sich dann ein Graph einlesen,
aufbauen und ausgeben:
/**
* This is just for debugging.
*
* @param argv first argument has to be a filename.
*/
public static void main(String[] argv)
{
LabyrinthGraph G = null;
if(argv.length != 1)
{
System.err.println("Filename as argument expected!");
return;
}
try
{
G = new MyGraph(argv[0]); // Read graph. Use your class name!
}
catch(IOException e)
{
System.err.println("IOException: " + e.getMessage());
return;
}
catch(LabyrinthException e)
{
System.err.println("LabyrinthException: " + e.getMessage());
return;
}
System.out.println(G); // toString() is your friend.
}
Wege suchen
Als leichte Übung ist zunächst als Klasse für Wege das Interface Path
zu
implementieren.
computePath
irgendeinen Weg zwischen
diesen Knoten zurückliefert, er muß weder kurz noch schön sein.
Visualisierung des Labyrinths
Dank der schon vorprogrammierten Klasse LabyrinthViewer
ist es Euch ein leichtes, das Labyrinth graphisch
anzuzeigen. Entwerft wie üblich eine Klasse, die von
Frame
abgeleitet ist und den ganzen Oberflächenkram
übernimmt. Alle anderen Euren Klassen sollen von Oberfläche
nichts wissen.
LabyrinthViewer
wird wie jede andere Component
in Euer Fenster per add
eingefügt. Der anzuzeigende Graph mit/ohne Weg wird dann per setLabyrinth
gesetzt. Das
Malen geschieht dann automatisch.
setLabyrinth
gesetzter Graph eine
andere Ausdehnung als der alte haben, paßt sich der
LabyrinthViewer
automatisch in der Größe an. Dafür
muß sich allerdings auch die Fenstergröße ändern, weshalb Ihr
direkt nach dem Aufruf von setLabyrinth
einmal die
pack
Methode Eures Frames aufrufen solltet:
setLayout(new BorderLayout());
add("Center", viewer = new LabyrinthViewer());
.
.
.
viewer.setLabyrinth(graph, path);
pack();
FileDialog
).
Per Maus das Labyrinth ändern
Wie schon oben erklärt, soll der Benutzer in Eurem Graphen
Kanten einfügen und löschen können. Nach einer solchen Aktion
sollt Ihr sofort einen neuen Weg berechnen (vielleicht kommt
allerdings wieder der gleiche raus) und ihn mitsamt dem geänderten
Graphen anzeigen.
ActionListener
einen LabyrinthClickListener
mit addLabyrinthClickListener
beim LabyrinthViewer
installieren.
viewer.addLabyrinthClickListener(new LabyrinthClickListener() {
public void actionPerformed(LabyrinthClickEvent e)
{
handleClick(e);
}
});
actionPerformed
des Listeners aufgerufen. Übergeben wird ihm dabei ein LabyrinthClickEvent
-Objekt,
das die Koordinaten der Fliese enthält und den Bereich der
Fliese, auf den geklickt wurde (getPart
).
Ressourcen
labyrinth
-Package
labyrinth
-Packages
Alexander SchwartzOlaf
Jahn
Last modified: Tue Apr 25 17:23:34 MET DST 2000