image: teaser.webp kind: projekt language: Ruby / Python languages: [ruby, python] topic: computing duration: 6 Doppelstunden template: https://github.com/specht/routenplaner-starter.git template_name: der Vorlage des Routenplaners reference: [graphen, heap]

Ein Routenplaner für Berlin

Du baust einen Routenplaner wie den in deinem Handy, mit echten Karten von OpenStreetMap: Dein Programm macht aus den Straßen und Wegen von Berlin einen Graphen und findet darin den kürzesten Weg, von deiner Schule zum Botanischen Garten oder quer durch die Stadt zum Brandenburger Tor. Erst in Steglitz-Zehlendorf, dann in jedem anderen Bezirk und in ganz Berlin, wo der schnelle Weg auch eine schnelle Suche braucht.

1. Einrichten

Repository klonen

Für diese Anleitung brauchst du die Vorlage des Routenplaners:

Stelle zuerst sicher, dass du keinen Ordner geöffnet hast. Um sicherzugehen, drücke einfach den Shortcut für »Ordner schließen«: StrgK und dann F. Dein Workspace sollte jetzt ungefähr so aussehen:

Ein leerer Workspace ohne geöffneten Ordner.

Klicke auf »Clone Repository« und füge die Adresse der Vorlage ein:

https://github.com/specht/routenplaner-starter.git

Das Eingabefeld für die Adresse des Repositorys.

Bestätige mit Enter. Danach wählst du das Verzeichnis, in dem das Repository gespeichert werden soll: den Ordner /workspace.

Die Auswahl des Zielordners /workspace.

Das Repository wird jetzt in einen Unterordner von /workspace geklont, der so heißt wie das Repository. Beantworte die Frage, ob es jetzt geöffnet werden soll, mit »Open«:

Die Frage, ob das geklonte Repository geöffnet werden soll.

Den Server starten

Der Routenplaner läuft im Browser. Die Seite mit der Karte bekommt er von einem kleinen Server in Ruby, der mit Sinatra gebaut ist. Sinatra installierst du einmal, es bleibt in deinem Workspace:

gem install sinatra rackup puma

Dann startest du den Server:

ruby server.rb

Im Terminal läuft server.rb: steglitz-zehlendorf: 45181 Wege, 126 Orte, »Noch nicht fertig: read_ways«, die Adresse der Karte zum Anklicken, und Sinatra hat auf Port 4567 begonnen.

Der Routenplaner läuft im Browser. Die Seite mit der Karte bekommt er von einem kleinen Server in Python, der mit Flask gebaut ist. Flask installierst du einmal, es bleibt in deinem Workspace:

pip install flask

Dann startest du den Server:

python3 server.py

Im Terminal läuft server.py: steglitz-zehlendorf: 45181 Wege, 126 Orte, »Noch nicht fertig: read_ways« und die Adresse der Karte zum Anklicken.

Der Server sagt, was er gefunden hat: 45 181 Wege in Steglitz-Zehlendorf und 126 Orte. Dass read_ways noch nicht fertig ist, stimmt: Das schreibst du gleich selbst. Der Server läuft so lange, bis du ihn mit StrgC anhältst.

Darunter steht die Adresse der Karte, http://127.0.0.1:4567. Klicke mit gedrückter Strg-Taste darauf: Visual Studio Code leitet den Port 4567 an eine Adresse weiter, die dein Browser erreicht, und öffnet sie.

Die Karte von Steglitz-Zehlendorf im Browser, gezeichnet aus den Daten: Hauptstraßen gelb, die Autobahn orange, Wohnstraßen weiß; beschriftet sind Sehenswürdigkeiten wie der Botanische Garten, das Rathaus Zehlendorf und der Bahnhof Wannsee. Links oben die Felder für Start und Ziel, unten rechts der Maßstab.

Ziehen verschiebt die Karte, das Mausrad und die Knöpfe rechts oben zoomen, ein Doppelklick holt eine Stelle heran. Oben links wählst du Start und Ziel: Tipp den Anfang eines Namens, und die Seite schlägt Sehenswürdigkeiten, Plätze und Schulen in Steglitz-Zehlendorf vor. Adressen gibt es mit Absicht nicht. Die »Legende« darunter sagt, welche Farbe welche Art von Weg ist. Einen Weg findet die Seite noch nicht; das bringst du ihr bei.

Die Vorlage hat die Karte von Steglitz-Zehlendorf. Jeden anderen Bezirk lädt der Server beim ersten Start selbst herunter, wenn du ihn so startest:

