0% found this document useful (0 votes)
21 views24 pages

Learn Recursion, Dammit - Daniel Zingaro

The book 'Learn Recursion, Dammit' by Daniel Zingaro introduces recursion through practical problems, specifically using the example of determining valid configurations of dogs in a park based on their enmities. It emphasizes a recursive approach to solving problems by breaking them down into smaller subproblems and provides a helper function to check if a dog can be added to a configuration. The book is part of the Lean Publishing process, allowing for iterative feedback and improvement from readers.

Uploaded by

Aman Ali
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
21 views24 pages

Learn Recursion, Dammit - Daniel Zingaro

The book 'Learn Recursion, Dammit' by Daniel Zingaro introduces recursion through practical problems, specifically using the example of determining valid configurations of dogs in a park based on their enmities. It emphasizes a recursive approach to solving problems by breaking them down into smaller subproblems and provides a helper function to check if a dog can be added to a configuration. The book is part of the Lean Publishing process, allowing for iterative feedback and improvement from readers.

Uploaded by

Aman Ali
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 24

Learn Recursion, Dammit

Daniel Zingaro
Learn Recursion, Dammit
Daniel Zingaro
This book is for sale at http://leanpub.com/learnrecursion

This version was published on 2023-09-12

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.

© 2020 - 2023 Daniel Zingaro


Contents

Chapter 1: The Bad Dogs Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1


Bad Dogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Bonus: Returning the Valid Configurations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Summary: What the Hell Is Recursion, Then? . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

Chapter 2: The Best Team Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19


The Best Team . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Chapter 1: The Bad Dogs Problem
Instead of starting with a boring-ass abstract description of what recursion is, we’re going to start
with a problem that we’d like to solve. We’ll see that we can solve this problem using ideas that lead
us to recursion. Then we’ll describe what recursion is more generally.

Bad Dogs
I grew up with a Jack Russell Terrier dog named Spino. He was such a bad boy! But such a cute dog,
you know?
One time I was out walking with Spino and a woman asked if she could pet him. I shook my head
as dramatically as possible – “no, no, don’t” – but she pet him anyway, and: hop, SNAP! Little Spino
boy bit her finger.
Another time he snarled and growled nonstop for six hours inside the house while roofers worked
outside. Did he ever want to chunk those roofers!
And taking Spino to a doggie park? Almost no chance. He had only one friend named Stretchy. Spino
and every other dog were enemies. Those dogs certainly could not be in the park together.

The Problem
While Spino may have been the world’s best dog and the only dog I think worth writing about, it’s
more interesting if we allow other dogs as well. So let’s imagine that we have zero or more dogs,
and that we also know which pairs of dogs are enemies. For example, say that we have the five dogs
NiceBoy, Homer, Sky, Stretchy, and Spino; and that the following pairs of dogs are enemies:

• Spino and NiceBoy


• Spino and Homer
• Spino and Sky
• Sky and Homer

“Spino and NiceBoy” is in here, so they’re enemies. I’m not also going to include things like “NiceBoy
and Spino”, with the dogs the other way around, because that wouldn’t tell us anything new.
We want to determine the total number of valid configurations of dogs in a park. A configuration
of dogs is valid if there are no enemies among those dogs.
Let’s make sure we’ve got this. Suppose we put NiceBoy and Homer into a park – is that a valid
configuration?
Chapter 1: The Bad Dogs Problem 2

It is! NiceBoy and Homer are not enemies. All is fine.


What about NiceBoy, Homer, and Stretchy – is that a valid configuration?
Yes again! There are no enemies here.
Now let’s do NiceBoy, Homer, and Spino – is this one valid?
Nope! This set of three dogs has some enemies in it. For example, NiceBoy and Spino are enemies.
Homer and Spino are enemies, too, but that doesn’t matter: once we find one pair of enemy dogs in
the park, that’s enough to make the configuration invalid.
We’ve found a few valid configurations so far – but we want to know how many there are in total.
The answer here is 14, and here are all of the valid configurations:

• no dogs at all
• Stretchy
• Sky
• Sky, Stretchy
• Homer
• Homer, Stretchy
• NiceBoy
• NiceBoy, Stretchy
• NiceBoy, Sky
• NiceBoy, Sky, Stretchy
• NiceBoy, Homer
• NiceBoy, Homer, Stretchy
• Spino
• Spino, Stretchy

Notice that putting no dogs into the park is always a valid configuration: you can’t have any enemies
among zero dogs!

Is It Safe to Add This Dog?


How would you solve this problem? You can probably imagine starting with an empty park, and
then adding dogs to it to arrive at valid configurations of dogs. That’s not so far from the kind of
solution we’ll end up writing.
Let’s not get ahead of ourselves though.
To warm up and get us started, we’re going to write a little helper function that tells us whether we
can add a given dog to a given configuration of dogs. It will be able to answer questions like “if we
have NiceBoy and Homer in the park, can we also add Spino?” The answer to this question is no.
This function doesn’t solve the full problem on its own. But it’s nevertheless useful because as we
build up valid configurations, we can call it to tell us whether adding a dog is allowed or not.
Here’s the signature for the function that we want:
Chapter 1: The Bad Dogs Problem 3

