The problem: The Shrinking Function
Here is a question in the style of the ACSL Recursive Functions category. Read it now; by the end of Lesson 1 you will answer it with pencil and paper, and by the end of Lesson 3 you will write a program that answers it for any
x.Find f(20), where
f(x) = { f(x - 3) + x if x > 10 { 2 * x - 5 if x <= 10The definition of
fusesfitself. To knowf(20)you needf(17); to knowf(17)you needf(14); and so on. Does it ever stop? What number comes out?The programming part
Write a program that reads one whole number
xand printsf(x), for the samef.Sample Input
20Sample Output
73Constraints
-1000 <= x <= 2000
To solve it you need three skills, one per lesson: how to read an ACSL recursive definition and work it out with a call table (Lesson 1), the other kinds of recursive definitions ACSL asks about (Lesson 2), and how to write a recursive function in Python (Lesson 3).
-
Read a recursive definition in ACSL's cases notation, and tell the base case from the recursive case.
-
Evaluate it by hand with a call table: down to the base case, then back up.
-
Handle conditions with mod, floor and abs, functions of two variables, and multiple and indirect recursion.
-
Write recursive functions in Python, and follow the call stack.
-
Lesson 1 — Reading a recursive definition
A definition is recursive when it uses itself. Think of a set of nesting dolls: to open the big doll you open it and find a smaller doll, which you open in the same way, until you reach the tiny doll that does not open.
A recursive function has two kinds of rules:
- a recursive case, which gives the answer in terms of the same function on a different, usually smaller input, such as
f(x - 3) + x;
- a recursive case, which gives the answer in terms of the same function on a different, usually smaller input, such as
- a base case, which gives the answer directly, with no call to the function, such as
2 * x - 5.
The base case is the tiny doll. Without it the calls would never stop.
- a base case, which gives the answer directly, with no call to the function, such as
The cases notation
ACSL writes a recursive function as a list of rows inside one big bracket. Each row is a value, then the condition under which that row is used.
f(x) = { f(x - 2) + 1 if x > 3 { x if x <= 3Read it like this: "to find
f(x), look down the rows for the one whose condition is true for yourx, and work out that row's value." The conditions never overlap, so exactly one row fits eachx. A last row may sayotherwise, which means "when no row above fits".A few notes on how the definitions in this book are written:
We write It means ACSL may also write 2 * x2 times x 2xor2·xx mod 3the remainder when x is divided by 3 x mod 3abs(x)x without its minus sign abs(x)or│x│floor(x / 2)x divided by 2, rounded down to a whole number ⌊x/2⌋orint(x/2)<=,>=at most, at least ≤,≥!=is not equal to ≠For example,
floor(7 / 2)is3,17 mod 5is2, andabs(-4)is4.The call-table method
To find
f(8)for the definition above, work in two passes.Pass 1, going down. Start with
f(8). Find the row that fits, and write what it turns into. Keep going until you reach a base case, which is a plain number.call which row fits turns into f(8)8 > 3f(6) + 1f(6)6 > 3f(4) + 1f(4)4 > 3f(2) + 1f(2)2 <= 32(base case)Pass 2, coming back up. Now fill in the values from the bottom of the table to the top.
-
f(2) = 2 -
f(4) = f(2) + 1 = 3 -
f(6) = f(4) + 1 = 4 -
f(8) = f(6) + 1 = 5
-
So
f(8) = 5.During pass 1 you cannot work out any value yet, because each row is waiting for the row below it. Only the base case gives a number, and every other value is built from it on the way back up.
Solving the opening problem
Now try
f(20)from the top of the unit yourself, before you read on. Make the table, then fill in the values.call which row fits turns into f(20)20 > 10f(17) + 20f(17)17 > 10f(14) + 17f(14)14 > 10f(11) + 14f(11)11 > 10f(8) + 11f(8)8 <= 102 * 8 - 5 = 11(base case)Coming back up:
f(8) = 11,f(11) = 11 + 11 = 22,f(14) = 22 + 14 = 36,f(17) = 36 + 17 = 53, andf(20) = 53 + 20 = 73.The
+ xin each row uses thexof that row. In the row forf(14)it adds14, not20. Writing each row'sxin the table stops you from mixing them up.Checking pass 1 with a program
Pass 1 only has to follow the arguments
20, 17, 14, 11, 8until the base case. A short loop can list them for you. It subtracts 3 whilex > 10, just like the first row of the definition, and keeps the numbers in a string.x = 20 chain = str(x) while x > 10: x = x - 3 chain = chain + " " + str(x) print(chain)Scroll the code for more ↓
Output
20 17 14 11 8The loop lists the calls but does not add anything up; pass 2 is still yours to do. The version that reads
xwithinput()is saved asassets/l1.py.Try it x = int(input()) chain = str(x) while x > 10: x = x - 3 chain = chain + " " + str(x) print(chain)Scroll the code for more ↓
Run it from this unit's folder:
python assets/l1.py < assets/l1/1.inIt prints
20 17 14 11 8, the first column of your call table.Conditions that use mod, floor and abs
ACSL conditions are often more than
x > 10. Here the row depends on whetherxis odd or even:g(x) = { g(x - 1) + x if x > 0 and x mod 2 = 1 { g(floor(x / 2)) + 1 if x > 0 and x mod 2 = 0 { 0 if x = 0Find
g(13).call which row fits turns into g(13)13 is odd g(12) + 13g(12)12 is even g(6) + 1g(6)6 is even g(3) + 1g(3)3 is odd g(2) + 3g(2)2 is even g(1) + 1g(1)1 is odd g(0) + 1g(0)x = 00(base case)Coming back up:
g(0) = 0,g(1) = 1,g(2) = 2,g(3) = 5,g(6) = 6,g(12) = 7, andg(13) = 20.The argument does not always shrink by the same amount. Sometimes it drops by 1, sometimes it halves. Check the condition fresh for every row.
With
abs, the argument can even turn negative on the way down:h(x) = { h(abs(x) - 5) + 1 if abs(x) > 3 { x if abs(x) <= 3For
h(9):abs(9) = 9 > 3, soh(9) = h(4) + 1. Thenabs(4) = 4 > 3, soh(4) = h(-1) + 1. Nowabs(-1) = 1 <= 3, soh(-1) = -1, a base case. Coming back up:h(4) = 0andh(9) = 1.The base case returns
xitself, and herexis-1, so keep the minus sign.absis applied only where the definition writes it.Lesson 2 — More kinds of recursive functions
ACSL asks about a few different shapes of recursive function. The call-table method works for all of them; only the bookkeeping changes.
Two variables
A function can take two inputs. Each row changes one or both of them.
f(x, y) = { f(x - 2, y + 1) + y if x > y { y - x if x <= yFind
f(9, 1).call which row fits turns into f(9, 1)9 > 1f(7, 2) + 1f(7, 2)7 > 2f(5, 3) + 2f(5, 3)5 > 3f(3, 4) + 3f(3, 4)3 <= 44 - 3 = 1(base case)Coming back up:
f(5, 3) = 1 + 3 = 4,f(7, 2) = 4 + 2 = 6, andf(9, 1) = 6 + 1 = 7.xgoes down whileygoes up, so they meet. With two variables, keep both in every row of the table; the+ yuses that row'sy.Multiple recursion
In multiple recursion, one row calls the function more than once.
f(x) = { f(x - 1) + 2 * f(x - 2) if x >= 2 { x if x <= 1To find
f(4)you needf(3)andf(2). Each of those needs two more calls, so the calls branch out like a tree:f(4) / \ f(3) f(2) / \ / \ f(2) f(1) f(1) f(0) / \ f(1) f(0)The tree for
f(4)already has 9 calls, andf(2)is worked out twice. For a biggerxthe tree grows very fast.The table trick. Instead of drawing the tree, work upward from the base cases, one value at a time. Each new value needs only the two values just before it.
x 0 1 2 3 4 5 6 f(x) 0 1 1 + 2·0 = 1 1 + 2·1 = 3 3 + 2·1 = 5 5 + 2·3 = 11 11 + 2·5 = 21 So
f(4) = 5andf(6) = 21.The table gives the same answers as the tree, because
f(2)is always1, however many times it is asked for. Working bottom-up just avoids doing the same work twice. Be careful which value gets the2 *: here it is always the one two places back.A program can build the same table with a list. Position
xof the list holdsf(x), sotable[x - 1]andtable[x - 2]are the two values just before.n = 6 table = [] for x in range(n + 1): if x <= 1: table.append(x) else: table.append(table[x - 1] + 2 * table[x - 2]) print(table)Scroll the code for more ↓
Output
[0, 1, 1, 3, 5, 11, 21]The
ifinside the loop plays the part of the base-case row, and theelseplays the recursive row. The version that readsnand prints the row of the table with spaces is saved asassets/l2.py.Try it n = int(input()) table = [] for x in range(n + 1): if x <= 1: table.append(x) else: table.append(table[x - 1] + 2 * table[x - 2]) row = str(table[0]) for x in range(1, n + 1): row = row + " " + str(table[x]) print(row)Scroll the code for more ↓
Run it from this unit's folder:
python assets/l2.py < assets/l2/1.inIt prints
0 1 1 3 5 11 21, the bottom row of the table above.Indirect recursion
Intermediate and above.
In indirect recursion, one function calls a second function, which calls the first one back. Neither one calls itself directly, but together they go round in a loop until a base case stops them.
f(x) = { g(x - 1) + 1 if x > 0 { 0 if x <= 0 g(x) = { 2 * f(floor(x / 2)) if x > 1 { x + 1 if x <= 1Find
f(10). Add a column for which function is being called.call which row fits turns into f(10)10 > 0g(9) + 1g(9)9 > 12 * f(4)f(4)4 > 0g(3) + 1g(3)3 > 12 * f(1)f(1)1 > 0g(0) + 1g(0)0 <= 10 + 1 = 1(base case)Coming back up:
f(1) = 1 + 1 = 2,g(3) = 2 * 2 = 4,f(4) = 4 + 1 = 5,g(9) = 2 * 5 = 10, andf(10) = 10 + 1 = 11.Each row uses the rules of the function named in its call.
g(0)usesg's rows, even though it was called fromf.Does it ever stop?
Before you start a table, check that the calls head toward a base case.
f(x) = { f(x + 2) - 1 if x > 0 { 5 if x <= 0For
f(3):f(3)needsf(5), which needsf(7), and so on forever.xmoves away from the base case, so thisf(3)has no value. This is called infinite recursion. ACSL definitions always reach a base case, but when you invent your own, make sure every recursive row moves toward one.ACSL never asks about a function applied to its own result, such as
f(f(x)), so this book does not either.Lesson 3 — Recursion in Python
A Python function can call itself. Here is the opening problem's
f, written as a Python function. Compare it with the definition row by row.def f(x): if x > 10: return f(x - 3) + x return 2 * x - 5 print(f(20)) print(f(8))Scroll the code for more ↓
Output
73 11Each row of the cases notation becomes a branch of the function. The
ifholds the recursive case, which callsfagain onx - 3. The last line is the base case: it returns a number without callingf.f(20)gives73, the same answer as your call table.Watching the calls
Add two
printlines to see the calls happen: one when a call starts, and one just before it returns.def f(x): print("start f(" + str(x) + ")") if x > 10: answer = f(x - 3) + x else: answer = 2 * x - 5 print("f(" + str(x) + ") = " + str(answer)) return answer print(f(17))Scroll the code for more ↓
Output
start f(17) start f(14) start f(11) start f(8) f(8) = 11 f(11) = 22 f(14) = 36 f(17) = 53 53All the
startlines come first, going down:17,14,11,8. Then the answers come back up in the opposite order:f(8)first,f(17)last. That is exactly the two passes of the call table: Python does pass 1 and pass 2 for you.The call stack
When
f(17)callsf(14), the callf(17)is not finished: it still has to add17. Python remembers every unfinished call, each with its ownx, in a pile called the call stack.f(8) <- running now; it is a base case, so it returns 11 f(11) waiting to add 11 f(14) waiting to add 14 f(17) waiting to add 17A new call goes on top of the pile. When the top call returns, it is removed, and the call below it carries on with the returned value. Because each call has its own
x, thexinf(11)stays11whilef(8)runs.The pile cannot grow forever. Python stops a program with a
RecursionErrorwhen calls go about 1000 deep. That happens when a function has no base case, or when a recursive case moves away from it:Error demo: this code stops with an error def broken(x): return broken(x - 1) + 1 print(broken(5))Scroll the code for more ↓
brokenhas no base case, so the calls gobroken(4),broken(3), ...,broken(-990), ... until Python gives up. This program is not run here: it could only end in an error. Two rules keep a recursive function safe: check the base case before you recurse, and make sure every recursive call moves toward a base case.mod, floor and abs in Python
The ACSL operations have Python partners you already know:
Definition Python x mod 2x % 2floor(x / 2)x // 2abs(x)abs(x)Here is
gfrom Lesson 1.def g(x): if x > 0 and x % 2 == 1: return g(x - 1) + x if x > 0 and x % 2 == 0: return g(x // 2) + 1 return 0 print(g(13)) print(-7 // 2)Scroll the code for more ↓
Output
20 -4//always rounds down, so-7 // 2is-4, not-3. That matchesfloor, even for negative numbers. The definitions in this book usemodonly with numbers that are 0 or more, where%matches it exactly.Two variables, multiple and indirect recursion
Every kind of definition from Lesson 2 goes into Python the same way: one branch per row.
def two(x, y): if x > y: return two(x - 2, y + 1) + y return y - x def many(x): if x >= 2: return many(x - 1) + 2 * many(x - 2) return x print(two(9, 1)) print(many(6))Scroll the code for more ↓
Output
7 21manycalls itself twice in one line, just like the tree in Lesson 2. It gives21, the same as the table. For largexit gets slow, because it repeats work: the table method is faster by hand and in code.For indirect recursion, write both functions. Python looks up
gonly whenfactually runs, so it does not matter thatfis written first.def f(x): if x > 0: return g(x - 1) + 1 return 0 def g(x): if x > 1: return 2 * f(x // 2) return x + 1 print(f(10))Scroll the code for more ↓
Output
11Returning two things at once
Sometimes you want a function to report two results, for example its value and how many calls it took. Return a tuple, and unpack it where you call the function.
def value_and_calls(x): if x > 10: value, calls = value_and_calls(x - 3) return (value + x, calls + 1) return (2 * x - 5, 1) value, calls = value_and_calls(20) print(value, calls)Scroll the code for more ↓
Output
73 5The base case counts itself as 1 call, and every recursive case adds 1 for itself to the count its call gave back.
f(20)takes 5 calls:f(20),f(17),f(14),f(11)andf(8).Put it together: here is a complete solution to the programming part of the opening problem. It reads
xand printsf(x). It is saved asassets/l3.py.Try it def f(x): if x > 10: return f(x - 3) + x return 2 * x - 5 x = int(input()) print(f(x))Scroll the code for more ↓
Run it from this unit's folder:
python assets/l3.py < assets/l3/1.inThe definition becomes the function, and the rest of the program is just "read, call, print". Test the edges the constraints allow: a base case straight away (
assets/l3/2.in), a value just above the boundary (assets/l3/3.in), a negativex(assets/l3/4.in), and the largestx(assets/l3/5.in), which makes about 660 calls, safely under Python's limit.-
A recursive definition needs a base case that stops the calls.
-
A call table works in two passes: pass 1 goes down to the base case, and pass 2 puts the values back up. For the opening problem, f(20) = 73.
-
For multiple recursion, fill a table upward from the base cases instead of drawing the whole tree.
-
A Python function can call itself; the call stack remembers every unfinished call, and a missing base case ends in a
RecursionError.
-
You can now evaluate an ACSL recursive function by hand and check it with a program.