AREA=mitte ruby server.rb

Die Namen der Bezirke: charlottenburg-wilmersdorf, friedrichshain-kreuzberg, lichtenberg, marzahn-hellersdorf, mitte, neukoelln, pankow, reinickendorf, spandau, steglitz-zehlendorf, tempelhof-schoeneberg, treptow-koepenick. Ganz Berlin heißt berlin, aber damit warte lieber, bis deine Suche schnell ist.

2. Die Karte

Die Karte kommt von OpenStreetMap, einer Karte der ganzen Welt, an der alle mitzeichnen dürfen. Sie besteht aus Wegen: Jede Straße, jeder Fußweg, jede Treppe ist eine Folge von Punkten. Wo sich zwei Wege kreuzen, teilen sie sich einen Punkt.

Öffne data/steglitz.tsv. Jede Zeile ist ein Weg, die vier Teile trennt ein Tabulator:

residential     Schloßstraße  26735834 13.326700,52.456700 26735901 13.325300,52.457700 …
Teil Bedeutung
Art was für ein Weg: residential (Wohnstraße), primary (Hauptstraße), footway (Fußweg), cycleway (Radweg), steps (Treppe), …
Einbahnstraße yes, wenn man nur in Richtung der Punkte fahren darf, -1 nur dagegen, sonst leer
Name der Name der Straße, oft leer
Punkte je eine ID und die Lage als Länge und Breite in Grad, mit Komma dazwischen

Die ID macht den Punkt eindeutig: Zwei Wege mit derselben ID an einem Punkt sind an dieser Stelle verbunden. Länge und Breite sind die Koordinaten, wie in jeder Karten-App: Steglitz liegt bei 13,3 Grad östlicher Länge und 52,45 Grad nördlicher Breite.

Aufgabe 1Wie viele Fußwege?

Wie viele Wege in data/steglitz-zehlendorf.tsv sind Fußwege, also von der Art footway?

Tipp

grep -c zählt die Zeilen, auf die ein Muster passt: grep -c "^footway" data/steglitz-zehlendorf.tsv. Das ^ heißt »am Anfang der Zeile«.

Jetzt liest dein Programm die Datei. read_ways in routes.rb soll für jeden Weg einen Hash zurückgeben, mit der Art, der Einbahnstraße, dem Namen und den Punkten. Ein Punkt ist ein Array aus ID, Länge und Breite:

{ kind: 'residential', oneway: '', name: 'Schloßstraße',
  points: [['26735834', 13.3267, 52.4567], ['26735901', 13.3253, 52.4577]] }

File.readlines(path, chomp: true) gibt die Zeilen ohne Zeilenumbruch, split("\t") teilt eine Zeile an den Tabulatoren. Die Punkte teilst du an den Leerzeichen und nimmst je zwei Teile zusammen: each_slice(2). to_f macht aus '13.326700' die Zahl 13.3267.

Jetzt liest dein Programm die Datei. read_ways in routes.py soll für jeden Weg ein Dictionary zurückgeben, mit der Art, der Einbahnstraße, dem Namen und den Punkten. Ein Punkt ist eine Liste aus ID, Länge und Breite:

{'kind': 'residential', 'oneway': '', 'name': 'Schloßstraße',
 'points': [['26735834', 13.3267, 52.4567], ['26735901', 13.3253, 52.4577]]}

for line in open(path, encoding='utf-8') geht die Zeilen durch, line.rstrip('\n') nimmt den Zeilenumbruch weg, split('\t') teilt eine Zeile an den Tabulatoren. Die Punkte teilst du an den Leerzeichen; items[0::2] sind dann die IDs, items[1::2] die Lagen, und zip nimmt beide zusammen. float macht aus '13.326700' die Zahl 13.3267.

Ausprobieren kannst du read_ways mit der kleinen Testkarte data/test.tsv: drei Wege, fünf Punkte.

Aufgabe 2Die Wege lesen

Schreib read_ways in routes.rb.

Aufgabe 2Die Wege lesen

Schreib read_ways in routes.py.

3. Wie weit ist es?

Für den kürzesten Weg braucht dein Programm Abstände in Metern. Die Koordinaten sind aber in Grad. Die Erde hat einen Radius von etwa 6 371 km; ein Grad ist ein 360stel ihres Umfangs:

$$1° = \frac{2\pi \cdot 6\,371\,000\ \text{m}}{360} \approx 111\,195\ \text{m}$$

