diff --git a/10_classes_02_exercises.ipynb b/10_classes_02_exercises.ipynb index ee6f7ccf0f5cd3ccf9e7d24b0143c77d67a23763..269e79a7ded9cd99b1b3234c3cb3249de089ed6c 100644 --- a/10_classes_02_exercises.ipynb +++ b/10_classes_02_exercises.ipynb @@ -27,14 +27,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Berlin Tourist Guide: A Traveling Salesman Problem" + "## Berlin Tourist Guide: A Minimal Hamiltonian Path Problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This notebook is a hands-on and tutorial-like application to show how to load data from web services like [Google Maps](https://developers.google.com/maps) and use them to solve a logistics problem, namely a **[Traveling Salesman Problem ](https://en.wikipedia.org/wiki/Traveling_salesman_problem)**.\n", + "This notebook is a hands-on and tutorial-like application to show how to load data from web services like [Google Maps](https://developers.google.com/maps) and use them to solve a logistics problem.\n", "\n", "Imagine that a tourist lands at Berlin's [Tegel Airport ](https://en.wikipedia.org/wiki/Berlin_Tegel_Airport) in the morning and has his \"connecting\" flight from [Schönefeld Airport ](https://en.wikipedia.org/wiki/Berlin_Sch%C3%B6nefeld_Airport) in the evening. By the time, the flights were scheduled, the airline thought that there would be only one airport in Berlin.\n", "\n", @@ -1223,13 +1223,16 @@ "source": [ "Let us find the cost minimal order of traveling from the `arrival` airport to the `departure` airport while visiting all the `sights`.\n", "\n", - "This problem can be expressed as finding the shortest so-called [Hamiltonian path ](https://en.wikipedia.org/wiki/Hamiltonian_path) from the `start` to `end` on the `Map` (i.e., a path that visits each intermediate node exactly once). With the \"hack\" of assuming the distance of traveling from the `end` to the `start` to be `0` and thereby effectively merging the two airports into a single node, the problem can be viewed as a so-called [traveling salesman problem ](https://en.wikipedia.org/wiki/Traveling_salesman_problem) (TSP).\n", + "This problem can be expressed as finding the shortest so-called [Hamiltonian path ](https://en.wikipedia.org/wiki/Hamiltonian_path) from the `start` to `end` on the `Map` (i.e., a path that visits each intermediate node exactly once).\n", + "This type of problem is a close relative of the so-called [Traveling Salesman Problem (TSP) ](https://en.wikipedia.org/wiki/Traveling_salesman_problem), which will maybe be more familiar.\n", "\n", - "The TSP is a hard problem to solve but also well studied in the literature. Assuming symmetric distances, a TSP with $n$ nodes has $\\frac{(n-1)!}{2}$ possible routes. $(n-1)$ because any node can be the `start` / `end` and divided by $2$ as the problem is symmetric.\n", + "On an undirected (i.e. assuming symmetrical distances), complete (i.e. assuming we can go from\n", + "any node to another) graph with $n + 2$ nodes, there are $n!$ Hamiltonian paths between any two\n", + "(distinct) start and end nodes.\n", "\n", - "Starting with about $n = 20$, the TSP is almost impossible to solve exactly in a reasonable amount of time. Luckily, we do not have that many `sights` to visit, and so we use a [brute force ](https://en.wikipedia.org/wiki/Brute-force_search) approach and simply loop over all possible routes to find the shortest.\n", + "Starting with about $n = 20$, finding the minimal Hamiltonian path is almost impossible to solve exactly in a reasonable amount of time. Luckily, we do not have that many `sights` to visit, and so we use a [brute force ](https://en.wikipedia.org/wiki/Brute-force_search) approach and simply loop over all possible routes to find the shortest.\n", "\n", - "In the case of our tourist, we \"only\" need to try out `181_440` possible routes because the two airports are effectively one node and $n$ becomes $10$." + "In the case of our tourist, we \"only\" need to try out `362_880` possible routes." ] }, { @@ -1247,7 +1250,7 @@ "metadata": {}, "outputs": [], "source": [ - "math.factorial(len(places) + 1 - 1) // 2" + "math.factorial(len(places))" ] }, { @@ -1275,7 +1278,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "However, if we use [permutations() ](https://docs.python.org/3/library/itertools.html#itertools.permutations) as is, we try out *redundant* routes. For example, transferred to our case, the tuples `(1, 2, 3)` and `(3, 2, 1)` represent the *same* route as the distances are symmetric and the tourist could be going in either direction. To obtain the *unique* routes, we use an `if`-clause in a \"hacky\" way by only accepting routes where the first node has a smaller value than the last. Thus, we keep, for example, `(1, 2, 3)` and discard `(3, 2, 1)`." + "As the code cell below shows, the number of permutations of `sights` corresponds with the correct number of routes to be looped over." ] }, { @@ -1284,61 +1287,7 @@ "metadata": {}, "outputs": [], "source": [ - "for permutation in itertools.permutations(numbers):\n", - " if permutation[0] < permutation[-1]:\n", - " print(permutation)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to compare `Place`s as numbers, we would have to implement, among others, the `__eq__()` special method. Otherwise, we get a `TypeError`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Place(arrival, client=api) < Place(departure, client=api)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As a quick and dirty solution, we use the `location` property on a `Place` to do the comparison." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Place(arrival, client=api).location < Place(departure, client=api).location" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As the code cell below shows, combining [permutations() ](https://docs.python.org/3/library/itertools.html#itertools.permutations) with an `if`-clause results in the correct number of routes to be looped over." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sum(\n", - " 1\n", - " for route in itertools.permutations(places)\n", - " if route[0].location < route[-1].location\n", - ")" + "sum(1 for route in itertools.permutations(places))" ] }, { @@ -1351,7 +1300,7 @@ "\n", "**Q32**: Finish the `evaluate()` method as described!\n", "\n", - "Second, we create a `brute_force()` method that needs no arguments. It loops over all possible routes to find the shortest. As the `start` and `end` of a route are fixed, we only need to look at `permutation`s of inner nodes. Each `permutation` can then be traversed in a forward and a backward order. `brute_force` enables method chaining as well.\n", + "Second, we create a `brute_force()` method that needs no arguments. It loops over all possible routes to find the shortest. As the `start` and `end` of a route are fixed, we only need to look at `permutation`s of inner nodes. `brute_force` enables method chaining as well.\n", "\n", "**Q33**: Finish the `brute_force()` method as described!" ] @@ -1360,7 +1309,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### The `Map` Class (continued): Brute Forcing the TSP" + "### The `Map` Class (continued): Brute Forcing the minimal Hamiltonian path problem" ] }, { @@ -1399,24 +1348,14 @@ "\n", " The route is plotted on the folium.Map.\n", " \"\"\"\n", - " # Assume a very high cost to begin with.\n", - " min_cost = ...\n", - " best_route = None\n", - "\n", - " # Loop over all permutations of the intermediate nodes to visit.\n", - " for permutation in ...:\n", - " # Skip redundant permutations.\n", - " if ...:\n", - " ...\n", - " # Travel through the routes in both directions.\n", - " for route in (permutation, permutation[::-1]):\n", - " # Add start and end to the route.\n", - " route = (..., *route, ...)\n", - " # Check if a route is cheaper than all routes seen before.\n", - " cost = ...\n", - " if ...:\n", - " min_cost = ...\n", - " best_route = ...\n", + " # Generator of routes from start to end\n", + " routes = ((..., *permutation, ...) for permutation in ...)\n", + "\n", + " # Generator of pairs of (route, cost of route)\n", + " route_cost_pairs = ((route, ...) for route in routes)\n", + "\n", + " # Select the best route/cost pair (based on cost)\n", + " best_route, _ = min(route_cost_pairs, key=lambda x: ...)\n", "\n", " # Plot the route on the map\n", " folium.PolyLine(\n", @@ -1499,4 +1438,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file