1 def dog_ok(enemies, current_park, new_dog):

There are three parameters:

• enemies is the full list of pairs of dogs that are enemies.


• current_park is the list of dogs in the current park.
• new_dog is the name of the new dog that we’re wanting to add to the current park.

Here’s the full function.

1 def dog_ok(enemies, current_park, new_dog):


2 """
3 Return True if we can add new_dog to current_park, otherwise return False.
4 We can add new_dog to current_park if new_dog has no enemies already in the park.
5
6 enemies is a list of [dog_a, dog_b] enemies.
7 current_park is a list of the dogs already in the park.
8 new_dog is the name of a dog.
9 """
10 for dog_a, dog_b in enemies:
11 if dog_a == new_dog:
12 if dog_b in current_park:
13 return False
14 elif dog_b == new_dog:
15 if dog_a in current_park:
16 return False
17 return True

The function starts with a docstring (2-9) discussing how to call the function. It tells you what
the function returns and the purpose of each parameter. It does not tell you how the function is
implemented: docstrings are written to help people call our functions, and those people don’t care
what we did on the inside to implement the desired behaviour. I’ll include a similar-looking docstring
for each function in this book.
The plan for the code is to loop through all pairs of enemies (10), checking for a pair that includes both
new_dog and another dog that’s already in the park. If we ever find such a pair, we’ll immediately
return False to indicate that we cannot add new_dog to the park.
We have to be careful because new_dog could be the first dog in an enemy pair or the second dog
in an enemy pair. If it’s the first dog (11), then we check whether the second is already in the park.
If it’s the second dog (14), then we check whether the first is already in the park. In any other case
besides these, this pair of enemies doesn’t matter, and we move on to the next one.
If we happen to get to the last line (17), then it means that new_dog is not enemies with any of the
existing dogs in the park. In this case, we return True: it is safe to add new_dog to the current park.
Let’s try this function with a couple of examples.
Chapter 1: The Bad Dogs Problem 4

1 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],


2 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
3 >>> dog_ok(the_enemies, ['NiceBoy', 'Homer'], 'Stretchy')
4 True
5 >>> dog_ok(the_enemies, ['NiceBoy', 'Homer'], 'Spino')
6 False

In the first example (3), we have NiceBoy and Homer in the park, and we want to know whether we
can add Stretchy. We can! Stretchy is not an enemy with any dog currently in the park.
In the second example (5), we again have NiceBoy and Homer in the park, and this time we want to
know whether we can add Spino. We cannot! Spino is an enemy with at least one dog in the park.
Does our function work with the empty park – the park with no dogs yet?

1 >>> dog_ok(the_enemies, [], 'Spino')


2 True

Looks good–we can add Spino to the empty park, so True is the correct return value.
Make sure you’re cool with this helper function before you continue. We’re about to use it in a
full-blown solution to the problem.

Some Configurations Don’t Have the Dog, The Rest Do


If I stopped right now and asked you to write code to determine the number of valid dog
configurations, you might work toward a solution that uses some loops to build up different subsets
of the dogs and test whether each is valid. That would be an iterative solution. In an iterative solution,
you tell the computer exactly what to do, exactly which steps to carry out.
But we’re not going to do that here. Instead, I’m going to provide you some simple rules that will
always produce the correct answer.

Rule 1
Suppose that we have any valid configuration of dogs, and no further dogs to consider. Then
the number of valid configurations is 1.

If we have no dogs in our configuration and no further dogs to consider, then the only valid
configuration of dogs is the one we started with… aka there’s only 1 valid configuration. Similarly,
if we have a configuration of some dogs – like Spino by himself, or Spino and Stretchy – and no
further dogs to consider, then the only valid configuration of dogs is the one we started with… again,
just 1 configuration.
That rule 1 there only applies in some very simple cases; specifically, when there are no further dogs
to consider. Think of this situation as an empty Python list of dogs to consider. But that’s pretty
Chapter 1: The Bad Dogs Problem 5

boring, because in all interesting cases of this problem we are going to have at least some dogs to
consider!
So, we are going to need rule 2. (Don’t you dare stop reading here! This will all make sense.)

Rule 2
Suppose that we have any valid configuration of dogs, and at least one remaining dog
to consider. Choose the dog that we’ll consider next and call it d. The number of valid
configurations is the number of valid configurations that don’t include d plus the number of
valid configurations that do include d.

What the heck is rule 2 saying? To explain, I’ll say something that you can’t argue with: some of the
valid configurations don’t contain d, and the rest do include d.
For example, let’s say that right now we have only NiceBoy in the park and that we still have to
figure out what to do with all of the other dogs. Let’s suppose that the next dog we’re going to
consider is Homer (so Homer is our d dog). All I’m saying here is that the valid configurations are
those that include NiceBoy but not Homer, plus those that include NiceBoy and Homer. If we could
solve these two smaller subproblems, then we could just add up those two smaller solutions to get
the overall solution.
Using our original list of pairs of enemy dogs, we see that there are 4 valid configurations that
include NiceBoy and not Homer:

1. NiceBoy
2. NiceBoy, Stretchy
3. NiceBoy, Sky
4. NiceBoy, Sky, Stretchy

and there are 2 valid configurations that include NiceBoy and Homer:

1. NiceBoy, Homer
2. NiceBoy, Homer, Stretchy

By rule 2, we now know that the total number of configurations that include NiceBoy is 4 + 2 = 6.
“Wait, wait”, you say. “I see how we can break up the big overall solution, which is 6, to the sum
of two smaller solutions, which are 4 and 2. But where are we supposed to get the 4 and the 2? I
feel like we started with one problem we don’t know how to solve and replaced it with two new
problems we don’t know how to solve! And, shoot, if we keep this up, later those two problems will
become four, then eight, then sixteen… exploding! Exploding problems everywhere, none of which
we know how to solve…”
Hey hey, hang in there. OK so, yes, we have replaced one problem by two problems. But these two
new problems are simpler than the original one, because they concern one fewer dog. After all, we
Chapter 1: The Bad Dogs Problem 6

don’t have to consider Homer anymore: Homer is in some configurations and not others, but in
each case we will next consider what to do with some dog that we haven’t considered yet. These
two smaller problems are called subproblems, because we need their solutions to be able to solve the
full problem that we care about.
The next question is “How are we supposed to solve those two subproblems, then?”
And the answer is: we use exactly the same procedure (rule 2) that we used for the original problem!
Let’s do one.
Let’s solve the subproblem when NiceBoy is in the configurations and Homer isn’t. (This is one of
the two subproblems we need to solve for the bigger problem of the number of valid configurations
that include NiceBoy.) The remaining dogs we have to consider are Spino, Sky, and Stretchy. We
have to pick our next dog to consider from those three… let’s pick Stretchy (that’s our next d dog).
Now we need to solve two subsubproblems for this subproblem: the number of valid configurations
that contain NiceBoy, don’t contain Homer, and don’t contain Stretchy; and the number of valid
configurations that contain NiceBoy, don’t contain Homer, and do contain Stretchy.
There are two configurations of the first type:

1. NiceBoy
2. NiceBoy, Sky

and there are two configurations of the second type:

1. NiceBoy, Stretchy
2. NiceBoy, Sky, Stretchy

So, by rule 2 again, the number of valid configurations that contain NiceBoy and don’t contain
Homer is 2+2=4.
And where do we get these 2 and 2 subsubsolutions? We can repeat the same process on those, using
rule 2 again and again, considering one further dog each time until there are no dogs left to consider.
When there are no dogs left to consider, we’re in rule 1, and we can stop.
You know those two rules, rule 1 and rule 2, that we’ve been using? This is an example of a recursive
definition of a problem. A recursive definition refers to a smaller version of the problem in the
solution to the bigger, original problem that you’re trying to solve. Rule 1 is called a base case:
it’s a case that we can solve directly, without knowing anything else or having to solve smaller
subproblems. And rule 2 is called a recursive case. It tells you how to solve a problem in terms of
solutions to smaller subproblems.
I hope that it’s intuitively clear that our recursive definition here, using rule 1 and rule 2, can be used
to find the number of valid configurations no matter how many dogs there are and no matter how
many enemy relationships there are between those dogs. Remember, every time we use rule 2, we
Chapter 1: The Bad Dogs Problem 7

ask for the solution to a smaller problem that has one fewer dog. That process can’t go on forever…
we don’t have an infinite number of dogs!
If you’re still skeptical, I’ll offer a quick recursive example that may feel more familiar. Right now
at work I’m supposed to be working on a 40-page report about… well, actually I don’t even know:
the instructions themselves are too boring for me to read. (So instead I’m working on this recursion
book.) What I could do to write the report is write it myself. But what I could also do is a more
recursive-looking approach: I could ask two of my friends to write 20 pages each, then jam those
two collections of 20 pages together to produce the final report. (You might worry that someone
would notice that the transition between pages 20 and 21 doesn’t make any sense. But anyone who
even tries to read that thing will fall asleep halfway through page 1 anyway, so there’s no risk of
that.)
We’ve gone from one problem to solve to two smaller subproblems to solve.
Now let’s take the perspective of one of my friends. They have 20 boring pages to write. They can
use the same process that I used. Namely, rather than write it themselves, they can rope two of their
friends into writing ten pages each.
And what about a ten-page friend? Well, maybe ten pages is doable for them, so they can just write
it (a base case). Or they could split the problem again, foisting the work onto two of their friends
each of whom will write five pages.
In any recursive solution to a problem, we need at least one base case and at least one recursive case.
If there is no base case, then we’ll keep recursing forever. (A friend will eventually be asked to write
like 0.0000001 pages!) If there’s no recursive case, then it simply isn’t a recursive solution.

Coding the Rules