Das gilt für die Breite, nach Norden. Nach Osten stimmt es nur am Äquator: Die Längengrade laufen an den Polen zusammen, und je weiter nördlich man ist, desto näher liegen sie beieinander, um den Faktor \(\cos(\text{Breite})\). In Berlin ist ein Grad Länge nur gut 67 km lang.

Aufgabe 3Ein Grad ist nicht gleich ein Grad

Warum ist in Berlin ein Grad Länge (nach Osten) kürzer als ein Grad Breite (nach Norden)?

Ein Grad ist nicht gleich ein Grad

Auf den paar Kilometern einer Stadt ist die Erde fast flach. Du rechnest beide Unterschiede in Meter um und nimmst den Satz des Pythagoras:

$$d = \sqrt{(\Delta\text{Länge} \cdot \cos(\text{Breite}))^2 + (\Delta\text{Breite})^2} \cdot \frac{\pi}{180} \cdot 6\,371\,000\ \text{m}$$

Die Winkel sind dabei im Bogenmaß: Der Cosinus (Math.cos in Ruby, math.cos in Python) und die Umrechnung mit \(\pi / 180\) erwarten keine Grad. Als Breite nimmst du die Mitte zwischen den beiden Punkten.

Aufgabe 4Wie weit ist es?

Schreib distance in routes.rb.

Aufgabe 4Wie weit ist es?

Schreib distance in routes.py.

4. Vom Weg zum Graphen

Ein Routenplaner sucht nicht in Wegen, sondern in einem Graphen: Punkte, die Knoten, und Verbindungen zwischen ihnen, die Kanten. Jede Kante hat eine Länge in Metern. Aus jedem Weg werden so viele Kanten, wie er Abschnitte hat: zwischen dem ersten und dem zweiten Punkt, dem zweiten und dem dritten und so weiter. Ein Fußweg, der zwei Straßen verbindet, wird zu Kanten, die über die gemeinsamen Punkte an beide anschließen.

build_graph bekommt die Wege und gibt zwei Hashes zurück: nodes mit der Lage jedes Punktes, edges mit seinen Nachbarn und dem Abstand zu jedem. Kanten gelten erst einmal in beide Richtungen; um Einbahnstraßen kümmerst du dich später:

{ nodes: { '2' => [13.301, 52.45], … },
  edges: { '2' => [['1', 67.8], ['3', 67.8], ['4', 111.2]], … } }

each_cons(2) gibt immer zwei aufeinanderfolgende Punkte eines Weges. Hash.new { |hash, id| hash[id] = [] } ist ein Hash, der für einen neuen Schlüssel von selbst ein leeres Array anlegt.

build_graph bekommt die Wege und gibt zwei Dictionaries zurück: 'nodes' mit der Lage jedes Punktes, 'edges' mit seinen Nachbarn und dem Abstand zu jedem. Kanten gelten erst einmal in beide Richtungen; um Einbahnstraßen kümmerst du dich später:

{'nodes': {'2': [13.301, 52.45], …},
 'edges': {'2': [['1', 67.8], ['3', 67.8], ['4', 111.2]], …}}

zip(points, points[1:]) gibt immer zwei aufeinanderfolgende Punkte eines Weges. edges.setdefault(id, []) gibt die Liste der Nachbarn von id und legt sie an, wenn es sie noch nicht gibt.

Aufgabe 5Vom Weg zum Graphen

Schreib build_graph in routes.rb.

Aufgabe 5Vom Weg zum Graphen

Schreib build_graph in routes.py.

Halte den Server mit StrgC an und starte ihn neu. Er baut jetzt mit deinen Funktionen den Graphen von Steglitz-Zehlendorf. Wähle auf der Seite unter »Ansicht« den »Graph«: Über der blassen Karte siehst du, was build_graph gebaut hat, jeden Knoten als Punkt und jede Kante als Linie. Klicke auf eine Kreuzung: Die Seite zeigt den Knoten dort und seine Nachbarn.

Der Graph über der blassen Karte, nah herangezoomt an der Kreuzung Kuhligkshofstraße und Albrechtstraße: die Knoten als blaue Punkte, die Kanten als blaue Linien. Der angeklickte Knoten ist pink, seine vier Nachbarn sind mit pinken Linien verbunden; oben links steht, dass er 4 Nachbarn hat und wie weit sie entfernt sind.

Eine Straße ist im Graphen mehr als ihre Kreuzungen: Wo sie eine Kurve macht, hat sie Punkte, die nur ihre Form zeichnen. Auch das sind Knoten, mit genau zwei Nachbarn.

