5.2.7. Spezielle Hilfsdatenstrukturen für Graphenalgorithmen

Lernziele

Die Klasse node_list
Die Klasse node_set
Die Klasse node_pq
Die Klasse b_node_pq
Die Klasse node_partition
Die Klasse edge_set

Häufig müssen in Graphenalgorithmen Knoten in einer linearen Liste verwaltet werden. Dafür kann natürlich eine list<node> verwendet werden. Für diese spezielle Kombination jedoch bietet LEDA eine besonders effiziente Implementierung in Form der Klasse node_list an, die allerdings eine gegenüber der Klasse list eingeschränkte Schnittstelle besitzt. Zudem kann jeder Knoten eines Graphen zu jedem Zeitpunkt in nur einer node_list enthalten sein. Der Test, ob ein bestimmter Knoten in einer node_list enthalten ist, benötigt nur erwartete Zeit O(1).

Dem entsprechend gibt es für Mengen von Knoten den speziellen Typ node_set und für Partitionen von Knoten den Typ node_partition. Für Prioritäts-Warteschlangen von Knoten bietet LEDA die Typen node_pq und b_node_pq an, wobei letzterer nur für Prioritäts-Warteschlangen mit beschränkten, ganzzahligen Prioritäten geeignet ist.

Darüber hinaus gibt es die Klasse edge_set zum effizienten Verwalten von Kantenmengen.

Alle diese Klassen sollten i. Allg. aus Effizienzgründen überall dort verwendet werden, wo sonst der entsprechend parametrisierte Containertyp bzw. die entsprechend parametrisierte Prioritäts-Warteschlange verwendet würde und das Programm mit den Funktionen der eingeschränkten Schnittstelle auskommt.

Auf eine genauere Beschreibung der Schnittstellen wollen wir hier verzichten, da sie ohnehin mit den Schnittstellen der allgemeineren Typen weitgehend übereinstimmen. Vielmehr sei hierfür auf die Manualseiten von node_list, node_set, node_partition, node_pq, b_node_pq und edge_set verwiesen.

Ein Beispiel: Eine Implementierung von TOPSORT()

Als Beispiel für den sinnvollen Gebrauch einer node_list wollen wir eine Möglichkeit aufzeigen, die Funktion TOPSORT() zur Berechnung einer Topologischen Sortierung eines Graphen zu implementieren.

Wir laufen einmal über alle Knoten des Graphen, merken uns die ursprünglichen Eingangsgrade und speichern alle Knoten, die Eingangsgrad 0 haben, in einer node_list namens ZEROINDEG. Solange diese Liste nicht leer ist, ziehen wir einen Knoten heraus und geben ihm die nächste Nummer in der Toplogischen Sortierung. Wir entfernen ihn (konzeptionell) aus dem Graphen, indem wir die Eingangsgrade aller benachbarten Knoten um 1 vermindern. Wird dabei der Eingangsgrad eines Knotens auf 0 gesetzt, so wird dieser Knoten nach ZEROINDEG aufgenommen. Das Verfahren endet, wenn ZEROINDEG leer ist.

Hat am Ende jeder Knoten eine (fortlaufende) Nummer erhalten, so hat der Graph eine Topologische Sortierung. Ist dagegen ZEROINDEG leer geworden, ohne dass alle Knoten eine Nummer erhalten haben, so besitzt der Graph einen Zyklus.

Filename: TOPSORT.C
LEDA-Benutzer ab Version 5.0: Beachten Sie, dass die Header-Dateien nun in Module (Unterverzeichnisse) partitioniert sind!
#include <LEDA/graph.h>
#include <LEDA/node_array.h>
#include <LEDA/node_list.h>

using leda::graph;
using leda::node_array;
using leda::node_list;
using leda::node;

bool TOPSORT(const graph& G, node_array<int>& top_num)
{
  // store all nodes with indegree = 0 in ZEROINDEG
  node_list ZEROINDEG;  
  node_array<int> INDEG(G, 0); // and count indegrees
  node v;
  forall_nodes(v, G) 
    if((INDEG[v] = G.indeg(v)) == 0) 
      ZEROINDEG.append(v);


  // while there is a node with indegree = 0, pop it and assign next
  // topological number to it
  int counter = 0 ;
  while(!ZEROINDEG.empty()) { 
    v = ZEROINDEG.pop();
    top_num[v] = ++counter;
    // decrement indegree of all adjacent nodes w
    // append w to ZEROINDEG if indegree becomes 0
    node w;
    forall_adj_nodes(w, v)
      if(--INDEG[w] == 0) 
        ZEROINDEG.append(w);
  }

  // G has a topological sorting iff all nodes have been numbered
  return counter == G.number_of_nodes();
}

Da jeder Knoten v nur einmal nach ZEROINDEG aufgenommen wird und jede Kante (v,w) nur einmal betrachtet wird, hat dieses Verfahren Laufzeit O(n+m).

Übungen

Übung 61.

Wenn TOPSORT() endet, ohne dass jeder Knoten eine Nummer erhalten hat, so besitzt der Graph einen Zyklus. Jeder Knoten, der dann noch keine Nummer hat, liegt in einem Zyklus: Jeder Knoten im Restgraphen hat Eingangsgrad > 0, also einen Vorgänger; daher kann man bei einem beliebigen Knoten starten und immer zu einem beliebigen Vorgänger weiter wandern, bis man auf einen Knoten stösst, der schon einmal besucht wurde. Dann hat man mit dem so durchlaufenen Pfad einen Zyklus gefunden.

Manchmal ist es wichtig, einen solchen Zyklus zu erkennen und zu beseitigen, etwa beim statischen Linken eines Programmes gegen mehrere Bibliotheken. Diese müssen dem Linker in der (umgekehrten) Reihenfolge einer Topologischen Sortierung ihrer gegenseitigen Abhängigkeiten angegeben werden (und dürfen somit keine zirkulären Abhängigkeiten enthalten).

Erweitern Sie TOPSORT() so, dass es zusätzlich Zyklen erkennt und zurückliefert.