intro-to-python/04_iteration_review_and_exercises.ipynb

941 lines
25 KiB
Text

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"# Chapter 4: Iteration"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Content Review"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Read [Chapter 4](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/04_iteration.ipynb) of the book. Then work through the fourteen review questions."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Essay Questions "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Answer the following questions briefly with *at most* 300 characters per question!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q1**: What is so \"special\" about the number **7919**?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q2**: Solving a problem with a **recursion** is not only popular in computer science and math. Name some examples from the fields of business or economics where problems are also solved in a **backwards** fashion!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q3**: Explain what **duck typing** means! Why can it cause problems? Why it is [not a bug but a feature](https://www.urbandictionary.com/define.php?term=It%27s%20not%20a%20bug%2C%20it%27s%20a%20feature)?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q4**: What is **syntactic sugar**?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q5**: Describe in your own words why the **recursive** version of `fibonacci()`, the \"Easy at first Glance\" example in the chapter, is computationally **inefficient**! Why does the **iterative** version of `fibonacci()`, the \"Hard at first Glance\" example, run so much faster?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q6**: What is the conceptual difference between a **container** and a **list**?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q7**: What is a good use case for the `for`-loop's optional `else`-clause?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### True / False Questions"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Motivate your answer with *one short* sentence!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q8**: When a **recursion** does **not** reach the base case, this is an example of the **early exit** strategy."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q9**: Any programming language **without** looping constructs like the `for` or `while` statements is **not** Turing complete."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q10**: A **recursive** formulation is the same as a **circular** one: The terms are **synonyms**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q11**: Formulating a computational problem as a **recursion** results in an **efficient** implementation."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q12**: Whereas a **recursion** may accidently result in a **never ending** program, `while`-loops and `for`-loops are guaranteed to **terminate**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q13**: Before writing **any** kind of **loop**, we **always** need to think about a **stopping criterion** ahead of time."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q14**: **Container** types such as `list` objects are characterized by their **support** for **being looped over**, for example as in:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
"for element in container:\n",
" # do something for every element\n",
" ...\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Coding Exercises"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Towers of Hanoi"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A popular example for a problem that is solved with a 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 Initative](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 in order 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": [
"**Q15.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": [
"**Q15.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 **Q15.1**?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.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": [
"**Q15.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 **Q15.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 progam'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 actually 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 in order 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 top most $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()` will be `int` objects!\n",
"\n",
"Once completed, `sol()` should print out all the moves in the correct order. With **printing a move**, we simply 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 **Q15.5** to **Q15.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",
" # ...\n",
" # ...\n",
" # ...\n",
" # ...\n",
" # ...\n",
" # ...\n",
" # ...\n",
" # ...\n",
" # ...\n",
"\n",
" # answer to Q15.7\n",
" # ...\n",
" # ..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.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": [
"**Q15.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": [
"**Q15.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": [
"**Q15.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 Re-Factoring"
]
},
{
"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 **Q15.9** and **Q15.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",
" # ...\n",
" # ..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.9**: Copy the base case from `sol()`!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.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 just copy and paste it?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.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",
"In fact, 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.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 **Q15.12** to **Q15.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": [
"**Q15.12**: Copy the base case from `hanoi()`! What count should be returned when it is reached?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.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": [
"**Q15.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": [
"**Q15.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 **Q15.2**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\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",
"**Q15.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 build 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 **Q15.17** and **Q15.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 between the ()\n",
" # answer to Q15.18\n",
" hanoi_ordered(..., offset=count) # <- answer to Q15.18 between the ()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.17**: Copy the base case from the original `hanoi()`!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Q15.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": [
"**Q15.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": [
"**Q15.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.3"
},
"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": 2
}