Das Travelling-Salesman-Problem (TSP)
Gesucht wird in einem Graphen G über eine Menge V ein
Algorithmus, der den kürzesten Weg errechnet, der von einem Knoten
u
zu einem Knoten v führt und alle anderen Knoten mindestens
einmal besucht.
Der Name der Problemstellung bezieht sich auf die alltägliche
Situation eines Handlungsreisenden, eine Reihe von Kunden besuchen zu müssen.
Er stellt fest, in welchen Städten die Kunden zu besuchen sind und
versucht eine Route zu finden, die möglichst kurz ist. Im Allgemeinem
ist dieses Problem in allen ähnlichen Aufgabenstellungen enthalten,
bei denen es um Tourenplanung geht; es wird also ständig von Handlungsreisenden,
Speditionen, usw. zu lösen sein. Um so bedauernswerter ist es, daß
wir keinen Algorithmus angeben können, der das Problem vollständig
in polynominaler Zeit löst.
In der ursprünglichen Formulierung des TSPs sind Anfangs- und
Endpunkt der Tour identisch, und es wird gefordert, jede Stadt auch nicht
mehr als nur einmal zu besuchen. Dies ist eine Einschränkung, die
dazu führt, daß es in vielen Fällen überhaupt keine
Lösung gibt. Daher beschäftigen wir uns mit dem TSP in der oben
angegebenen allgemeineren Formulierung.
In dem Beispiel gibt es zum Beispiel keine
Lösung des TSP für eine gesuchte Tour von San Francisco nach
Palo Alto, die alle Städte nur einmal besucht. Es gibt aber eine Lösung
mit einem Zusatzbesuch von zum Beispiel San Francisco. Zur Lösung
des TSPs generieren wir alle möglichen Wege von u nach v,
die alle Städte besuchen und jede höchsten AriMax mal,
also benutzen wir AriMax als Schranke für die Anzahl der Besuche
einer Stadt. Zur Generierung aller Wege können wir einen Algorithmus
verwenden, der Ähnlichkeit mit der Tiefensuche hat. Bei ihm kam es
jedoch darauf an, daß alle Knoten aufgezählt werden, aber nicht
entlang eines Weges, was aber beim TSP der Fall ist. Wir müssen die
Knoten eines Graphen derart aufzählen, daß dabei ein Weg entsteht.
Dies kann man durch Rücksetzen der gemachten Besuche erreichen, wenn
man in eine Sackgasse gerät.
.
Algorithmus:
TSP(StartIndex, EndIndex: IndexType; var Distance :Real; AriMax: Integer);
var Schranke, AktWegLaenge :Real;
WegeMax :IndexType;
type TMinEntfs = array[KnotenAnzahl] of IndexType;
var MinWeg :TMinEntf;
Besuche(k: IndexType; var Weg :TMinEntfs; maxIndex: IndexType);
var BisherigeLaenge :Real;
Knoten :PKnoten;
n :IndexType;
begin
inc(maxIndex)
Weg[maxIndex] = k
if erster Durchlauf then
AktWegLaenge = 0
BisherigeLaenge = 0
else
BisherigeLaenge = AktWegLaenge
AktWegLaenge = AktWegLaenge + Kantenwert(k, Weg[maxIndex-1])
end-if
inc(Marke(k))
if AktWegLaenge < Schranke then
if ist_eine_TSP-Lösung then
MinWeg = Weg
Schranke = AktWegLaenge
else
for-each x in (Kante von Knoten(k)) do
if Marke(Kante(k, x).ZielKnoten) < AriMax then
Besuche(Kante(k, x).ZielKnoten, Weg, maxIndex)
end-if
end-for-each
end-if
end-if
AktWegLaenge = BisherigeLaenge
Weg[maxIndex] = 0
dec(Marke(k))
end
var Weg :TMinEntfs;
KnotenNr, WegIndex, n :IndexType;
begin
WegeMax = AriMax * FCount + 3
KantenCount = 0
Schranke = 0
for-each x in Kanten do
Schranke := Schranke + x.Wert
end-for-each
for n = 1 to KnotenAnzahl do
Weg(n) = 0
MinWeg(n) = 0
Marken(n) = 0
end-for
Besuche(StartIndex, Weg, 0)
Distance = Schranke
end
.
Das heißt:
Zur Lösung des TSPs generieren wir alle möglichen Wege von u
nach v, die alle Städte besuchen und jede höchsten AriMax
mal. Zum Generieren aller Wege verwenden wir einen Tiefensuchen-ähnlichen
Algorithmus. Die Knoten müssen derart aufgezählt werden, daß
dabei ein Weg entsteht. Dies kann man durch Rücksetzen der gemachten
Besuche erreichen, wenn man in eine Sackgasse gerät. Der kürzeste
Weg wird gespeichert und nach durchlaufen aller möglichen Wege steht
der kürzeste Weg im Speicher zur Verfügung.
.
Beispielrealisierung (in gekürzter Fassung):
Die Implementation basiert auf
diesen Definitionen.
function TGraph.TravellingSalesmanProblem(StartIndex, EndIndex: IndexType;
var KantenCount: IndexType; var Distance :Real; AriMax: Integer): Boolean;
var Schranke :Real;
HatWeg :Boolean;
WegeMax :IndexType;
AktWegLaenge :Real;
type TMinEntfs = array[1..1] of IndexType;
var MinWeg :^TMinEntfs;
function TesteWeg(var Weg :TMinEntfs; maxIndex: IndexType): Boolean;
var k :IndexType;
begin // über alle Knoten den Zielknoten erreicht?
Result := (Weg[maxIndex] = EndIndex) and (maxIndex > FCount-1);
k := 1;
while (k <= FCount) and Result do
begin
Result := PointItem(k).Marke <> 0;
inc(k);
end;
end;
procedure Besuche(k: IndexType; var Weg :TMinEntfs; maxIndex: IndexType);
var BisherigeLaenge :Real;
Knoten :PKnoten;
n :IndexType;
begin
inc(maxIndex);
if maxIndex >= WegeMax-2 then
raise Exception.Create('Der Weg wurde zu lang ...');
Weg[maxIndex] := k;
if maxIndex = 1 then // allererster Aufruf?
begin
AktWegLaenge := 0;
BisherigeLaenge := 0;
end
else
begin
BisherigeLaenge := AktWegLaenge;
Knoten := PointItem(Weg[maxIndex-1]);
if Knoten.KantenCount > 0 then // inc(AktWegLaenge, Kantenwert)
for n := 0 to Knoten.KantenCount-1 do
if Knoten.Kanten[n].DestKnoten = k then
AktWegLaenge := AktWegLaenge + Knoten.Kanten[n].Value;
end;
Knoten := PointItem(k);
inc(Knoten.Marke);
if AktWegLaenge < Schranke then // bereits gegangener Weg innerhalb Grenze?
if TesteWeg(Weg, maxIndex) then // Ziel bereits erreicht?
begin // Aktuellen Weg umkopieren
Move(Weg, MinWeg^, (maxIndex+1) * SizeOf(Weg[1]));
Schranke := AktWegLaenge;
HatWeg := True;
end
else // Besuche jeden Nachbarknoten
if Knoten.KantenCount > 0 then
for n := 0 to Knoten.KantenCount-1 do
if PointItem(Knoten.Kanten[n].DestKnoten).Marke < AriMax then
Besuche(Knoten.Kanten[n].DestKnoten, Weg, maxIndex);
AktWegLaenge := BisherigeLaenge; // die Rekursion rückgängig machen
Weg[maxIndex] := 0;
dec(PointItem(k).Marke);
end;
procedure CalculateSchranke;
var Counter, n :IndexType;
begin // Summe der Gewichtung sämtlicher Kanten
Schranke := 0;
if FStart <> nil then
for Counter := 1 to FCount do
with PointItem(i)^ do
if KantenCount > 0 then
for n := 0 to KantenCount - 1 do
Schranke := Schranke + Kanten[n].Value;
end;
var Weg :^TMinEntfs;
KnotenNr, WegIndex, n :IndexType;
Knoten :PKnoten;
begin
WegeMax := AriMax * FCount + 3;
KantenCount := 0;
Distance := 0;
GetMem(MinWeg, sizeof(MinWeg[1]) * WegeMax);
try
CalculateSchranke;
GetMem(Weg, sizeof(Weg[1]) * WegeMax);
try
FillChar(Weg^, sizeof(Weg[1]) * WegeMax, 0);
FillChar(MinWeg^, sizeof(MinWeg[1]) * WegeMax, 0);
ZeroKnotenMarken;
HatWeg := False;
Besuche(StartIndex, Weg^, 0); // starten der Rekursion
KantenCount := 0;
ZeroKnotenMarken;
ZeroKantenMarken;
if HatWeg then // markieren des Weges
begin
KnotenNr := MinWeg[1];
WegIndex := 2;
Knoten := nil;
while KnotenNr >= 1 do
begin
if assigned(Knoten) then
begin
if Knoten.KantenCount > 0 then
for n := 0 to Knoten.KantenCount-1 do
if Knoten.Kanten[n].DestKnoten = KnotenNr then
inc(Knoten.Kanten[n].Marke);
inc(Knoten.Marke);
end;
Knoten := PointItem(KnotenNr);
KnotenNr := MinWeg[WegIndex];
inc(WegIndex);
inc(KantenCount);
end;
if assigned(Knoten) then
inc(Knoten.Marke);
Distance := Schranke;
end;
finally
FreeMem(Weg, sizeof(Weg[1]) * WegeMax);
end;
finally
FreeMem(MinWeg, sizeof(MinWeg[1]) * WegeMax);
end;
Result := HatWeg;
end;
.
Effizienzbetrachtung:
Die Suche nach einer Lösung des TSPs gelingt bei 15 Städten
in rund 0,71 Sekunden wenn man nur ein Besuch pro Stadt zuläßt.
Wenn wir zulassen, daß jede Stadt bis zu zweimal besucht werden darf,
wächst der Rechenaufwand ganz erheblich auf 17,94 Sekunden.
Bei gelichteten Graphen von nur zehn Städten, wie im Beispiel,
braucht man bei einmaligen Besuch 0,31 Sekunden, beziehungsweise
bei bis zu doppelten Besuchen 0,67 Sekunden. Die Zahl der
generierten Wege bei 15 Städten und maximal einem Besuch liegt bei
483, bei bis zu zwei Besuchen sind jedoch 3.832.913 Wege erforderlich,
was an der Vielzahl von unsinnigen Doppelbesuchen, die der Algorithmus
nicht als solcher erkennt, liegt. Die oben genannten Anzahl der
generierten Wege ergibt sich, wenn man die Schranke automatisch
abschätzen läßt. Mit einer kleineren Schranke erniedrigt
sich die Zahl der Wege erheblich.
Die Laufzeit L des Programms ist Proportional zu 2N,
also gilt L = c * 2N mit einer geeigneten Konstante
c.
Bei 15 Städten und einer Laufzeit 0,71 Sekunden müßte
c
ungefähr bei 0,71s = c * 215 =>c = 0,71s /
215 = 0,71s / 32768 = 0,00002166... Bei einem Graphen
mit 40 Knoten wäre so eine Laufzeit L von 0,00002166...
* 240 = 23823646,72 s = 6617,6796444.... h = 275,73665185185...
d, also über 9 Monate, zu erwarten. Mit jeder zusätzlicher
Stadt verdoppelt sich die Laufzeit. Das bedeutet, daß auch bessere
Computer oder gar der allgemeine Fortschritt in der Technik hier nicht
großartig hilft. Man kann jedes Jahr ungefähr eine Stadt mehr
in der gleichen Zeit berechnen.
In einem Graphen, bei dem von jedem Knoten genau zwei Kanten wegführen,
gibt es offensichtlich genau 2N Wege der Länge N.
In diesem Fall ist die Abschätzung der Laufzeit durch 2N
leicht einzusehen. In anders strukturierten Graphen muß anders argumentiert
werden, aber normalerweise wird sich jedesmal eine exponentiell wachsende
Laufzeit ergeben.
.
Optimierungsvorschläge:
Man könnte dem Algorithmus mitteilen, welche Städte doppelt besucht
werden könnten und bei welchen es sinnlos ist. So könnte
man eine Menge sinnloser Wege sparen. Offensichtlich kann man sehr
viel Laufzeit sparen, wenn man die Suche auf einfache Wege beschränkt.
Trotzdem kann man nicht hoffen, umfangreiche TSPs mit diesem Programm berechnen
zu können. Man kann das Programm sicherlich noch optimieren, was aber
keinen nennenswerten Einfluß auf die Laufzeit haben wird, denn diese
entsteht bei der Generierung aller möglichen Wege.