5. Breitensuche

Jetzt sucht dein Programm einen Weg. Die einfachste Suche geht vom Start aus wie eine Welle: erst zu allen Nachbarn des Starts, dann zu deren Nachbarn, dann zu den Nachbarn der Nachbarn. Das heißt Breitensuche. Die Knoten, die sie gefunden, aber noch nicht weiterverfolgt hat, stehen in einer Queue: Wer zuerst hineinkommt, ist zuerst wieder dran.

queue.shift nimmt den ersten Knoten heraus, queue << node stellt einen hinten an.

Damit die Suche am Ende den Weg kennt, merkt sie sich für jeden gefundenen Knoten, von welchem aus sie ihn gefunden hat: seinen Vorgänger. In previous steht für den Start nil, für jeden anderen gefundenen Knoten sein Vorgänger:

previous = { '11' => nil, '12' => '11', '21' => '11', '13' => '12', … }

queue.append(node) stellt einen Knoten hinten an. Vorne herausnehmen ginge mit queue.pop(0), aber das schiebt jedes Mal die ganze Liste nach vorne; schneller merkst du dir in first, welcher Knoten als Nächstes dran ist, und zählst weiter: node = queue[first], dann first += 1.

Damit die Suche am Ende den Weg kennt, merkt sie sich für jeden gefundenen Knoten, von welchem aus sie ihn gefunden hat: seinen Vorgänger. In previous steht für den Start None, für jeden anderen gefundenen Knoten sein Vorgänger:

previous = {'11': None, '12': '11', '21': '11', '13': '12', …}

So weiß die Suche auch, welche Knoten sie schon kennt: Ein Nachbar, der schon in previous steht, kommt nicht noch einmal in die Queue.

Ist die Suche am Ziel, geht der Weg vom Ziel aus rückwärts: zum Vorgänger des Ziels, zu dessen Vorgänger und so weiter, bis zum Start, der keinen Vorgänger hat. Das macht path_to. Weil du den Weg rückwärts findest, kommt jeder Knoten vorne dazu: path.unshift(node) in Ruby, path.insert(0, node) in Python.

Aufgabe 6Der Weg zurück

Schreib path_to in routes.rb.

Aufgabe 6Der Weg zurück

Schreib path_to in routes.py.

Die Breitensuche selbst, Schritt für Schritt:

  1. In der Queue steht nur der Start, in previous der Start ohne Vorgänger.
  2. Nimm den ersten Knoten aus der Queue und schreib ihn in visited. Ist es das Ziel, bist du fertig.
  3. Jeder Nachbar, der noch nicht in previous steht, bekommt den Knoten als Vorgänger und kommt hinten in die Queue.
  4. Weiter mit 2, solange die Queue nicht leer ist. Wird sie leer, ohne dass das Ziel dran war, gibt es keinen Weg.

visited braucht die Suche selbst nicht: Die Seite zeigt damit, in welcher Reihenfolge die Suche die Knoten besucht hat. Zum Ausprobieren gibt es die Testkarte data/detour.tsv: Die Lindenstraße geht in fünf Kanten geradeaus vom Knoten 11 zum Knoten 16, der Eichenweg macht einen Bogen und braucht nur drei.

Aufgabe 7Breitensuche

Schreib breadth_first in routes.rb.

Aufgabe 7Breitensuche

Schreib breadth_first in routes.py.

Starte den Server neu, wähle einen Start und ein Ziel und lass die Breitensuche suchen. Die Seite zeigt erst die Suche, jeden besuchten Knoten als Punkt, von Gelb (früh besucht) bis Rot (spät), und dann den Weg, den sie gefunden hat:

Die Breitensuche vom Gymnasium Steglitz zum Botanischen Garten: Über der blassen Karte liegen die besuchten Knoten und Kanten, gelb um die Schule, nach außen orange und rot; der gefundene Weg ist eine lila Linie. Oben links steht, wie lang er ist, über wie viele Knoten er geht und wie viele Knoten die Suche besucht hat, darunter der Schieber, mit dem man die Suche Schritt für Schritt ansieht.

Die Welle breitet sich nach Kanten aus, nicht nach Metern: Im Botanischen Garten, wo die Wege viele kurze Kanten haben, kommt sie langsam voran, auf langen geraden Straßen schnell. Darum macht ihr Weg Zacken und Umwege.

Aufgabe 8Wenige Kanten, langer Weg

