Дата и время публикации:
Проблема и решение
1. Суть проблемы
Обычно, рекурсию принято рассматривать на примере факториала, который описывается формулой (1) [3.1]
(1) $n! = 1 \times 2 \times 3 \times...\times n\times(n-1)!$
Ее запись выглядит последовательностью выполнения операций умножения $n \times (n-1)!$, где $n = 1$ является базовым случаем или ограничением числа рекурсивных вызовов, как показано в листинге 1.1
Листинг 1.1
print ("===== n! =====")
nmax=5
def sf(n:int,t:str)->int:
if n == 1 :
print( 'o:'+t+str(int(nmax-n))+', x: '+str(n))
return 1
print( 'item:'+t+str(int(nmax-n))+', n: '+str(n))
return n* sf(n-1,'a')
print ('Res: ' + str(sf(nmax,'r')))
...
Как следует из листинга 1.1, она заключается в рекурсивном вызове функции $n \times f(n-1)$ и умножения возвращаемого значения согласно формулы (1), что подтверждает дамп 1.2, в котором выполняется код из листинга 1.1
Дамп 1.2
===== n! ===== item:r0, n: 5 item:a1, n: 4 item:a2, n: 3 item:a3, n: 2 o:a4, x: 1 Res: 120 ...
Что объяснимо, когда, согласно листингу 1.1, значение $n-1$ достигнет базового случая и станет равным 1 будет решена формула (1)
2. Решение
Давайте представим, что есть более сложная формула (2)
(2) $f(x) = f(x-1) + 2 \times f(x-2) + 1$ , где $f(0) = 0 || f(1) = 1$,
которую решал последовательно: сначала, упростил до рекурсивной формулы (3)
(2) $f(x) = f(x-2) + 1$
Её реализация на Python показана в листинге 2.1
Листинг 2.1
...
print ("===== f(x-2) + 1 =====")
nmax=8
def cf(x:int,t:str)->int:
if x == 1 or x == 0 :
print( 'o:'+t+str(int(nmax-x))+', x: '+str(x))
return 1
print( 'i:'+t+str(int(nmax-x))+', x: '+str(x))
return cf(x-2,'a') + 1
print ('Res: ' + str(cf(nmax,'r')))
...
В результате, будет последовательно высчитана рекурсивно формула (3) :
x: 8 f(8) = f(6) + 1 = 5 x: 6 f(6) = f(4) + 1 = 4 x: 4 f(4) = f(2) + 1 = 3 x: 2 f(2) = f(0) + 1 = 2 x: 0 f(0) = 1
Теперь усложним формулу (3) и добавим множитель 2, как показано в формуле (4)
p style="text-align:center">(2) $f(x) = 2 \times f(x-2) + 1$,которую воплотил в Python, как показано в листинге 2.2
Листинг 2.2
...
print ("===== 2*f(x-2) + 1 =====")
nmax=7
def cf(x:int,t:str)->int:
if x == 1 or x == 0 :
print( 'o:'+t+str(int(nmax-x))+', x: '+str(x))
return 1
print( 'i:'+t+str(int(nmax-x))+', x: '+str(x))
return 2*cf(x-2,'a') + 1
print ('Res: ' + str(cf(nmax,'r')))
...
Соответственно, получил следующие итерации вызова рекурсивной формулы (4):
f(7) = 2*f(5) + 1 = 15 f(5) = 2*f(3) + 1 = 7 f(3) = 2*f(1) + 1 = 3 f(1) = 1
В конце, добавил f(x-1) к формуле (4), тем самым привел ее к изначальному виду, который соответстыует формуле (2) и формуле (5), последняя имеет более приземленный вид для отслеживания всех переходов в дереве рекурсивных вызовов:
(5) $fb(x-1) + 2 \times fa(x-2) + 1$ , где $fb(0) == 1$, $fa(0) == 1$ и $fb(1) == 1$, $fa(1) == 1$
Она показывает наличие ветвления рекурсивной функции вправо — $fb(x) и влево из формулы (2) и упрошает реализацию в Python, как показано в листинге 2.3
Листинг 2.3
...
print ("===== f(x-1) + 2*f(x-2) + 1 =====")
nmax=5
def cf(x:int)->int:
if x == 1 or x == 0 :
return 1
return cf(x-1) + 2*cf(x-2) + 1
print (cf(nmax,'r'))
...
В результате было получен число, равное 31, что является правильным решением, которое можно объяснить, если изготовить синтетический код, как показано в листинге 2.4
Листинг 2.4
print ("===== f(x-1) + 2*f(x-2) + 1 =====")
nmax=5
xarrs=list()
def cf(x:int,t:str,bn:int,an:int, xarrs:listint,int],str])->int:
if t == 'a' :
an += 1
if t == 'b' :
bn += 1
if t == 'r' :
an = 0
bn = 0
xs=(lambda x,t: x+2 if t == 'a' else ( x+1 if t == 'b' else x))(x,t)
svalue=f"{t}({xs}) => f({x})"
xarrs.append(bn,an],svalue])
if x == 1 or x == 0 :
return 1
return cf(x-1,'b',bn,an,xarrs) + 2*cf(x-2,'a',bn,an,xarrs) + 1
print ('Res: ' + str(cf(nmax,'r', 0,0,xarrs)))
print ('Recursive calls table:')
xtable = xarrs
i=0
while i < xtable.__len__() :
print (xtable[i])
i += 1
Он будет регистрировать, в моем случае на процессоре AMD Ryzen 7 3750H, следующие несовсем внятные результаты, показанные в дампе 2.5
Листинг 2.5
... ===== f(x-1) + 2*f(x-2) + 1 ===== Res: 31 Recursive calls table: [[0, 0], 'fr(5) => f(5)'] [[0, 1], 'fa(5) => f(3)'] [[0, 2], 'fa(3) => f(1)'] [[1, 0], 'fb(5) => f(4)'] [[1, 1], 'fa(4) => f(2)'] [[1, 1], 'fb(3) => f(2)'] [[1, 2], 'fa(2) => f(0)'] [[1, 2], 'fa(2) => f(0)'] [[2, 0], 'fb(4) => f(3)'] [[2, 1], 'fa(3) => f(1)'] [[2, 1], 'fb(2) => f(1)'] [[2, 1], 'fb(2) => f(1)'] [[3, 0], 'fb(3) => f(2)'] [[3, 1], 'fa(2) => f(0)'] [[4, 0], 'fb(2) => f(1)']
Поэтому, когда просортировал таблицу по первому элементу [3.6], как показано в листинге 2.6
Листинг 2.6
... ===== f(x-1) + 2*f(x-2) + 1 ===== Res: 31 Recursive calls table: [[0, 0], 'r(5) => f(5)'] [[1, 0], 'b(5) => f(4)'] [[2, 0], 'b(4) => f(3)'] [[3, 0], 'b(3) => f(2)'] [[4, 0], 'b(2) => f(1)'] [[3, 1], 'a(2) => f(0)'] [[2, 1], 'a(3) => f(1)'] [[1, 1], 'a(4) => f(2)'] [[2, 1], 'b(2) => f(1)'] [[1, 2], 'a(2) => f(0)'] [[0, 1], 'a(5) => f(3)'] [[1, 1], 'b(3) => f(2)'] [[2, 1], 'b(2) => f(1)'] [[1, 2], 'a(2) => f(0)'] [[0, 2], 'a(3) => f(1)']
В результате получил следующие решения рекурсивной функции (2), как показано на схеме 2.7, которая содержит переход рекурсии от узла к узлу, прикинутые на пальцах.
Рисунок 2.7
Показанные выше переходы от одного узла к другому формируют следующую таблицу последовательности рекурсивных вызовов, одновременно служащей таблицей истинности всех состояния числа x (=5) для рекурсивно вызываемых $fa()$ и $fb()$, как показано в таблице 2.8
| Адрес | A | B | C | D | E | F | |
|---|---|---|---|---|---|---|---|
| 0 | (0,0) – f0(5) | ||||||
| 1 | (1,0) – fb(4) | (0,1) – 'fa(3)' | |||||
| 2 | (2,0) – fb(3) | (1,1) – fa(2) | (1,1) – fb(2) | (0,2) – fa(1) | (0,1) – 'fa(3)' | ||
| 3 | (3,0) – fb(2) | (2,1) – fa(1) | (2,1) – fb(1) | (1,2) – fa(0) | (2,1) – fb(1) | (1,2) – fa(0) | |
| 4 | (4,0) – fb(1) | (3,1) – fa(0) | |||||
Затем, решил задачу на Python получения выше представленной таблицы 2.8 и показанного на рисунке 2.7 графа истинности всех состояний числа X для рекурсивно вызываемых $fa()$ и $fb()$. Код на Python можно скачать с моей странички в gist.github.com, после выполнения которого смог произвести расчеты, подтверждающие получение числа 31, как показано на рисунке 2.10
Рисунок 2.10
Таким образом, сколько бы не было бы членов у рекурсивной функции, порождающие дополнительно вложенные рекурсивные вызовы, и какая бы она не была сложной, все расчеты для выбранной рекурсивной функции следует производит, снизу вверх с точки, которая называется базовым случаем, как наглядно показывает алгоритм на рисунке 2.10 и приведенные выше расчеты для формул (3) и (4).
3. Библиография
3.1 RapidTables.com — Factorial (n!)
3.2 Tree Traversal in Python Using Recursion (With Code)
3.4 Python | Sort a list according to the second element in sublist