Now we’re going to work on writing Python code to carry out rule 1 and rule 2. We’ll write a
function that takes three parameters: the list of dogs that we need to consider, the pairs of dogs that
are enemies, and the current configuration of dogs. We’ll return the number of valid configurations
of dogs starting with those in the current configuration.
One important prerequisite for calling this function: we’ll make sure we always call it with a valid
configuration of dogs. That is, we’ll never start it off in the situation where some dogs in the park
are enemies.
Here’s the header and docstring for the function that we’re trying to write:
Chapter 1: The Bad Dogs Problem 8

1 def num_configs(dogs, enemies, current_park):


2 """
3 Return the number of valid configurations that can be made from current_park
4 using the given dogs.
5 A configuration is valid if it does not have a pair of dogs that are enemies.
6
7 dogs is a list of dog names.
8 enemies is a list of [dog_a, dog_b] enemies.
9 current_park is a list of the dogs already in the park;
10 that is, it is a valid configuration of dogs.
11 """

Remember rule 1?

Rule 1
Suppose that we have any valid configuration of dogs, and no further dogs to consider. Then
the number of valid configurations is 1.

We can implement this rule directly in Python code, like this:

1 if dogs == []:
2 return 1

If you put those two lines as the body of our num_configs function, then our function will work for
any case where there are no dogs to process. Here are some examples:

1 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],


2 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
3 >>> num_configs([], the_enemies, ['Spino'])
4 1
5 >>> num_configs([], the_enemies, ['Spino', 'Stretchy'])
6 1
7 >>> num_configs([], the_enemies, [])
8 1

The first call (3) is asking this: “starting with Spino, and considering no further dogs, how many
valid configurations of dogs are there?” The answer is 1: just Spino by himself. That’s the only valid
configuration here involving Spino.
The second call (5) is asking this: “starting with Spino and Stretchy, and considering no further dogs,
how many valid configurations of dogs are there?” Again, the answer is 1: just Spino and Stretchy
in the park.
Chapter 1: The Bad Dogs Problem 9

And the third call (7)? This one starts with no dogs in the park. As we’re still considering zero other
dogs, the answer is still 1. The only valid configuration this time is the one with no dogs. The empty
dog park!
In my experience, people don’t have any problem with the code for rule 1. “It works when we have
no dogs to consider… big deal.”
But many people do have a problem with the code for rule 2.

Rule 2
Suppose that we have any valid configuration of dogs, and at least one remaining dog
to consider. Choose the dog that we’ll consider next and call it d. The number of valid
configurations is the number of valid configurations that don’t include d plus the number of
valid configurations that do include d.

We’re going to literally translate this into Python. For d, I’ll choose the first dog from the dogs list:
dogs[0]. (We could use any dog in the list, but dogs[0] is the easiest to work with.)

To figure out the number of valid configurations, we have to add two things:

1. The number of valid configurations that don’t include dogs[0], plus


2. The number of valid configurations that do include dogs[0]

How do we figure out the number of valid configurations that don’t include dogs[0]? Recursively,
baby! Like this:

1 without_dog = num_configs(dogs[1:], enemies, current_park)

What are we doing? We’re making a call to this same function, but with one fewer dog. This
is recursion: calling a function from within its own code. The list dogs[1:] is the same as dogs
except it doesn’t contain dogs[0]. (That’s Python slice syntax.) The second and third parameters are
unchanged: we have the same pairs of enemies and the same dogs in the park (because dogs[0] is
not in the configurations that we want right now).
This call to num_configs is recursive, yes, but otherwise it is the same as any function call. In
particular, Python pauses the current execution of num_configs, runs the recursive one (the one
that figures out the value for without_dog), and then returns to the current execution to finish the
job. Of course, that recursive call of num_configs might make its own recursive calls, so a lot of
function invocations and returns may happen by the time we get back to the current execution.
How does Python pause the current execution of the function? How does it keep track of the recursive
calls? How does it organize all of this?
Answer: we don’t care.
Chapter 1: The Bad Dogs Problem 10

… no, really. I think it’s a big mistake at this point to trace the execution of a recursive function
inside the computer. In Chapter 3 we’ll do it, just for completeness. But for now, we need to focus
on understanding the rules that make up our solution, and implementing those rules in Python.
Speaking of rules: we’re not quite done implementing rule 2 yet. We still have to figure out The
number of valid configurations that do include dogs[0]. We’ll store this result in with_dog, so that
when we’re done we’ll have both without_dog and with_dog and can just add them up.
How do we do it? It’s tempting to try this:

1 with_dog = num_configs(dogs[1:], enemies, current_park + [dogs[0]])

Our first argument changes just as before: to not include dogs[0], thereby having one fewer dog.
This time, the third argument changes as well: if we’re going to include this dog in the configurations
then we’d better add it to current_park!
Unfortunately, this is not quite correct. The problem is that this code will blissfully add dogs even if
that dog has enemies already in the park configuration. We have to be careful to only add dogs[0]
if it has no enemies with any of the other dogs in the current configuration.
Earlier, we wrote a helper function dog_ok to help us with this very task! Recall that it tells us whether
we can safely add a dog to a configuration. We can use it like this:

