Lernziele
| Algorithmen aus graph_draw.h |
| Spring Embedding |
| Straight Line Embedding |
| Das Benutzer-Koordinatensystem von window und GraphWin |
| Animationen in GraphWin |
Ein wesentlicher Punkt im Umgang mit Graphen ist deren visuelle Darstellung. Von ein und demselben Graphen können i. d. R. sowohl „gute“, d. h. übersichtliche Zeichnungen angefertigt werden, als auch „schlechte“, d. h. unübersichtliche, in denen etwa sehr viele Kanten sich gegenseitig kreuzen, was beim Betrachter zu großer Verwirrung führen kann.
Es gibt mehrere Ansätze zum Zeichnen von Graphen, die sich in der Methodik wesentlich voneinander unterscheiden. Wir werden hier zwei davon vorstellen: Spring Embedding und Straight Line Embedding. LEDA bietet diese und andere gute Algorithmen zum Zeichnen von Graphen in Form von Funktionen an, die in der Header-Datei graph_draw.h deklariert sind.
Ein Spring Embedder sieht die Knoten als kleine Metallringe in der Ebene an, die durch Federn (engl. spring) miteinander verbunden sind und sich deshalb gegenseitig abstossen bzw. anziehen. Ein Spring Embedder arbeitet in Iterationen. In jeder Iteration werden für jeden Knoten v die auf ihn wirkenden Kräfte berechnet. Jede inzidente Kante (u,v) zieht ihn mit der Kraft f(u,v) in Richtung von u, wobei f(u,v) proportional zur Differenz aus dem Abstand von u und v und der Länge der Feder ist („Hooke'sches Gesetz“). Umgekehrt stösst ihn jede ausgehende Kante (v,u) mit der Kraft f(v,u)=-f(u,v) von u ab. Nachdem alle Kräfte summiert wurden, werden die Ringe gemäß den auf sie wirkenden Kräften in der Ebene bewegt. (Durch die auf ihn wirkende Kraft erfährt der Ring eine bestimmte Beschleunigung in eine bestimmte Richtung, die für einen kurzen Zeitraum als konstant angesehen wird; die neue Position des Ringes ist die Position am Ende dieses Zeitraums.) Danach geht der Spring Embedder in die nächste Iteration. Bei hinreichend großer Anzahl von Iterationen wird ein Gleichgewichtszustand erreicht, bei dem die auf jeden Ring wirkende Kraft 0 ist.
Abbildung 5.29 zeigt eine Zeichnung eines Graphen vor einem Spring Embedding und danach.
![]() |
Anmerkung |
|---|---|
Streng genommen unterscheidet man zwischen Zeichnungen, Einbettungen und planaren Einbettungen eines Graphen. Eine Zeichnung ist irgendeine Art und Weise, einen Graphen auf eine Oberfläche (wie z. B. ein Blatt Papier oder die Oberfläche einer Kugel) zu zeichnen. Eine Einbettung ist eine Zeichnung mit der zusätzlichen Eigenschaft, dass sich keine Kanten kreuzen. Eine planare Einbettung ist eine Einbettung in die zweidimensionale Ebene. Somit ist die Bezeichnung „Sping Embedder“ nicht ganz gerechtfertigt, da ein Spring Embedder i. Allg. keine (planare) Einbettung erzeugt, auch wenn der zu zeichnende Graph planar ist. Wie wir aber in Abbildung 5.29 sehen, ist das Ergebnis eines Spring Embeddings meistens nahezu planar, d. h. nur wenige Kanten schneiden sich in der erzeugten Zeichnung. Trotzdem wäre der Name „Spring Drawer“ angemessener. |
|
LEDAs Spring Embedder verbirgt sich hinter der folgenden Funktion:
void SPRING_EMBEDDING(graph& G,
node_array<double>& xpos, node_array<double>& ypos,
double xleft, double xright, double ybottom, double ytop,
int iterations = 250);Die Parameter sind wie folgt zu verstehen: SPRING_EMBEDDING() berechnet in insgesamt iterations Iterationen neue Koordinaten für die Knoten, die alle innerhalb des Rechteckes [xleft,xright] x [ybottom,ytop] liegen. Die ursprünglichen Koordinaten der Knoten müssen in den Knoten-Arrays xpos und ypos übergeben werden; dort legt die Funktion auch die neu berechneten Koordinaten wieder ab.
Um diese Funktion in einem GraphWin benutzen zu können, müssen wir erst einige weitere Konzepte der Klasse GraphWin und der Elternklasse window kennen lernen.
Jedem GraphWin (und jedem window, wovon GraphWin ja abgeleitet ist) liegt ein Benutzer-Koordinatensystem zugrunde, in dem die x-Koordinaten von links nach rechts, und die y-Koordinaten von unten nach oben anwachsen, das also die in der Mathematik übliche Orientierung besitzt. Dieses Koordinatensystem wird durch drei Zahlen vom Typ double definiert: xmin, die minimale x-Koordinate, xmax, die maximale x-Koordinate und ymin, die minimale y-Koordinate. Diese Koordinaten werden auf die Pixel-Koordinaten des am Bildschirm sichtbaren Fensters abgebildet; dadurch ergibt sich ymax, die maximale y-Koordinate, und ein Skalierungsfaktor scale, der immer dann benutzt wird, wenn (bei einer Zeichenoperation) Benutzer-Koordinaten in Pixel-Koordinaten umgerechnet werden müssen.
Alle Zeichenoperationen in einem window benutzen dieses Benutzer-Koordinatensystem, um die zur Zeichnung nötigen Koordinaten zu spezifizieren. Der Benutzer muss sich also nie mit Pixel-Positionen herumschlagen! Wir haben dieses Koordinatensystem bisher noch nie explizit benutzt, werden es aber gleich tun: Wir müssen der Funktion SPRING_EMBEDDING() mitteilen, in welches Rechteck sie die Knoten anordnen soll, und dann die Knoten auf diese neu berechneten Koordinaten verschieben.
Das Benutzer-Koordinatensystem kann durch die Methode init() festgelegt werden:
void W.init(double xmin, double xmax, double ymin);
Diese haben wir bisher ebenfalls nicht benutzt. Wird dieser Aufruf ausgelassen, so ergibt sich das Benutzer-Koordinatensystem durch die Geometrie des Fensters am Bildschirm, s. Übung 68.
Als Rechteck für SPRING_EMBEDDING() wählen wir einfach das Rechteck des Benutzer-Koordinatensystem. Mit den Methoden xmin(), xmax(), ymin() und ymax() der Klasse window können die Eckwerte des derzeitigen Benutzer-Koordinatensystems erfragt werden. Für die Klasse GraphWin gibt es die speziellen Methoden get_xmin() usw., die in Betracht ziehen, dass am unteren Rand der Zeichenfläche eines GraphWin zusätzliche Statusinformationen angezeigt werden, so dass die tatsächliche Zeichenfläche etwas kleiner ist als die des gesamten Fensters.
Um die ursprünglichen Koordinaten eines Knoten v im Benutzer-Koordinatensystem zu erfragen, benutzen wir die Methode
point p = gw.get_position(v);
die ein Objekt der Klasse point zurückliefert. Die beiden Koordinaten dieses Punktes können mit
double x = p.xcoord(); double y = p.ycoord();
abgefragt werden. Durch diese Methoden können wir SPRING_EMBEDDING() die augenblicklichen Koordinaten der Knoten im Benutzer-Koordinatensystem mitteilen.
Nachdem der Spring Embedder die neuen Koordinaten berechnet hat, müssen wir die Knoten an diese Koordinaten verschieben. Das geschieht durch einen Aufruf
gw.set_position(xpos, ypos);
Die beiden Knoten-Arrays müssen dabei Zahlen vom Typ double beinhalten.
Die Verschiebung wird dabei animiert gezeigt: Das Attribut animation_steps der Klasse GraphWin gibt an, wie viele diskrete Teilschritte in die Bewegung von Knoten (und Kanten) vom Start- zum Endpunkt eingefügt werden. Hat dieses Attribut den Wert 0, so geschieht die Bewegung ohne Teilschritte, d. h. unmittelbar. Je höher der Wert, desto glatter (und langsamer) wird die Animation der Bewegung. Der Wert dieses Attributs lässt sich durch
gw.set_animation_steps(number_of_steps);
Das folgende Programm stellt die soeben eingeführten Konzepte vor. Abbildung 5.29 wurde damit erzeugt.
#include <LEDA/graph_draw.h>
#include <LEDA/graph.h>
#include <LEDA/graphwin.h>
#include <LEDA/node_array.h>
#include <LEDA/point.h>
using leda::graph;
using leda::node;
using leda::node_array;
using leda::GraphWin;
using leda::window;
using leda::point;
int main()
{
graph G;
GraphWin gw(G, 500, 500, "Spring Embedding");
gw.set_edge_width(2);
gw.set_animation_steps(15);
gw.display(window::center,window::center);
while(gw.edit()) {
// get user coordinates of drawing area
double xleft = gw.get_xmin();
double xright = gw.get_xmax();
double ybottom = gw.get_ymin();
double ytop = gw.get_ymax();
// get user coordinates of nodes
node_array<double> xpos(G);
node_array<double> ypos(G);
node v;
forall_nodes(v,G) {
point p = gw.get_position(v);
xpos[v] = p.xcoord();
ypos[v] = p.ycoord();
}
// number of iterations of spring embedding algorithm
const int iterations = 1000;
// compute embedding, starting with old embedding
SPRING_EMBEDDING(G, xpos, ypos,
xleft, xright, ybottom, ytop,
iterations);
// move nodes to new positions
// to change layout instantly, set animations_steps to 0
gw.set_position(xpos, ypos);
}
}
Ist ein Graph planar, so kann immer eine planare Einbettung gefunden werden, in der jede Kante als Geradensegment gezeichnet ist. Eine solche Einbettung heißt Straight Line Embedding. Abbildung 5.30 zeigt eine solche Einbettung.
Abbildung 5.30. Straight Line Embedding eines Graphen
![]() Vor dem Straight Line Embedding.
|
![]() Nach dem Straight Line Embedding.
|
LEDA bietet einen Straight Line Embedder durch die Funktion
int STRAIGHT_LINE_EMBEDDING(graph& G,
node_array<int>& xc, node_array<int>& yc);an. Der Graph G muss dabei planar sein. Die Funktion hinterlässt in xc und yc die ganzzahligen Koordinaten eines Straight Line Embeddings und gibt die maximale berechnete Koordinate zurück.
Das folgende Programm zeigt die Verwendung dieser Funktion. Neu daran ist die Methode
gw.place_into_win();
von GraphWin. Nachdem die neuen (ganzzahligen) Koordinaten der Knoten berechnet und mittels set_position() gesetzt wurden, ist es notwendig, diese so zu skalieren, dass sie in das Benutzer-Koordinatensystem des auf dem Bildschirm sichtbaren Fensters passen, und anschließend die Knoten auf diese skalierten Koordinaten zu transferieren. Dies leistet place_into_win().
#include <LEDA/graph_draw.h>
#include <LEDA/graph_misc.h>
#include <LEDA/graph.h>
#include <LEDA/graphwin.h>
#include <LEDA/node_array.h>
using leda::graph;
using leda::node;
using leda::node_array;
using leda::GraphWin;
using leda::window;
int main()
{
graph G;
GraphWin gw(G, 500, 500, "Straight Line Embedding");
gw.set_node_label_type(leda::no_label);
gw.set_node_width(15);
gw.display(window::center,window::center);
while(gw.edit()) {
if(!Is_Planar(G)) {
gw.message("G is not planar. Click <done> and edit again.");
gw.wait();
gw.del_message();
continue;
}
node_array<int> xc(G);
node_array<int> yc(G);
STRAIGHT_LINE_EMBEDDING(G, xc, yc);
// set_position only accepts doubles, so we must copy xc and yc
node_array<double> xpos(G);
node_array<double> ypos(G);
node v;
forall_nodes(v, G) {
xpos[v] = xc[v];
ypos[v] = yc[v];
}
// do not animate the setting of the new positions
gw.set_animation_steps(0);
gw.set_position(xpos, ypos);
// but animate the following place_into_win() very slowly
gw.set_animation_steps(100);
// scale and translate the layout so that it fits into the window
gw.place_into_win();
}
}
Weitere Informationen zum Benutzer-Koordinatensystem finden sich auf der Manualseite der Klasse window.
Die verschiedenen Funktionen von LEDA zum Zeichnen von Graphen sind auf der entsprechenden Manualseite beschrieben.
| Übung 68. |
Mit den Methoden get_xmin(), get_xmax(), get_ymin() und get_ymax() der Klasse GraphWin können die Eckwerte des derzeitigen Benutzer-Koordinatensystems erfragt werden. Legen Sie ein GraphWin wie bisher an, ohne durch init() das Koordinatensystem explizit zu setzen, und geben Sie die dann implizit festgelegten Eckwerte aus und interpretieren Sie diese. |
| Übung 69. | Ein Tutte Embedder erzeugt Zeichnungen wie die aus Abbildung 5.31, indem er jeden Knoten als Massepunkt interpretiert und dann versucht, ihn in das Gravitationszentrum seiner benachbarten Knoten zu positionieren. Dieser Algorithmus erzeugt für planare Graphen planare Einbettungen; die dabei erzeugten Faces sind konvex. Auch für nicht-planare Graphen liefert der Algorithmus oft recht übersichtliche Zeichnungen. Die LEDA-Funktion
bool TUTTE_EMBEDDING(graph G, list<node> fixed_nodes,
node_array<double>& xpos,
node_array<double>& ypos);erzeugt für G ein Tutte Embedding, dessen Faces alle konvex sind. Ist letzteres nicht möglich, gibt sie false zurück. Sie erwartet in fixed_nodes eine Liste von Knoten, deren Position im Embedding bereits fixiert ist; die Koordinaten dieser Knoten müssen in xpos und ypos abgelegt sein. (In Abbildung 5.31 z. B. wurden die Knoten 0-5 fixiert.) Sie legt die Koordinaten des Embeddings in xpos und ypos ab. Schreiben Sie ein Programm, mit dem in einem GraphWin ein Graph eingegeben und dann das Ergebnis eines Aufrufs von TUTTE_EMBEDDING() sichtbar gemacht werden kann. Die fixierten Knoten können z. B. mit gw.get_selected_nodes() ermittelt werden. |