Die Breitensuche findet den Weg mit den wenigsten Kanten. Warum ist das auf einer Karte oft nicht der kürzeste Weg?

Wenige Kanten, langer Weg

6. Dijkstra

Den kürzesten Weg in Metern findet die Suche von Edsger W. Dijkstra aus dem Jahr 1959. Sie nimmt als Nächstes nicht den Knoten, der am längsten wartet, sondern den, der vom Start aus am nächsten liegt. Dafür merkt sie sich für jeden gefundenen Knoten, wie weit es bis zu ihm ist, auf dem besten Weg, den sie bis jetzt kennt:

  1. metres sagt, wie weit jeder gefundene Knoten vom Start weg ist, previous wieder seinen Vorgänger, frontier die Knoten, die gefunden, aber noch nicht dran waren, mit ihrem Abstand. Am Anfang steht in allen dreien nur der Start, mit 0 Metern und ohne Vorgänger.
  2. Nimm aus frontier den Knoten mit dem kleinsten Abstand und schreib ihn in visited. Ist es das Ziel, bist du fertig.
  3. Für jeden Nachbarn: Wie weit wäre er über diesen Knoten? Sein Abstand plus die Länge der Kante. Kennst du den Nachbarn noch nicht, oder ist der Weg über diesen Knoten kürzer als der bisher beste, bekommt er den neuen Abstand und diesen Knoten als Vorgänger und kommt (wieder) in frontier.
  4. Weiter mit 2, solange frontier nicht leer ist.

Warum stimmt der Abstand, wenn ein Knoten dran ist? Alle anderen in frontier sind weiter weg, und jeder Umweg über sie macht den Weg nur länger: Kanten sind nie kürzer als 0 Meter.

frontier.min_by { |_id, m| m } gibt den Knoten mit dem kleinsten Abstand, als Paar aus ID und Metern. frontier.delete(node) nimmt ihn heraus. Am Ende baut path_to den Weg, genau wie bei der Breitensuche.

min(frontier, key=frontier.get) gibt den Knoten mit dem kleinsten Abstand, frontier.pop(node) nimmt ihn heraus und gibt seinen Abstand. Am Ende baut path_to den Weg, genau wie bei der Breitensuche.

Aufgabe 9Dijkstra

Schreib dijkstra in routes.rb.

Aufgabe 9Dijkstra

Schreib dijkstra in routes.py.

Starte den Server neu und suche denselben Weg noch einmal, jetzt mit »Dijkstra«:

Dijkstra vom Gymnasium Steglitz zum Botanischen Garten: Die besuchten Knoten liegen gelb nah an der Schule und werden in alle Richtungen gleichmäßig orange und rot, wie Ringe um den Start. Der Weg ist eine lila Linie. Oben links stehen Länge, Knoten und wie viele die Suche besucht hat.

Jetzt breitet sich die Welle in Metern aus, wie Ringe um den Start. Der Weg ist 1,8 km lang statt 2,1 km: Er hat mehr Knoten, aber keine Zacken. Wie verschieden die beiden Wege sein können, zeigt erst ein weiter Weg.

Mit dem Schieber unter dem Ergebnis siehst du dir die Suche Schritt für Schritt an: ▶ spielt sie ab, ‹ und › gehen einen Knoten zurück und vor, das Tempo »Langsam« zeigt jeden einzelnen. Hast du angehalten, bleibt der Schritt stehen, wenn du die andere Suche wählst. So vergleichst du beide an derselben Stelle.

Probier einen weiten Weg, zur Pfaueninsel ganz im Westen. Dahin kommt man nur mit der Fähre, und auch die ist ein Weg der Karte:

Die Breitensuche macht gut 7 Kilometer Umweg. Dijkstra findet den kürzesten Weg, aber schau auf die Zeit: ein paar Sekunden statt Bruchteilen davon. Beide besuchen fast den ganzen Bezirk, der Unterschied ist die Suche nach dem kleinsten Abstand: Sie geht bei jedem Schritt durch die ganze frontier, und die wird groß. Wie es schneller geht, kommt als Nächstes.

7. Eine Priority Queue

Dijkstra sucht bei jedem Schritt in der ganzen frontier nach dem kleinsten Abstand. Stehen dort tausend Knoten, sind das tausend Vergleiche, bei jedem der hunderttausend Schritte. Schneller geht es mit einer Priority Queue: Man legt Werte mit einer Priorität hinein und bekommt immer den mit der kleinsten heraus, ohne alle anzusehen.