1 if dog_ok(enemies, current_park, dogs[0]):


2 with_dog = num_configs(dogs[1:], enemies, current_park + [dogs[0]])
3 else:
4 with_dog = 0

Now we make the recursive call only if it’s legit to add dogs[0]. Otherwise, we know that dogs[0]
can’t be involved in any valid configuration stemming from the current configuration, so we just
set with_dog to 0.
Now we have all of the ingredients for this num_configs function. Here’s the whole thing!

1 def num_configs(dogs, enemies, current_park):


2 """
3 Return the number of valid configurations that can be made from current_park
4 using the given dogs.
5 A configuration is valid if it does not have a pair of dogs that are enemies.
6
7 dogs is a list of dog names.
8 enemies is a list of [dog_a, dog_b] enemies.
9 current_park is a list of the dogs already in the park;
10 that is, it is a valid configuration of dogs.
11 """
Chapter 1: The Bad Dogs Problem 11

12 if dogs == []:
13 return 1
14
15 without_dog = num_configs(dogs[1:], enemies, current_park)
16
17 if dog_ok(enemies, current_park, dogs[0]):
18 with_dog = num_configs(dogs[1:], enemies, current_park + [dogs[0]])
19 else:
20 with_dog = 0
21
22 return without_dog + with_dog

That return statement at the bottom is crucial. Without that, the function won’t return anything!
And we need it to return the total configurations without the current dog plus the total configura-
tions with the current dog.

Testing the Function


Let’s test our num_configs function to make sure it works in the way we expect. Earlier, we manually
figured out the number of valid configurations starting with only NiceBoy in the configurations. The
answer was 6. Our function agrees:

