Donnerstag, 27. März 2014

Travel-Salesman Problem - ein paar Algorithmen


Naive Version – Kürzeste Distanz in Hin und Rückrichtung

(Komplexität in average-c ase ermittelt)
1.       Für jeden Punkt bestimme Abstand zu n anderen Punkten = O(n^2)
Datenstruktur: HashMap P auf Punkte; Jeder Punkt a enthält Liste mit Tupeln T(a)
wobei gilt: t(a,b) = (b,d(a,b)), a,b \in P; T(a) = { t(a, .) }
2.       Setze Cursor p, s auf Startposition.
3.       Für einen  Punkte p in HashMap P:       (Hinrichtung)
a.       Wähle nächsten Nachbarn q \in T(p) aus
mit Bedingung: q \in P .
-->  O(k)  mit k=n..1 = |T(p)| = O(n)     (ref1)
b.      Füge Kante (p,q) zum Ergebnis erg hinzu
c.       Lösche q aus P, lösche alle t(q,x) \in T(x) mit x \in P
-->
O( |P|*|T|) = O(n^2)      (ref2)
4.       Für einen  Punkte r \in P:         (Rückrichtung)
a.       Wähle nächsten Nachbarn s \in T(r)
Wenn q==s ist und P nicht leer, dann wähle nächstes naheliegendes s.
-->
O(n)
b.      Füge Kante (r,s) zum Ergenis erg hinzu.
c.       Lösche s aus P, lösche alle t(s,x) \in T(x) mit x \in P
-->
O(n^2)
5.       Setze p=q  und r=s und wiederhole ab 3.
Wenn p==r ist und P ist leer, dann haben wir den Weg gefunden.
Da wir (ref1) und (ref2) erst für n=|P| dann für n-1, n-2, … machen müssen ergibt sich O(n*(n+n^2)) im ersten Schritt und O((n-1)*(n+n^2)) im zweiten Schritt und so weiter.
Wir erhalten also O(n!).
Leider ist die Suche nicht optimal. Es kann zu Überkreuzungen kommen, und dieses wäre dann keine optimale Strecke. Siehe dazu folgende Abbildung: 


Abbildung 1 -  Weg mit Überkreuzung
schwarz,dünn=Hinrichtung und rot,dick=Rückrichtung
Der Fehler der Prozedur kann beliebig groß werden.

 

Naive Version – Aufspannen eines Suchbaums.

Komplexität dieses naiven Algorithmus ist O(n!) in Average-Case.

1.       Man wählt einen beliebigen Knotenpunkt a \in P als Wurzel und starte folgende rekursive Prozedur:
2.       Entferne a aus P
3.       Für alle b \in P:
a.       Füge t(a,b) = (b,d_a+d(a,b)) als Kind von a ein.
(mit d_a die Distanz vom Elternelement)
b.      Für alle t(a,r) \in T(a):
(heißt für alle Kinder wiederholen)
c.       Rufe rekursiv ab 2. auf, mit a = r                                                            --> O(n!)
4.       Durchlaufe den Baum und bestimme die Strecke (\sum_a,b{d(a,b)}).                          --> O(n!)
Speichere die Strecke als Blatt ab.
Merke dir dabei den kürzesten Weg.

Der Algorithmus hat keinen beliebig großen Fehler. Er liefert das Optimale Ergebnis. Hat aber in fast jedem Schritt O(n!).


Advanced Version – Algorithmus mit Ackermannfunktion

Komplexität dieses advanced Algorithmuses ist O(n). Es wird jedoch keine optimale Lösung gefunden. Ich schätze den Fehler auf O(Af); Af = Ackermannfunktion.

1.       Man wählt einen beliebigen Knoten a als Wurzelknoten aus und folgt folgender rekursiver Prozedur – mit l=1:
2.       Entferne a aus P
3.       Für alle b \in P:
a.       Berechne d für Af(l) Elemente x \in P: d=sum_x{d(x_i,x_i+1)} mit x_i+1 nächstes (geographisch) Element zu x_i.
b.      Speicher t(a,b) mit (b,d) (d.h. speichere die berechnete Distanz in ein Kindelement)
c.       Speichere außerdem eine Liste von Knoten, die den Weg für Af(n) Elemente beschreiben.
d.      l=l+1; Rufe rekursiv ab 2. auf mit a=b
4.       Suche über die Blätter die kleinste Distanz d. Die gespeicherten Wege beschreiben den „Round-Trip“.