Ein binärer Heap macht das so: Die Einträge stehen in einem Array, aber man denkt sie sich als Baum. Die Kinder des Eintrags an der Stelle i stehen an den Stellen 2 * i + 1 und 2 * i + 2, sein Elternteil an der Stelle (i - 1) / 2. Und es gilt immer: Kein Kind ist kleiner als sein Elternteil. Ganz oben, an der Stelle 0, steht deshalb das Kleinste.

  • push: Hänge den neuen Eintrag hinten an. Solange er kleiner ist als sein Elternteil, tausche die beiden; so steigt er nach oben, bis er passt.
  • pop: Nimm den Eintrag an der Stelle 0. Setze den letzten an seine Stelle. Solange eines seiner Kinder kleiner ist, tausche ihn mit dem kleineren Kind; so sinkt er nach unten.

Beides braucht nur so viele Schritte, wie der Baum hoch ist: bei hunderttausend Einträgen 17.

In routes.rb gibt es die Klasse MinHeap schon, mit dem Array @items für Paare aus Priorität und Wert. Es fehlen push und pop.

In routes.py gibt es die Klasse MinHeap schon, mit der Liste self.items für Paare aus Priorität und Wert. Es fehlen push und pop. Python hat für so etwas das Modul heapq; hier baust du den Heap selbst, damit du siehst, wie er arbeitet.

Aufgabe 10Ein Heap

Schreib push und pop der Klasse MinHeap in routes.rb.

Aufgabe 10Ein Heap

Schreib push und pop der Klasse MinHeap in routes.py.

Dann bekommt dijkstra den Heap statt frontier. Ein Knoten kann jetzt mehrmals im Heap stehen: Findet die Suche einen kürzeren Weg zu ihm, legt sie ihn mit dem neuen Abstand noch einmal hinein, statt den alten Eintrag zu suchen. Kommt später der alte Eintrag heraus, war der Knoten schon dran, und die Suche überspringt ihn. Dafür merkt sie sich in done, welche Knoten fertig sind:

until heap.empty?
    so_far, node = heap.pop
    next if done[node]

    done[node] = true
    visited << node
    break if node == goal

    graph[:edges][node].each do |neighbour, length|
        total = so_far + length
        next if costs.key?(neighbour) && costs[neighbour] <= total

        costs[neighbour] = total
        previous[neighbour] = node
        heap.push(total, neighbour)
    end
end
while not heap.empty():
    so_far, node = heap.pop()
    if node in done:
        continue
    done.add(node)
    visited.append(node)
    if node == goal:
        break
    for neighbour, length in graph['edges'].get(node, []):
        total = so_far + length
        if neighbour in costs and costs[neighbour] <= total:
            continue
        costs[neighbour] = total
        previous[neighbour] = node
        heap.push(total, neighbour)

Prüfe dijkstra mit dem Knopf oben noch einmal, starte den Server neu und such den Weg zur Pfaueninsel wieder mit Dijkstra. Derselbe Weg, dieselben besuchten Knoten, aber statt ein paar Sekunden noch knapp eine.

8. A*: mit dem Ziel vor Augen

Dijkstra sucht in alle Richtungen gleich, auch weg vom Ziel. Dabei weiß die Karte, wo das Ziel liegt. A* (gesprochen »A-Stern«) nutzt das: Die Priorität eines Knotens ist nicht nur der Weg bis zu ihm, sondern der Weg bis zu ihm plus eine Schätzung für den Rest, die Luftlinie zum Ziel. Knoten in Richtung des Ziels kommen so früher dran, Knoten in der falschen Richtung später oder nie.

$$\text{Priorität} = \text{Weg vom Start bis zum Knoten} + \text{Luftlinie vom Knoten zum Ziel}$$

Der Weg bleibt der kürzeste, solange die Schätzung nie zu groß ist: Kein Weg auf der Straße ist kürzer als die Luftlinie. Sonst ist A* genau Dijkstra, mit dem Heap. Nur kommt beim Hineinlegen die Schätzung dazu, und der Abstand eines Knotens steht in costs, nicht in der Priorität:

heap.push(total + estimate.(neighbour), neighbour)

estimate ist eine kleine Funktion für die Luftlinie, mit deinem distance und der Lage des Ziels, graph[:nodes][goal]. In Ruby schreibst du sie als Lambda: estimate = ->(id) { … }, aufgerufen mit estimate.(id).

heap.push(total + estimate(neighbour), neighbour)

