Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Assignment D: Recursive Problem Solving (15 Pts + 4 Extra Pts)
Recursion is not just a *"function calling itself"*, it is a way of thinking
about a class of problems that can be split into simple "*base cases"* and
remaining *"sub-problems"* that are *"self-similar"*.
A *"sub-problem"* is self-similar when it exactly looks the same as the
original problem, just smaller (e.g. reduced by one element). At some point,
the simple "*base case"* has been reached that yields a primitive solution.
A recursive *solution function* exploiting self-similarity has two phases:
1. *Reduction:* - slicing the problem (e.g. a list of numbers) into one
element (e.g. the first number) and a remaining *sub-problem*
(e.g. the list of remaining numbers).
1. *Recursion:* - invoke the same function for the *sub-problem*
until the *sub-problem* has been reduced to the *base case*.
Return the solution for the *base case*.
1. *Construction:* - results of recursive invocations are considered
as solutions of *sub-problems* and are combined with the element
that was isolated at the particular level of recursion.
While this approach is elegant from a thinking-about-problems and programming
point of view, it has cost associated for using the
[Callstack](https://en.wikipedia.org/wiki/Call_stack)
using a data structure of an abstract data type
[Stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))
for recursions.
### Challenges
- [Challenge 1:](#1-challenge-simple-recursion-sum-numbers) Simple recursion: *sum* numbers
- [Challenge 2:](#2-challenge-fibonacci-numbers) Fibonacci numbers
- [Challenge 3:](#3-challenge-permutation) Permutation
- [Challenge 4:](#4-challenge-powerset) Powerset
- [Challenge 5:](#5-challenge-find-matching-pairs) Find Matching Pairs
- [Challenge 6:](#6-challenge-combinatorial-problem-of-finding-numbers) Combinatorial Problem of Finding Numbers
- [Challenge 7:](#7-challenge-hard-problem-of-finding-numbers) Hard Problem of Finding Numbers
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
Points: [2, 1, 2, 2, 2, 3, 2, +4 extra pts]
File [recursion.py](recursion.py) has function headers defined for each challenge.
Use those functions and complete code.
### 1.) Challenge: Simple recursion: *sum()* numbers
Computing the *sum* of numbers is most often performed *iteratively* as a loop
over given numbers and adding them in a result variable.
Solving the problem *recursively* illustrates the concept of self-similarity
and recursive problem solving.
Use the following approach:
1. *Reduction:* - split the given list of numbers into a first element (first number)
and a list of remaining numbers (*sub-problem*). Remember the first element.
1. *Recursion:* - invoke *sum()* for the list of remaining numbers until the base case
has been reached: *sum( [ ] ) = 0* or *sum( [n] )=n*.
1. *Construction:* - add the remembered element to the value returned from the
recursive invocation of *sum()*.
Complete: `sum(_numbers)` using this approach:
```py
def sum(self, _numbers) -> int:
# your code
return #...
```
Remove comment from `run_choices` and run the program:
```py
run_choices = [
1, # Challenge 1, Simple recursion: sum numbers
...
]
```
Output:
```
n1.numbers: [9, 4, 8, 10, 2, 4, 8, 3, 14, 4, 8]
sum(n1.numbers): 74
```
Answer questions:
1. How many time is the *"first element"* stored?
How much memory is used for applying the function to a list of *n* numbers?
1. What is the run-time estimate for *sum()* given a list of *n* numbers?
1. How many *stack-frames* are used for a list of *n* numbers?
(2 Pts)
### 2.) Challenge: Fibonacci numbers
[Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_number) were first
described in Indian mathematics as early as 200 BC in works by *Pingala* on
enumerating possible patterns of Sanskrit poetry formed from syllables of two lengths.
Italian mathematician *Leonardo of Pisa*, later known as
*[Fibonacci](https://en.wikipedia.org/wiki/Fibonacci)*,
introduced the sequence to Western European mathematics in his 1202 book
*[Liber Abaci](https://en.wikipedia.org/wiki/Liber_Abaci)*.
Numbers of the *Fibonacci sequence* are defined as: *fib(0): 0*, *fib(1): 1*, *...*
and each following number is the sum of the two preceding numbers.
Fibonacci numbers are widely found in *nature*, *science*, *social behaviors* of
populations and *arts*, e.g. they form the basis of the
[Golden Ratio](https://www.adobe.com/creativecloud/design/discover/golden-ratio.html),
which is widely used in *painting* and *photography*, see also this
[1:32min](https://www.youtube.com/watch?v=v6PTrc0z4w4) video.
<img src="../markup/img/fibonacci.jpg" alt="drawing" width="640"/>
<!--  -->
Complete functions `fib(n)` and `fib_gen(n)`.
```py
def fib(self, _n) -> int:
# return value of n-th Fibonacci number
return #...
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# return a generator object that yields two lists, one with n and the
# other with corresponding fib(n)
yield #...
```
Remove comment from `run_choices` and run the program:
```py
run_choices = [
1, # Challenge 1, Simple recursion: sum numbers
2, # Challenge 2, Fibonacci numbers
...
]
```
Output:
```
n: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
fib(n): [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
```
Answer questions:
1. Explain the concept of a generator in Python.
1. Why can't `fib(60)` or `fib(90)` be computed recursively?
1. What is the more limiting constraint: memory use or needed run time?
```py
n = 30
print(f'fib({n}): {n1.fib(n)}')
n = 60
print(f'fib({n}): {n1.fib(n)}') # ??
n = 90
print(f'fib({n}): {n1.fib(n)}') # ??
```
Understand the problem and use a technique called
[memoization](https://stackoverflow.com/questions/7875380/recursive-fibonacci-memoization)
to make the solution work for *n=60* and *n=90* - still recursively (!).
Remove comments `#21` and `#22` from `run_choices` and run the program:
Output:
```
fib(30): 832040
fib(60): 1548008755920
fib(90): 2880067194370816120
```
(2 Pts)
### 3.) Challenge: Permutation
[Permutation](https://en.wikipedia.org/wiki/Permutation) is a list of all
arrangements of elements.
For example:
```py
perm([1, 2, 3]) -> [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
perm([]) -> [[]]
perm([1]) -> [[1]]
perm([1, 2]) -> [[1, 2], [2, 1]]
perm([1, 2, 3]) -> [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
perm([1, 2, 3, 4]) -> [[1, 2, 3, 4], [1, 2, 4, 3], ... [4, 3, 1, 2], [4, 3, 2, 1]]
```
Find a pattern how numbers are arranged for `perm([1, 2])` and `perm([1, 2, 3])`
and adapt it for `perm([1, 2, 3, 4])` to understand the algorithm.
Writing non-recursive code for that algorithm can be difficult, but it fits
well with the recursive sub-problen approach, which is elegant with the
four steps:
1. Return solutions for trivial cases: `[]`, `[1]`, `[1, 2]`.
1. Split the problem by removing the first number `n1` from the list leaving `r` as
remaining list (sub-problem).
1. Invoke `perm(r)` recursively on the remaining list.
1. Combine the result returned from `perm(r)` by adding `n1` to each element.
```py
def perm(self, _numbers) -> list:
res=[] # collect result
# code...
# 1. Return solutions for trivial cases: `[]`, `[1]`, `[1, 2]`.
# 2. Split the problem by removing the first number `n1` from the list
# leaving `r` as remaining list (sub-problem).
# 3. Invoke `perm(r)` recursively on the remaining list.
# 4. Combine the result by adding `n1` to each returned element from `perm(r)`.
#
return res
lst = [1, 2, 3]
perm = n1.perm(lst)
print(f'perm({lst}) -> {perm}')
lst = [1, 2, 3, 4]
perm = n1.perm(lst)
print(f'perm({lst}) -> {perm}')
```
Remove comment `#3` from `run_choices` and run the program:
Output:
```
perm([1, 2, 3]) -> [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
perm([1, 2, 3, 4]) -> [[1, 2, 3, 4], [1, 2, 4, 3], ... [4, 3, 1, 2], [4, 3, 2, 1]]
```
Answer questions:
- With a rising length of the input list, how does the number of permutations grow?
(2 Pts)
### 4.) Challenge: Powerset
[Powerset](https://en.wikipedia.org/wiki/Powerset) is a list of all
subsets of elements including the empty set.
For example:
```py
pset([1, 2, 3]) -> [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
```
Undertstand the pattern and complete function `pset()`.
```py
def pset(self, _numbers) -> list:
res=[] # collect result
# code...
# 1. Return solutions for trivial cases: `[]`, `[1]`, `[1, 2]`.
# 2. Split the problem by removing the first number `n1` from the list
# leaving `r` as remaining list (sub-problem).
# 3. Invoke `pset(r)` recursively on the remaining list.
# 4. Combine the result with the first element.
#
return res
lst = [1, 2, 3]
pset = n1.pset(lst)
print(f'pset({lst}) -> {pset}')
```
Remove comment `#4` from `run_choices` and run the program:
Output:
```py
pset([1, 2, 3]) -> [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
```
Answer questions:
- With a rising length of the input list, how does the size of the Powerset grow?
(2 Pts)
### 5.) Challenge: Find Matching Pairs
Write three functions to `find` elements in a list.
The first function to `find` elements that match a boolean `match_func`.
A second function `find_adjacent` that finds all indexes of adjacent pairs
of numbers.
The third function `find_pairs` that finds all pairs of numbers (not necessarily
adjacent) with the sum equal to `n`. For example, `n=12` can be combined from
the input list with pairs: `[3, 9], [4, 8], [2, 10]`.
```py
def find(self, _numbers, match_func) -> list:
res = [] # code...
return res
def find_adjacent(self, pair, _numbers) -> list:
res = [] # code...
return res
def find_pairs(self, n, _numbers) -> list:
res = [] # code...
return res
lst = [9, 4, 8, 10, 2, 4, 8, 3, 14, 4, 8] # input list
#
div3 = n1.find(lst, match_func=lambda n : n % 3 == 0)
print(f'find numbers divisible by 3: {div3}')
#
p = [4, 8] # find all indexes of adjacent numbers [4, 8]
adj = n1.find_adjacent(p, lst)
print(f'find_adjacent({p}, list): {adj}')
#
n = 12 # find all pairs from the input list that add to n
pairs = n1.find_pairs(n, lst)
print(f'find_pairs({n}, list) -> {pairs}')
```
Remove comments `#5`, `#51` and `#52` from `run_choices` and run the program:
Output:
```
find numbers divisible by 3: [9, 3]
find_adjacent([4, 8], list): [1, 5, 9]
find_pairs(12, list) -> [[3, 9], [4, 8], [2, 10]]
```
Answer questions:
- With a rising length of the input list, how many steps are needed to
complete each function in the best and worst case and on average?
| function | answers |
| ---------------- | ----------- |
| `find` | best case: ______, worst case: ______, average: ______ steps. |
| `find_adjacent` | best case: ______, worst case: ______, average: ______ steps. |
| `find_pairs` | best case: ______, worst case: ______, average: ______ steps. |
(3 Pts)
### 6.) Challenge: Combinatorial Problem of Finding Numbers
`find_all_sums` is a function that returns any combination of numbers from the
input list that add to `n`. For example, `n=14` can be combined from an
input list: `[8, 10, 2, 14, 4]` by combinations: `[4, 8, 2], [4, 10], [14]`.
A first approach to the problem is to understand the nature of possible
combinations from the input list. If all those combinations could be
generated, each could be tested whether their elements add to `n` and if,
collect them for the final result.
The order of numbers in solutions is not relevant (summation is commutative).
Duplicate solutions with same numbers, but in different order need be to removed.
```py
def find_all_sums(self, n, _numbers) -> list:
res = [] # code...
return res
lst = [8, 10, 2, 14, 4] # input list
n = 14
all = n1.find_all_sums(n, lst)
print(f'find_all_sums({n}, lst) -> {all}')
```
Output:
```py
find_all_sums(14, lst) -> [[4, 8, 2], [4, 10], [14]]
```
Test your solution with a larger input set:
```py
lst = [ # input list
260, 720, 225, 179, 101, 767, 167, 200, 157, 289,
318, 303, 153, 290, 201, 594, 457, 607, 592, 246,
]
n = 469
all = n1.find_all_sums(n, lst)
print(f'find_all_sums({n}, lst) -> {all}')
```
Remove comments `#6`, and `#61` from `run_choices` and run the program:
Output:
```
find_all_sums(469, lst) -> [[179, 290], [101, 167, 201]]
```
Answer questions:
- With a rising length of the input list, how does the number of possible
solutuions rise that must be tested?
(2 Pts)
### 7.) Challenge: Hard Problem of Finding Numbers
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
Larger data sets can no longer be solved *"brute force"* by exploring all possible
2^n combinations.
Find a solution using a recursive approach exploring a decision tree or
with tabulation.
```py
lst = [ # input list
260, 720, 225, 179, 101, 767, 167, 200, 157, 289,
318, 303, 153, 290, 201, 594, 457, 607, 592, 246,
132, 135, 584, 432, 591, 204, 417, 405, 362, 658,
136, 751, 583, 536, 293, 493, 431, 780, 563, 703,
400, 618, 397, 320, 513, 708, 319, 317, 685, 347,
758, 439, 145, 378, 158, 384, 551, 110, 408, 648,
847, 498, 50, 19, # 64 numbers
]
n = 469
all = n1.find_all_sums(n, lst)
for i, s in enumerate(all):
print(f' - {i+1:2}: sum({sum(s)}) -> {s}')
```
Sort output by lenght of solution (use length as primary and numeric value
of first element as secondary criteria).
Remove comment `#7` if you tackled the challenge and run the program:
Output:
```
1: sum(469) -> [290, 179]
2: sum(469) -> [19, 157, 293]
3: sum(469) -> [19, 246, 204]
4: sum(469) -> [19, 318, 132]
5: sum(469) -> [19, 400, 50]
6: sum(469) -> [50, 101, 318]
7: sum(469) -> [110, 201, 158]
8: sum(469) -> [136, 201, 132]
9: sum(469) -> [145, 167, 157]
10: sum(469) -> [158, 179, 132]
11: sum(469) -> [201, 101, 167]
12: sum(469) -> [19, 101, 204, 145]
13: sum(469) -> [19, 157, 135, 158]
14: sum(469) -> [19, 179, 135, 136]
15: sum(469) -> [19, 204, 136, 110]
16: sum(469) -> [19, 290, 110, 50]
17: sum(469) -> [19, 101, 167, 132, 50]
18: sum(469) -> [19, 132, 158, 110, 50]
```
( +4 Extra Pts)