Die Komplexität aus 3.a ergibt O(n) , da die nächstliegenden Af(l) Elemente berechnet werden.
Af(l) ist dabei die modifizierte Ackermannfunktion für Zweier-Potenzen. Dies ergibt Werte wie 2, 4, 8, 16, 65536.
D.h. wir spannen nur einen Baum der Größe der inversen Ackermannfunktion auf. Und die Inverse-Ackermannfunktion wird generell als maximal 5 gesehen.
Es ergibt sich also eine Komplexität von O(5*n) = O(n).
Die Suche nach dem kürzesten Weg ist dann O(n*(n-Af(1))*(n-Af(2))*(n-Af(3))*(n-Af(4))*(n-Af(5)) = O(n^5) .
Den Fehler kann man bei dieser geringen Komplexität gut einkreisen, indem man mehrmals den Algorithmus laufen lässt mit unterschiedlichen a am Start. Das löst gleichzeitig das Problem der Anfangskonfiguration bei heuristischen Verfahren.

Ich hoffe es war verständlich und hilfreich. Ich wünsche noch viel Spaß. Vielleicht gelingt es mir auch mal das Verfahren zu implementieren.



Viele Grüße, Sebastian

Sonntag, 23. März 2014

Data-Format matters

Hi,

Es heißt Suche ist im Average-Case in O(n*log(n)) auf einer unsortiertem Array machbar.
Das Array wird sortiert in O(n*log(n)) Schritten und dann die Suche in O(log(n)) Schritten (Beispiel Binärsuche).
Ist das Array schon sortiert, kann die Suche in O(log(n)) Schritten im Average-Case erfolgen.
Wir sehen: Das Dateiformat ist entscheident.
Weiter ist zu beachten, dass es besser ist die Elemente in einem Binärbaum ab zu speichern als in ein Array, denn: das Einfügen geschieht nicht in O(1), sondern in O(n) Schritten. Sie müssen jedesmal das Array neu kopieren um ein Element einzufügen. Für den Baum hingegen ist es nur notwendig einen neuen Knoten bei den Blättern im Baum einzufügen.
Wir fassen zusammen

                     Baum,        Liste,      Array,       Hashmap
Einfügen:      O(1),         O(1),       O(n),         O(1)
Suchen:         O(log(n)), O(n),       O(log(n)), O(1)

Wir sehen: Ein Array ist keine gute Wahl, weil das Einfügen zu lange dauert. Eine Liste ist nicht gut, weil das suchen zu lange dauert. Besser ist nur der Baum. Und optimal ist die Hashmap. Nur hat die Hashmap nur im Average-Case O(1) für suchen und einfügen. Gelegentlich muss die Hashmap vergrößert werden, das kostet dann O(n). Während das Vergrößern des Baums nur O(1) bedeutet.
Es kann natürlich dazu führen, dass man einen degenerierten Baum bekommt.
Dagegen hilft dann automatisches ausgleichen durch Knotenrotation. Der so genannte "AVL-Baum".
(http://de.wikipedia.org/wiki/AVL-Baum)
Das Ausgleichen des Baums kann im Durchschnitt wieder zuviel kosten. Daher gibt es den Fibonacci-Baum, welcher nicht "ständig" beim Einfügen rotiert.
(http://de.wikipedia.org/wiki/Fibonacci-Baum)

Meine These ist:Setzen wir die Anzahl der Kinder (s) pro Knoten auf s(x_k) = 2^(x_k) mit x_k = Tiefe des Knoten (k) und speichern den Wertebereich der Kinder, statt nur den Wert des Knotens, so bekommen wir für die Suche O(log(log(n))).
Im letzten Schritt muss noch durch die Liste der Kinder hindurch gegangen werden. Dies Liste ist unsotiert und benötigt theoretisch im worst-case O(n) Schritte. Da wir aber den average-case betrachten und sich die neue Tiefe durch log(log(n)) ergibt, sind auch nur log(log(n)) Elemente in der Liste der Kinder.

Verwendet man nun die Ackermannfunktion für s, dann ist ein rotieren nicht mehr notwendig, da die Tiefe praktisch beschränkt ist, da die Ackermannfunktion sehr schnell anwächst und in einer Tiefe von 5 schon alle Atome des Universums enthalten seien können.
Diese Tiefe ist wahrscheinlich zu klein.
Daher empfehle ich besser die Fibonacci-Funktion zu verwenden. Ein rotieren wäre dann wieder notwendig, würde aber deutlich seltener vorkommen.

Es zeigt sich, dass es möglich ist die Komplexität entweder in den Daten oder im Algorithmus darzustellen.

Ich gehe noch einen Schritt weiter und behaupte, dass sich das Travel-Sales-Man Problem in polynomieller Zeit lösen lässt, wenn man entsprechende Datenstrukturen als Grundlage hat.
Dazu ein anderes mal mehr.

Viele Grüße, Seboeh