estimate ist eine kleine Funktion für die Luftlinie, mit deinem distance und der Lage des Ziels, graph['nodes'][goal]. Du kannst sie innerhalb von a_star mit def schreiben.

Aufgabe 11A*

Schreib a_star in routes.rb.

Aufgabe 11A*

Schreib a_star in routes.py.

Wie viel A* spart, zeigt ein Weg quer durch die Stadt, zum Brandenburger Tor. Dafür braucht der Server drei Bezirke: Steglitz-Zehlendorf, Tempelhof-Schöneberg und Mitte. Halte ihn an und starte ihn so:

AREA=steglitz-zehlendorf,tempelhof-schoeneberg,mitte ruby server.rb
AREA=steglitz-zehlendorf,tempelhof-schoeneberg,mitte python3 server.py

Die beiden anderen Bezirke lädt er beim ersten Mal herunter. Der Graph hat dann fast 400 000 Knoten; das Lesen dauert ein paar Sekunden länger.

Derselbe Weg, 8 km, aber A* besucht nur ein Zehntel der Knoten: ein Band zum Ziel hin statt eines Kreises, und ist darum zehnmal so schnell fertig. Mit AREA=berlin hast du die ganze Stadt, 1,3 Millionen Knoten. Das Lesen dauert dann gut eine halbe Minute, aber A* findet auch dort einen Weg in Sekunden.

9. Zu Fuß, mit dem Rad, mit dem Auto

Bisher ist der kürzeste Weg gemeint, in Metern. Wer mit dem Auto fährt, will aber den schnellsten, und darf nicht überall fahren: nicht auf Fußwegen, nicht gegen Einbahnstraßen. Wer zu Fuß geht, kommt auf der Autobahn nicht weit. Dafür bekommt jede Art zu reisen ihren eigenen Graphen: nur mit den Wegen, die man so benutzen darf, und an jeder Kante, wie viele Sekunden man für sie braucht.

Wie schnell man wo vorankommt, steht in routes.rb in der Tabelle SPEEDS, in km/h: SPEEDS['car']['residential'] ist 25, in einer Wohnstraße fährt man mit dem Auto im Schnitt 25 km/h. Fehlt eine Art von Weg in der Tabelle, darf man ihn so nicht benutzen.

Wie schnell man wo vorankommt, steht in routes.py in der Tabelle SPEEDS, in km/h: SPEEDS['car']['residential'] ist 25, in einer Wohnstraße fährt man mit dem Auto im Schnitt 25 km/h. Fehlt eine Art von Weg in der Tabelle, darf man ihn so nicht benutzen.

build_travel_graph ist fast build_graph, mit drei Unterschieden:

  1. Ein Weg, dessen Art in SPEEDS[mode] fehlt, kommt nicht in den Graphen.
  2. An jeder Kante stehen Sekunden: die Meter geteilt durch das Tempo in Metern je Sekunde, also km/h geteilt durch 3,6.
  3. Für das Auto gelten die Einbahnstraßen: Bei yes gibt es nur die Kante in Richtung der Punkte, bei -1 nur die zurück. Zu Fuß und mit dem Rad gibt es beide.

Dazu gehört in den Graphen noch fastest, das größte Tempo der Tabelle in Metern je Sekunde. A* braucht es: Die Luftlinie in Metern geteilt durch fastest ist eine Schätzung in Sekunden, die nie zu groß ist, denn schneller als mit dem größten Tempo kommt man nirgends voran. Teile darum in a_star die Luftlinie durch fastest, wenn der Graph es hat, und sonst durch 1.

Aufgabe 12Zu Fuß, mit dem Rad, mit dem Auto

Schreib build_travel_graph in routes.rb.

Aufgabe 12Zu Fuß, mit dem Rad, mit dem Auto

Schreib build_travel_graph in routes.py.

Dijkstra und A* musst du sonst nicht ändern: Sie zählen zusammen, was an den Kanten steht, ob Meter oder Sekunden. Starte den Server neu. Über den Suchen auf der Seite wählst du jetzt, wie gereist wird:

Schau dir an, wie viele Knoten A* mit dem Auto besucht: Zu Fuß ist es ein Zehntel dessen, was Dijkstra besucht, mit dem Auto fast die Hälfte. Die Schätzung rechnet mit 90 km/h, dem Tempo auf der Autobahn, aber in der Stadt kommt man viel langsamer voran. Je weiter die Schätzung unter dem wirklichen Weg liegt, desto weniger hilft sie.