1 >>> some_dogs = ['Homer', 'Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> num_configs(some_dogs, the_enemies, ['NiceBoy'])
5 6

That works correctly because our call of num_configs correctly calls the function for each of the two
subproblems that the original problem depends on. Here’s one of those calls, the one that calculates
the number of valid configurations that include NiceBoy but not Homer. The answer is supposed to
be 4, and we get it right (this is what the without_dog variable gets):

1 >>> some_dogs = ['Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> num_configs(some_dogs, the_enemies, ['NiceBoy'])
5 4

Here’s the second of those calls, which returns 2 as expected (this is what the with_dog variable
gets):
Chapter 1: The Bad Dogs Problem 12

1 >>> some_dogs = ['Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> num_configs(some_dogs, the_enemies, ['NiceBoy', 'Homer'])
5 2

The function then returns the sum of these two, which is how we get our 6.

And Finally…
We want the total number of valid configurations, not only those that start with NiceBoy. If we can
get the total number of valid configurations, then we’re done… that’s the solution to the problem
we’re trying to solve.
The easiest way to do this is to start with all dogs available, and the empty park configuration, like
this:

1 >>> the_dogs = ['NiceBoy', 'Homer', 'Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> num_configs(the_dogs, the_enemies, [])
5 14

14 – that’s the correct answer! We got it right because we’re asking our function for all valid
configurations without any dog being forced to be in the park.
It’s a bit annoying to have to include the [] argument every time we want to solve this problem. I
mean, why should we have to? Why do we care that the num_configs function needs this empty list
argument? That’s an implementation detail.
We can simplify things a little and avoid having to pass this [] argument by defining a new wrapper
function called solve that takes not three, but only two parameters (the real ones that we care about).
It will then call num_configs for us and supply that pesky [] third argument. From now on, we’ll
call solve directly, not num_configs.
Here’s the solve function:
Chapter 1: The Bad Dogs Problem 13

1 def solve(dogs, enemies):


2 """
3 Return the number of valid configurations using the given dogs.
4 A configuration is valid if it does not have a pair of dogs that are enemies.
5
6 dogs is a list of dog names.
7 enemies is a list of [dog_a, dog_b] enemies.
8 """
9 return num_configs(dogs, enemies, [])

Again, don’t forget the return at the bottom. Without that, we’ll still do our calculation, but we
won’t return it to the caller. It’ll be lost! Whenever you see a “TypeError: ‘NoneType’ object is not
callable” error from Python, make sure your returns are correct.
Boom! Now we can call solve:

1 >>> the_dogs = ['NiceBoy', 'Homer', 'Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> solve(the_dogs, the_enemies)
5 14

Bad Spino, Bad!


There are a lot of ways to get recursive functions wrong. And when you do make a mistake, even
a seemingly small one, you’ll almost certainly notice that almost every call you make gives you the
wrong answer. There’s generally no graceful degradation here: if it’s wrong, boy is it wrong.
We’re going to spend some time now getting the code for our dog example wrong. This is important!
Too many tutorials throw all kinds of correct recursive code at you. It’s too easy to look at someone’s
code and be like, “yep, I see how it works, let’s move on”. I’m throwing this bad code at you to help
you prevent some kinds of errors in the future.
As our first example, suppose that we get the base case wrong. It’s actually pretty easy to get base
cases wrong in general when writing recursive functions. I think people blast through the base cases
too quickly sometimes, because after all they’re “easy”. But take your time! Because if you get the
base case wrong then probably nothing will work, not even huge cases that seem to have little to do
with the base case.
In the Bad Dogs problem, suppose that we incorrectly return 0 in the base case instead of 1. There
are zero dogs, after all, so maybe returning 0 makes sense? Anyone could make this mistake.
The code with the change looks like this:
Chapter 1: The Bad Dogs Problem 14

1 def num_configs(dogs, enemies, current_park):


2 if dogs == []:
3 return 0 # This is wrong now!
4
5 # The rest of the code is unchanged and still correct
6
7 without_dog = num_configs(dogs[1:], enemies, current_park)
8
9 if dog_ok(enemies, current_park, dogs[0]):
10 with_dog = num_configs(dogs[1:], enemies, current_park + [dogs[0]])
11 else:
12 with_dog = 0
13
14 return without_dog + with_dog

All I changed was return 1 to return 0 (3). And now everything is completely broken. Watch:

1 >>> the_dogs = ['NiceBoy', 'Homer', 'Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> solve(the_dogs, the_enemies)
5 0

Try any other case you want. You’ll get the incorrect answer of 0.
“OK fine, I see why the base case is wrong. But 0? 0 for every case? Why does nothing at all seem
to happen?”
In the correct solution (the one with return 1 in the base case instead of return 0), the effect is to
add 1 to our total each time we find a valid configuration of dogs. For example, if the answer ends
up being 14, then we must have hit the base case 14 times and accumulated those 14 values of 1.
But when the base case is return 0, every time we hit the base case, we just get a 0 back and
thereby accomplish nothing. There’s nothing else in the code that can add anything besides zeros
now! Things are happening, but since we’re just adding a bunch of zeroes, there’s no observable
effect.
Now let’s wreck the code in a different way. (Yeah… you don’t have to try too hard to get recursive
functions wrong!) Here we go. Try to guess what’s wrong with this before continuing with my
explanation below.
Chapter 1: The Bad Dogs Problem 15

1 def num_configs(dogs, enemies, current_park):


2 if dogs == []:
3 return 1
4
5 d = dogs.pop(0) # Remove first dog from list; call it d
6
7 without_dog = num_configs(dogs, enemies, current_park)
8
9 if dog_ok(enemies, current_park, d):
10 with_dog = num_configs(dogs, enemies, current_park + [d])
11 else:
12 with_dog = 0
13
14 return without_dog + with_dog

In previous versions of this function, we had to use dogs[1:] to refer to all of the dogs except the
first. So why not just remove dogs[0] from the list, and then forget about using dogs[1:] and use
just dogs?
That’s what this code does. It uses the list pop method to remove the first dog from the list (5), then
passes dogs to the recursive calls.
Seems like a reasonable solution. It even makes the code look a little cleaner, using d instead of
dogs[0] and avoiding the dogs[1:].

Clean or not, it’s wrong. Instead of getting the correct answer of 14 for our test case, we get something
else:

1 >>> the_dogs = ['NiceBoy', 'Homer', 'Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> solve(the_dogs, the_enemies)
5 6

What the hell? 6?


Think about the first call of this function that we make. When the function removes NiceBoy from
the dogs list, it’s gone forever. Poof! No way to get it back. When we make our first recursive call,
NiceBoy therefore isn’t in dogs. That in itself is fine–we don’t want NiceBoy in that recursive call
anyway. But that recursive call is also going to remove dogs from the dogs list and make them gone
forever. For example, the first thing it will do is remove Homer.
Eventually, we’ll resume the first call of our function, the one where NiceBoy is gone from dogs.
Except wait: other dogs are also gone now, like Homer, because recursive calls removed them! When
we make our second recursive call, the dogs list has fewer dogs in it than it should. (Homer is
nowhere in sight.) It’s for that reason that we get the wrong answer.
Chapter 1: The Bad Dogs Problem 16

Lesson: if you’re writing a recursive function that isn’t supposed to change a list, don’t even think
about changing that list. Operate on copies of the list and you’ll be much happier.

Bonus: Returning the Valid Configurations


We’ve spent a lot of time determining the number of valid dog configurations. But what if we want
the configurations themselves? This would look like a list of lists, where each inner list is one valid
configuration, like this:

1 >>> the_dogs = ['NiceBoy', 'Homer', 'Sky', 'Stretchy', 'Spino']


2 >>> the_enemies = [['Spino', 'NiceBoy'], ['Spino', 'Homer'],
3 ... ['Spino', 'Sky'], ['Sky', 'Homer']]
4 >>> all_solve(the_dogs, the_enemies)
5 [[], ['Stretchy'], ['Sky'], ['Sky', 'Stretchy'], ['Homer'],
6 ['Homer', 'Stretchy'], ['NiceBoy'], ['NiceBoy', 'Stretchy'],
7 ['NiceBoy', 'Sky'], ['NiceBoy', 'Sky', 'Stretchy'], ['NiceBoy', 'Homer'],
8 ['NiceBoy', 'Homer', 'Stretchy'], ['Spino'], ['Spino', 'Stretchy']]

To achieve this, we need a few related changes. Overall, we need to stop returning integers and
instead return lists.
For example, think about what we should do in the base case, aka when we’ve found a valid
configuration, we used to return 1. Not anymore: now we want to return the actual configuration
itself.
As you read the modified code, notice the effects of the change from returning integers to returning
a list. Here it is:

1 def all_configs(dogs, enemies, current_park):


2 """
3 Return a list of valid configurations that can be made from current_park
4 using the given dogs. A configuration is valid if it does not have a pair of dog\
5 s that are enemies.
6
7 dogs is a list of dog names.
8 enemies is a list of [dog_a, dog_b] enemies.
9 current_park is a list of the dogs already in the park;
10 that is, it is a valid configuration of dogs.
11 """
12 if dogs == []:
13 return [current_park]
14
15 without_dog = all_configs(dogs[1:], enemies, current_park)
Chapter 1: The Bad Dogs Problem 17

16
17 if dog_ok(enemies, current_park, dogs[0]):
18 with_dog = all_configs(dogs[1:], enemies, current_park + [dogs[0]])
19 else:
20 with_dog = []
21
22 return without_dog + with_dog
23
24
25 def all_solve(dogs, enemies):
26 """
27 Return a list of valid configurations using the given dogs.
28 A configuration is valid if it does not have a pair of dogs that are enemies.
29
30 dogs is a list of dog names.
31 enemies is a list of [dog_a, dog_b] enemies.
32 """
33 return all_configs(dogs, enemies, [])

Carefully check the base case (12)! We’re returning a list with this one configuration in it. We need the
list brackets in there to force the entire configuration to be a single element of our final configurations
list. It’s easy to get this wrong and instead put return current_park. I encourage you to try that
and see what happens.
Another change involves what happens when the current dog cannot be added to the current
configuration (19). Before, we set this with_dog variable to 0. Now, 0 doesn’t make sense – we
need lists, not integers! – so we set it to the empty list.
Our final return statement (21) is the same code as before, but it’s doing something very different
now. Rather than + doing integer addition, now it’s doing list concatenation: concatenating the two
lists of valid configurations to make our huge, final list of configurations.
One last thing, then I’ll let you go. There’s a neat connection between what we just did here –
generating the valid configurations themselves – and generating all subsets of a set. A subset is
just zero or more elements of a set. It could be no elements, or all of the elements, or something
in between. What’s neat is that if you simply include no pairs of enemy dogs, then you’ll end up
generating all subsets! The reason is that then you won’t exclude any configurations, because all
configurations are valid. This has nothing to do with our Bad Dogs problem: whenever you want
to generate all subsets of a set, you can use the code that we’ve written here and just leave out the
enemies stuff. Try it!
Chapter 1: The Bad Dogs Problem 18

Summary: What the Hell Is Recursion, Then?


Now we’ve seen our first example of solving a problem recursively, aka using recursion. Just to
make sure we don’t get lost in the specific example, I’m going to list the core of what I want you to
remember about recursion.

1. Recursion is a problem solving technique. “Wait wait, isn’t it when a function calls itself?”
Sure, that’s how it looks in code and what the computer does to execute it. But in this book,
and may I suggest more generally, I ask you to think of recursion as a way to solve a problem
by breaking it down into subproblems that are just smaller versions of the original problem.
Once we have subproblems that look just like the original problem (but smaller), we can use
recursion to solve them. Why? Because those subproblems can themselves be broken down
into subsubproblems, then subsubsubproblems, and so on, using the same approach that we
used on the original problem.
2. To use recursion, we need at least one base case and one recursive case.
3. A base case is a simple instance of the problem that you can solve without doing any more
recursion. You can just look at the problem instance and be like, “yep, the answer here is 1” (or
0, or the empty list, or the current configuration of dogs, or whatever the problem asks for).
4. A recursive case is a harder instance of the problem, one where you don’t immediately know
the answer. Really, it’s any instance that we can’t immediately solve in a base case. That said,
you almost know the answer, assuming that you can break the problem into subproblems. If
you can use the solutions to those subproblems to solve the original problem, then you’re all set
with your recursive case: just solve the subproblems recursively and then use those solutions
to solve the original problem.
5. When you make a recursive call, you need to think about it like any function call: by thinking
about what it does, not how it does it. The creepy thing here, that we don’t have when calling
some other function, is that you haven’t even finished writing the function yet, so how is a
recursive call supposed to work? Fair enough: this is creepy stuff. Just remember that eventually
these recursive calls will hit base cases. As long as the base cases are correct, and as long as our
original function correctly solves the problem using the solutions to the recursive calls, then
our full recursive solution will work.

To use recursion effectively, you might think that the most important skill is being able to trace all
of the function calls and returns. But no, that isn’t it. Rather, it’s the ability to see the solution to
a problem as not much more than the solution to simpler subproblems. Nail that skill and you nail
recursion, period.
Break a problem into subproblems of the same form as the original problem and use those
subproblem solutions to solve the original problem. That’s what we need to practice in order to
become recursion experts. In the next chapter, we continue toward that goal. We’ll meet a new
problem and a new decomposition of that problem into subproblems. But we’ll use the same kind
of recursive thinking that we just learned in the Bad Dogs problem. Do join me!
Chapter 2: The Best Team Problem
In Chapter 1, we learned what recursion is and how to write a recursive function. We even solved
one problem (the Bad Dogs problem) with recursion.
But one problem is not enough. The way to get good at recursion is to keep practicing with it and not
giving up. We need more practice! This chapter furnishes our second example of solving a problem
with recursion.

The Best Team


You ever notice how many North American NHL hockey team names basically mean, “we’re going
to whoop your ass”? Scary butt-kicking names like Predators, Hurricanes, Flames, and Avalanche.
How about the team called the Maple Leafs? Can they win? Well, they don’t have a scary name,
that’s for sure. If it were just based on names, then a maple leaf wouldn’t have a chance against a
predator. But games are won and lost on the ice, not on paper. …

The Problem
Suppose that we’re obsessed with a particular hockey league. The league has a bunch of teams that
play in it. For example, there might be the five teams Predators, Hurricanes, Flames, Avalanche, and
Maple Leafs. In a given season, each team plays each other team exactly once. For example, in our
five-team league here, the season would consist of the following games:

• Predators vs. Hurricanes


• Predators vs. Flames
• Predators vs. Avalanche
• Predators vs. Maple Leafs
• Hurricanes vs. Flames
• Hurricanes vs. Avalanche
• Hurricanes vs. Maple Leafs
• Flames vs. Avalanche
• Flames vs. Maple Leafs
• Avalanche vs. Maple Leafs
Chapter 2: The Best Team Problem 20

(Why does it work out to 10 games here? There are five teams, and each team plays four other
teams… shouldn’t the total number of games be 5 ∗ 4 = 20, then? Not quite, because this counts each
game twice. For example, it would count (Predators, Hurricanes) and (Hurricanes, Predators) as two
separate games. To undo this double counting, we need to divide by 2: 20/2 = 10 is the total number
of games in the season.)
Think about any game – let’s say the one between team A and team B. Each team wants a good
outcome for themselves in the game because that gives them points on the season.
There are three possible outcomes for the game:

1. Team A wins. In this case, team A gets three points and team B gets no points.
2. Team B wins. In this case, team B gets three points and team A gets no points.
3. The game is a tie. In this case, team A gets one point and team B gets one point.

In summary, you get three points if you win, one point if you tie, and zero points if you lose.
Now imagine that we’re somewhere in the middle of the season, with some games having already
been played. For each game that’s already been played, we know what its outcome is. For the games
that haven’t been played yet, we don’t know what their outcomes will be – anything could happen,
after all!
We say that a team that ends up with more points than each other team is called the President. Notice
with this definition that if two teams tie for the most points then we have no President.
We have a favourite team. (e.g. Maple Leafs.) Depending on what happens in the games that haven’t
been played yet, our favourite team might end up being the President or it might not. Maybe our
favourite team was doing really well already, has a bunch of games to play, and wins them all – in this
case, they could very likely end up being the President, despite other teams’ successes. Or maybe our
favourite team wasn’t doing so hot – then, even if they were to win all of their remaining games,
they still might not end up being the President. Or maybe our favourite team has no remaining
games, and whether they end up being the President depends on what other teams do.
The problem we want to solve is: in how many season outcomes will our favourite team be the
President?
Let’s stick with our five teams here and see an example. Imagine that 8 of the 10 games have already
been played. Let’s say that all of the Maple Leafs games have been played, and that the only games
that haven’t been played are Predators vs. Hurricanes and Hurricanes vs. Flames.
The points totals right now, after these 8 games have been played, might look like this:

team points
Predators 4
Hurricanes 4
Flames 0
Avalanche 6
Maple Leafs 7
Chapter 2: The Best Team Problem 21

There are 9 ways to finish the season. That’s because we have three possible outcomes for Predators
vs. Hurricanes, and for each of these we have three possible outcomes for Hurricanes vs. Flames. And
in most of these, the Maple Leafs don’t end up being the President. For example, if the Hurricanes
win one or both of their games, then they’ll have at least 7 points, tying or beating the Maple Leafs’
point total, and so the Maple Leafs won’t be President.
In fact, there are only two outcomes out of these nine where the Leafs do become the President. In
the first such outcome, Predators vs. Hurricanes is a tie and Hurricanes vs. Flames is a tie. In the
second such outcome, Predators vs. Hurricanes is a tie and Hurricanes vs. Flames is won by the
Flames. In the seven other outcomes, the Maple Leafs are not the President – there is always some
other team that gets at least 7 points. I encourage you to check all of these outcomes for yourself!
The correct answer for this test case is therefore 2.

You might also like