intro-to-python/04_iteration_20_exercises.ipynb
Alexander Hess e24cf31104 Restructure *.ipynb files
- split review and exercises into files on their own
- update the contents overviews to include links to
  reviews and exercises
2020-01-28 18:36:36 +01:00

688 lines
20 KiB
Text

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"# Chapter 4: Recursion & Looping"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Coding Exercises"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Read [Chapter 4](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/04_iteration_00_content.ipynb) of the book. Then, work through the exercises below."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Towers of Hanoi"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A popular example of a problem that is solved by recursion art the **[Towers of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi)**.\n",
"\n",
"In its basic version, a tower consisting of, for example, four disks with increasing radii, is placed on the left-most of **three** adjacent spots. In the following, we refer to the number of disks as $n$, so here $n = 4$.\n",
"\n",
"The task is to move the entire tower to the right-most spot whereby **two rules** must be obeyed:\n",
"\n",
"1. Disks can only be moved individually, and\n",
"2. a disk with a larger radius must *never* be placed on a disk with a smaller one.\n",
"\n",
"Although the **[Towers of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** are a **classic** example, introduced by the mathematician [Édouard Lucas](https://en.wikipedia.org/wiki/%C3%89douard_Lucas) already in 1883, it is still **actively** researched as this scholarly [article](https://www.worldscientific.com/doi/abs/10.1142/S1793830919300017?journalCode=dmaa&) published in January 2019 shows.\n",
"\n",
"Despite being so easy to formulate, the game is quite hard to solve.\n",
"\n",
"Below is an interactive illustration of the solution with the minimal number of moves for $n = 4$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src=\"static/towers_of_hanoi.gif\" width=\"60%\" align=\"left\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Watch the following video by [MIT](https://www.mit.edu/)'s professor [Richard Larson](https://idss.mit.edu/staff/richard-larson/) for a comprehensive introduction.\n",
"\n",
"The [MIT Blossoms Initiative](https://blossoms.mit.edu/) is primarily aimed at high school students and does not have any prerequisites.\n",
"\n",
"The video consists of three segments, the last of which is *not* necessary to have watched to solve the tasks below. So, watch the video until 37:55."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from IPython.display import YouTubeVideo\n",
"YouTubeVideo(\"UuIneNBbscc\", width=\"60%\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Video Review Questions"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.1**: Explain for the $n = 3$ case why it can be solved as a **recursion**!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.2**: How does the number of minimal moves needed to solve a problem with three spots and $n$ disks grow as a function of $n$? How does this relate to the answer to **Q1.1**?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.3**: The **[Towers of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** problem is of **exponential growth**. What does that mean? What does that imply for large $n$?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.4**: The video introduces the recursive relationship $Sol(4, 1, 3) = Sol(3, 1, 2) ~ \\bigoplus ~ Sol(1, 1, 3) ~ \\bigoplus ~ Sol(3, 2, 3)$. The $\\bigoplus$ is to be interpreted as some sort of \"plus\" operation. How does this \"plus\" operation work? How does this way of expressing the problem relate to the answer to **Q1.1**?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Naive Translation to Python"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As most likely the first couple of tries will result in *semantic* errors, it is advisable to have some sort of **visualization tool** for the program's output: For example, an online version of the game can be found **[here](https://www.mathsisfun.com/games/towerofhanoi.html)**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's first **generalize** the mathematical relationship from above.\n",
"\n",
"While the first number of $Sol(\\cdot)$ is the number of `disks` $n$, the second and third \"numbers\" are the **labels** for the three spots. Instead of spots `1`, `2`, and `3`, we could also call them `\"left\"`, `\"center\"`, and `\"right\"` in our Python implementation. When \"passed\" to the $Sol(\\cdot)$ \"function\" they take on the role of an `origin` (= $o$) and `destination` (= $d$) pair.\n",
"\n",
"So, the expression $Sol(4, 1, 3)$ is the same as $Sol(4, \\text{\"left\"}, \\text{\"right\"})$ and describes the problem of moving a tower consisting of $n = 4$ disks from `origin` `1` / `\"left\"` to `destination` `3` / `right`. As we have seen in the video, we need some `intermediate` (= $i$) spot."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In summary, the generalized functional relationship can be expressed as:\n",
"\n",
"$Sol(n, o, d) = Sol(n-1, o, i) ~ \\bigoplus ~ Sol(1, o, d) ~ \\bigoplus ~ Sol(n-1, i, d)$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In words, this means that to move a tower consisting of $n$ disks from an `origin` $o$ to a `destination` $d$, three steps must be executed:\n",
"\n",
"1. Move the topmost $n - 1$ disks of the tower temporarily from $o$ to $i$ (= sub-problem 1)\n",
"2. Move the remaining and largest disk from $o$ to $d$\n",
"3. Move the the $n - 1$ disks from the temporary spot $i$ to $d$ (= sub-problem 2)\n",
"\n",
"The two sub-problems can be solved via the same recursive logic."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$Sol(\\cdot)$ can be written in Python as a function `sol()` that takes three arguments `disks`, `origin`, and `destination` that mirror $n$, $o$, and $d$.\n",
"\n",
"Assume that all arguments to `sol()` are `int` objects!\n",
"\n",
"Once completed, `sol()` should print out all the moves in the correct order. With **printing a move**, we mean a line like \"1 -> 3\", short for \"Move the top-most disk from spot 1 to spot 3\".\n",
"\n",
"Write your answers to **Q1.5** to **Q1.7** into the subsequent code cell and finalize `sol()`! No need to write a docstring or validate the input here."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def sol(disks, origin, destination):\n",
"\n",
" # answer to Q15.5\n",
" # ...\n",
"\n",
" # answer to Q15.6\n",
" # ...\n",
"\n",
" # answer to Q15.7\n",
" # ..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.5**: What is the `disks` argument when the function reaches its **base case**? Check for the base case with a simple `if` statement and return from the function using the **early exit** pattern!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.6**: If not in the base case, `sol()` needs to determine the `intermediate` spot given concrete `origin` and `destination` arguments. For example, if called with `origin=1` and `destination=2`, `intermediate` must be `3`.\n",
"\n",
"Add **one** compound `if` statement to `sol()` that has a branch for **every** possible `origin`-`destination` pair that sets a variable `intermediate` to the correct temporary spot. **How many** branches will there be?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.7**: `sol()` needs to call itself **two more times** with the correct 2-pairs chosen from the three available spots `origin`, `intermediate`, and `destination`.\n",
"\n",
"In between the two recursive function calls, write a `print()` statement that prints out from where to where the \"remaining and largest\" disk has to be moved!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.8**: Execute the code cells below and confirm that the printed moves are correct!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sol(1, 1, 3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sol(2, 1, 3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sol(3, 1, 3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sol(4, 1, 3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Pythonic Refactoring"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The previous `sol()` implementation does the job, but the conditional statement needed in unnecessarily tedious. \n",
"\n",
"Let's create a more concise `hanoi()` function that, in addition to a positional `disks` argument, takes three keyword-only arguments `origin`, `intermediate`, and `destination` with default values `\"left\"`, `\"center\"`, and `\"right\"`.\n",
"\n",
"Write your answers to **Q1.9** and **Q1.10** into the subsequent code cell and finalize `hanoi()`! No need to write a docstring or validate the input here."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def hanoi(disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\"):\n",
"\n",
" # answer to Q15.9\n",
" # ...\n",
"\n",
" # answer to Q15.10\n",
" # ..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.9**: Copy the base case from `sol()`!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.10**: Instead of conditional logic, `hanoi()` calls itself **two times** with the **three** arguments `origin`, `intermediate`, and `destination` passed on in a **different** order.\n",
"\n",
"Figure out how the arguments are passed on in the two recursive `hanoi()` calls!\n",
"\n",
"Also, write a `print()` statement analogous to the one in `sol()` in between the two recursive function calls. Is it ok to copy and paste it?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.11**: Execute the code cells below and confirm that the printed moves are correct!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi(1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi(2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi(3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi(4)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We could, of course, also use **numeric labels** for the three steps like so."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi(3, origin=1, intermediate=2, destination=3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Passing a Value \"up\" the Recursion Tree"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's say we did not know about the **analytical formula** for the number of **minimal moves** given $n$.\n",
"\n",
"In such cases, we could modify a recursive function to return a count value to be passed up the recursion tree.\n",
"\n",
"This is similar to what we do in the recursive versions of `factorial()` and `fibonacci()` in [Chapter 4](https://github.com/webartifex/intro-to-python/blob/master/04_iteration_00_content.ipynb), where we pass up an intermediate result.\n",
"\n",
"Let's create a `hanoi_moves()` function that follows the same internal logic as `hanoi()`, but, instead of printing out the moves, returns the number of steps done so far in the recursion.\n",
"\n",
"Write your answers to **Q1.12** to **Q1.14** into the subsequent code cell and finalize `hanoi_moves()`! No need to write a docstring or validate the input here."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def hanoi_moves(disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\"):\n",
"\n",
" # answer to Q15.12\n",
" # ...\n",
"\n",
" moves = ... # <- answer to Q15.13\n",
" moves += hanoi_moves(...) # <- answer to Q15.14 between the ()\n",
" moves += hanoi_moves(...) # <- answer to Q15.14 between the ()\n",
"\n",
" return moves"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.12**: Copy the base case from `hanoi()`! What count should be returned when it is reached?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.13**: Initialize the variable `moves` with an appropriate count! This is the number of moves that corresponds to **one** recursive function call."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.14**: `moves` is updated with the counts passed up from the two recursive calls.\n",
"\n",
"Complete the two recursive function calls with the same arguments as in `hanoi()`!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.15**: Write a `for`-loop that prints out the **minimum number** of moves needed to solve Towers of Hanoi for any number of `disks` from `1` through `20` to confirm your answer to **Q1.2**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for ... in ...:\n",
" ..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Time Complexity"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Observe how quickly the `hanoi_moves()` function slows down for increasing `disks` arguments.\n",
"\n",
"With `disks` in the range from `24` through `26`, the computation time roughly doubles for each increase of `disks` by 1.\n",
"\n",
"**Q1.16**: Execute the code cells below and see for yourself!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%timeit -n 1 -r 1\n",
"print(\"Number of moves:\", hanoi_moves(24))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%timeit -n 1 -r 1\n",
"print(\"Number of moves:\", hanoi_moves(25))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%timeit -n 1 -r 1\n",
"print(\"Number of moves:\", hanoi_moves(26))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Passing a Value \"down\" the Recursion Tree (Advanced)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The above `hanoi()` prints the optimal solution's moves in the correct order but fails to label each move with an order number. This can be built in by passing on one more argument `offset` down the recursion tree. As the logic gets a bit \"involved,\" `hanoi_ordered()` below is almost finished.\n",
"\n",
"Write your answers to **Q1.17** and **Q1.18** into the subsequent code cell and finalize `hanoi_ordered()`! No need to write a docstring or validate the input here."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def hanoi_ordered(disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\", offset=None):\n",
"\n",
" # answer to Q15.17\n",
" # ...\n",
"\n",
" total = (2 ** disks - 1)\n",
" half = (2 ** (disks - 1) - 1)\n",
" count = total - half\n",
"\n",
" if offset is not None:\n",
" count += offset\n",
"\n",
" hanoi_ordered(..., offset=offset) # <- answer to Q15.18\n",
" # ... <- answer to Q15.18\n",
" hanoi_ordered(..., offset=count) # <- answer to Q15.18"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.17**: Copy the base case from the original `hanoi()`!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.18**: Complete the two recursive function calls with the same arguments as in `hanoi()` or `hanoi_moves()`! Do not change the already filled in `offset` arguments!\n",
"\n",
"Then, copy the `print()` statement from `hanoi()` and adjust it to print out `count` as well!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.19**: Execute the code cells below and confirm that the order numbers are correct!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi_ordered(1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi_ordered(2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi_ordered(3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hanoi_ordered(4)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Lastly, it is to be mentioned that for problem instances with a small `disks` argument, it is easier to collect all the moves first in a list and then add the order number with the [enumerate()](https://docs.python.org/3/library/functions.html#enumerate) built-in."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Open Question"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1.20**: Conducting your own research on the internet (max. 15 minutes), what can you say about generalizing the **[Towers of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** problem to a setting with **more than three** landing spots?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.4"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": false,
"sideBar": true,
"skip_h1_title": true,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": false,
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 4
}