10. Eine Rundfahrt

Jetzt besuchst du mehrere Orte und kommst am Ende zurück: eine Stadtrundfahrt zu den Sehenswürdigkeiten in Mitte. In welcher Reihenfolge ist sie am kürzesten? Das ist das Problem des Handlungsreisenden, eines der berühmtesten der Informatik.

Die Seite rechnet erst mit deiner Suche aus, wie weit es von jedem Ort zu jedem anderen ist. Bei acht Orten sind das 56 Suchen. Heraus kommt eine Tabelle costs: costs[i][j] ist der Weg vom Ort i zum Ort j. Damit arbeiten deine Funktionen, die Karte brauchen sie nicht mehr. Eine Reihenfolge ist ein Array der Nummern der Orte, das mit 0 beginnt, dem Startort. Am Ende geht es zu ihm zurück.

costs = [[0, 10, 5, 2, 14],     Reihenfolge [0, 3, 2, 1, 4]:
         [10, 0, 9, 8, 4],      0 → 3 → 2 → 1 → 4 → 0
         [5, 9, 0, 7, 11],      2 + 7 + 9 + 4 + 14 = 36
         [2, 8, 7, 0, 12],
         [14, 4, 11, 12, 0]]
Aufgabe 13Wie lang ist eine Rundfahrt?

Schreib tour_length in routes.rb.

Aufgabe 13Wie lang ist eine Rundfahrt?

Schreib tour_length in routes.py.

Die sichere Art, die beste Reihenfolge zu finden: alle ausprobieren. Der Startort steht fest, die anderen kommen in jeder möglichen Reihenfolge dahinter. Bei fünf Orten sind das 4 · 3 · 2 · 1 = 24, bei neun schon 40 320, bei zwanzig mehr, als ein Computer in deinem Leben ausprobieren kann.

(1...costs.size).to_a.permutation gibt alle Reihenfolgen der Orte außer dem Startort, min_by die mit der kleinsten Länge.

itertools.permutations(range(1, len(costs))) gibt alle Reihenfolgen der Orte außer dem Startort, min mit key= die mit der kleinsten Länge.

Aufgabe 14Alle Reihenfolgen

Schreib best_tour in routes.rb.

Aufgabe 14Alle Reihenfolgen

Schreib best_tour in routes.py.

Schneller, aber ohne Garantie: der nächste Nachbar. Vom Startort aus geht es immer zu dem Ort, der am nächsten liegt und an dem die Rundfahrt noch nicht war.

Aufgabe 15Der nächste Nachbar

Schreib nearest_tour in routes.rb.

Aufgabe 15Der nächste Nachbar

Schreib nearest_tour in routes.py.

Der nächste Nachbar macht oft einen Fehler: Am Anfang nimmt er die kurzen Wege, am Ende bleiben die langen übrig, und die Rundfahrt kreuzt sich selbst. 2-opt repariert das: Es nimmt ein Stück der Reihenfolge und dreht es um. Wird die Rundfahrt dadurch kürzer, bleibt es dabei. Das probiert es für jedes Stück, und so lange, bis kein Umdrehen mehr hilft.

[0, 3, 2, 1, 4]   36
[0, 3, 4, 1, 2]   32   das Stück 2, 1, 4 umgedreht
[0, 3, 1, 4, 2]   30   und dann das Stück 4, 1
Aufgabe 16Besser mit 2-opt

Schreib improve_tour in routes.rb.

Aufgabe 16Besser mit 2-opt

Schreib improve_tour in routes.py.

Wechsle auf der Seite zu »Rundfahrt«, füge Orte hinzu und probiere die drei Arten aus. Der erste Ort ist der Anfang. Ist die Rundfahrt berechnet, stehen die Orte in der Liste und auf der Karte in der Reihenfolge, in der sie besucht werden:

Der nächste Nachbar kommt auf 11,2 km, das Ausprobieren aller 5 040 Reihenfolgen auf 9,8 km. 2-opt, aus der Reihenfolge des nächsten Nachbarn, kommt bis auf 44 Meter an die beste heran, in einem Bruchteil der Zeit. Ganz die beste ist es nicht: 2-opt hört auf, wenn kein einzelnes Umdrehen mehr hilft, und das ist nicht immer die beste Rundfahrt. Für sehr viele Orte kennt niemand einen schnellen Weg zur besten. Routenplaner, Paketdienste und Busunternehmen arbeiten darum mit Verfahren wie 2-opt: gut genug, und rechtzeitig fertig.