Обновлено: Мой первоначальный рекурсивный код был неполным, так как я не умножал на f(l, r) сумму двух рекурсивных ветвей.
Учитывая функцию f(l, r), которая вычисляет что-то из узлов (l, r) двоичного дерева высоты max_height, я хочу вычислить общее значение, сложив значения левого и правого дочерних узлов, умножить сумму на значение родительского узла и передать это вдоль дерева.
У меня есть работающая рекурсивная реализация этого, но теперь я хочу устранить рекурсию с помощью цикла while и структур стека. Моя проблема в том, что я не знаю, как «передавать» значения во время цикла while. То есть я не знаю, как мне воспроизвести поведение, когда я умножаю текущее значение f(l, r) на сумму двух ветвей рекурсии.
Я включил два фрагмента кода: первый — текущая рекурсивная реализация, а второй — моя попытка более итеративного подхода. Последний код требует доработки, и я разместил TODO с комментариями, чтобы указать на некоторые из моих вопросов.
def recursive_travel(l, r, cur_height, max_height):
if cur_height == max_height - 1:
return f(l, r) * (f(l + 1, r) + f(l, r + 1))
return f(l, r)* (recursive_travel(l + 1, r, cur_height + 1, max_height) + recursive_travel(l, r + 1, cur_height + 1, max_height))
где будет первоначальный звонок recursive_travel(0, 0, 0, max_height).
Попытайтесь удалить рекурсию:
def iterative_travel(max_height):
call_stack = [(0, 0, 0)] # cur_height, l, r in that order
handled_stack = [] # TODO: Maybe I need to have something like this, or maybe I need a double array to store computed values?
# Precompute the value of r_c directly to an n x n table for fast access
pre_f = [[f(l, r) for l in range(0, max_height + 1)] for r in range(0, max_height + 1)]
while call_stack:
cur_height, l, r = stack.pop()
if max_height - 1 == cur_height:
# TODO: Not sure how to pass on the computed values
# TODO: Where I should put this value? In some table? In some stack?
value = pre_f[l, r] * (pre_f[l + 1, r] + pre_f[l, r + 1])
# TODO: Should I mark somewhere that the node (l, r) has been handled?
elif handled_stack:
# TODO: Not sure how to handle the computed values
pass
else:
# TODO: Do I do something to the current l and r here?
stack.append((current_depth + 1, l + 1, r))
stack.append((current_depth + 1, l, r + 1))
return 0 # TODO: Return the correct value
Какая у тебя причина это делать? Зачем усложнять, если у вас уже есть что-то простое и естественное?
Не могли бы вы предоставить минимальный пример использования для проверки кода, предоставленного в ответе, на соответствие исходной рекурсивной версии?
Я предлагаю вам прочитать reddit.com/r/compsci/comments/5mpozl/…, чтобы увидеть, как классифицируются узлы бинарного дерева и что такое листовые узлы (чтобы понять мое редактирование вашего вопроса).
Вы говорите о двоичном дереве, но я не вижу в вашем коде узлов или другой древовидной структуры данных, только целые числа. Можете ли вы уточнить?
Можете ли вы ответить на мой вопрос? В одном комментарии вы говорите «для быстрого доступа», и сложность вашего выполнения ужасна, поэтому, если вы пытаетесь сделать это ради скорости, вы пытаетесь пойти неправильным путем, и есть гораздо лучшее решение.
@nocomment Извините, что отвечаю поздно: рекурсивный подход работает хорошо, но время его выполнения даже с мемоизацией довольно плохое в приложении, с которым я работаю. По этой причине я пытаюсь выжать из кода как можно больше эффективности, устраняя ненужные вызовы рекурсии. Я знаю, что я мог бы также переключиться на другой язык, например, Rust или C++, но даже там устранение рекурсии не снизит производительность.
@trincot Забудьте мой предыдущий комментарий, он был неверным: пара (l, r) левого и правого поворотов сверху не характеризует однозначно узел в данном бинарном дереве, поскольку априори мы не указываем, в каком порядке мы выполняем повороты. Однако 1.) функция f и в этом случае, и в моем конкретном приложении зависит только от общего количества левых и правых поворотов, 2.) Меня волнует только общая сумма-произведение формы f(l, r) * (f(l + 1, r) + f(l, r+1)), поэтому по этой причине никакой дополнительной структуры данных не требуется (по крайней мере, на мой наивный взгляд).
Хорошо, я так и думал. Вот только мемоизация должна очень помочь. И я не думаю, что вы сделаете это быстрее, моделируя рекурсию в коде Python с помощью собственного стека вместо использования обычной рекурсии, встроенной в Python. Обычно это делает процесс не только более сложным, но и медленным. Я думаю, вам лучше спросить о запоминаемом рекурсивном решении и предоставить информацию о времени выполнения и max_height, чтобы мы действительно могли сделать что-то продуктивное. Похоже на проблему XY.
Спасибо за разъяснение, @Epsiloon. Это соответствует тому, что я понял, когда писал ответ. Было бы хорошо объяснить в своем вопросе, почему вы хотите перейти на итеративную версию кода. Просто ради вызова? Из соображений экономии времени? Из соображений эффективности памяти? В последних случаях, каковы входные данные и определение f, из-за которых у вас возникают проблемы?
Позвольте мне перефразировать: мемоизация должна сделать его оптимальным (если только ваш настоящий f не обладает каким-то свойством, позволяющим что-то делать еще быстрее). И нет смысла пытаться повторить немемоизированную рекурсию с помощью итеративной версии. Чтобы решить вашу реальную проблему, вам нужно спросить о вашей реальной проблеме. Имея достаточно информации, чтобы понять/воспроизвести это.
@nocomment Сначала я попробовал мемоизацию, и это очень помогло. Точная проблема, над которой я работаю, использует этот алгоритм с одним контекстно-зависимым f и другими значениями, которые зависят от некоторых других заданных параметров. К сожалению, я не могу описать конкретную проблему, с которой работаю. Сказав это, реализация предложенного Клаудио решения (к которому я стремился, но забыл, как это сделать - прошло несколько лет с момента моего урока DSA) значительно быстрее, чем решение с мемоизированной рекурсией. У меня еще не было времени проверить предложение Тринкота.
@trincot См. также мой последний ответ на комментарий nocomment. Запоминание решения рекурсии поначалу действительно помогло, но, по крайней мере, в моих тестах даже это немного медленнее, чем предложение Клаудио. У меня еще не было времени проверить ваши предложения, но заранее благодарю вас за помощь!
Что такое max_height в вашем реальном сценарии?
@trincot Надеюсь, как можно больше: запуск на моем обычном ноутбуке max_heightприблизительно 16-18 занимает в конкретном приложении около одной минуты с рекурсивным решением, в то время как итеративный подход занимает чуть больше одной секунды для max_height == 100.
Кстати, я определил простой f, например 1 + r + l*2, украсил f и recursive_travel@cache , и ваша рекурсивная функция почти сразу реагирует параметром 30. Поэтому я не могу воспроизвести проблему эффективности, с которой вы столкнулись.
Похоже, вы неправильно сделали мемоизацию. Не можем сказать/помочь, так как мы этого не видим. Не нужно описывать конкретную проблему, просто придумайте что-то сопоставимое.






Вы можете передавать значения, используя словарь, в котором хранится параметр вызова рекурсивной функции. См. комментарии в приведенном ниже коде, которые, надеюсь, ответят на заданные вами вопросы:
def recursive_travel(l, r, cur_height, max_height):
def f(l,r):
return l+r
if cur_height == max_height:
return f(l, r) * (f(l+1, r) + f(l, r+1))
return recursive_travel(l+1, r, cur_height+1, max_height) + recursive_travel(l, r+1, cur_height+1, max_height)
def stack_travel(l, r, min_height, max_height):
# Define the function f
def f(l, r):
return l + r
# Stack to hold states to process
stack = [(l, r, min_height)]
results = {} # Dictionary to store the results of subproblems
# Process until the stack is empty
while stack:
cur_l, cur_r, cur_height = stack.pop()
# Calculate base case directly
if cur_height == max_height:
if (cur_l, cur_r, cur_height) not in results:
results[(cur_l, cur_r, cur_height)] = f(cur_l, cur_r) * (f(cur_l + 1, cur_r) + f(cur_l, cur_r + 1))
else:
# Check if children results are ready
left_key = (cur_l + 1, cur_r, cur_height + 1)
right_key = (cur_l, cur_r + 1, cur_height + 1)
# Calculate if children are already processed
if left_key in results and right_key in results:
results[(cur_l, cur_r, cur_height)] = results[left_key] + results[right_key]
else:
# Re-add current node to process later
stack.append((cur_l, cur_r, cur_height))
# Ensure children are in the stack to be processed
if left_key not in results:
stack.append(left_key)
if right_key not in results:
stack.append(right_key)
# Return the result for the initial call
return results[(l, r, min_height)]
# Test the function
print(recursive_travel(1, 2, 3, 4))
print(stack_travel(1, 2, 3, 4))
print(recursive_travel(5, 6, 7, 8))
print(stack_travel(5, 6, 7, 8))
Что дает в качестве вывода:
80
80
624
624
предполагая, что версия стека работает должным образом.
ОБНОВЛЕНИЕ с учетом измененной рекурсивной функции:
def recursive_travel(l, r, cur_height, max_height):
def f(l, r):
return l*r*(l + r)
if cur_height == max_height - 1:
return f(l, r) * (f(l+1, r) + f(l, r+1))
return f(l,r)* (recursive_travel(l+1, r, cur_height+1, max_height) + recursive_travel(l, r+1, cur_height+1, max_height))
def stack_travel(l, r, min_height, max_height):
# Define the function f
def f(l, r):
return l*r*(l + r)
# Stack to hold states to process
stack = [(l, r, min_height)]
results = {} # Dictionary to store the results of subproblems
# Process until the stack is empty
while stack:
cur_l, cur_r, cur_height = stack.pop()
# Calculate base case directly when cur_height is max_height - 1
if cur_height == max_height - 1:
if (cur_l, cur_r, cur_height) not in results:
results[(cur_l, cur_r, cur_height)] = f(cur_l, cur_r) * (f(cur_l + 1, cur_r) + f(cur_l, cur_r + 1))
else:
# Check if children results are ready
left_key = (cur_l + 1, cur_r, cur_height + 1)
right_key = (cur_l, cur_r + 1, cur_height + 1)
# Calculate if children are already processed
if left_key in results and right_key in results:
# Multiply the sum of children results by f(cur_l, cur_r)
results[(cur_l, cur_r, cur_height)] = f(cur_l, cur_r) * (results[left_key] + results[right_key])
else:
# Re-add current node to process later
stack.append((cur_l, cur_r, cur_height))
# Ensure children are in the stack to be processed
if left_key not in results:
stack.append(left_key)
if right_key not in results:
stack.append(right_key)
# Return the result for the initial call
return results[(l, r, min_height)]
Для полноты картины ниже функция, предоставляемая тринкотом, адаптирована для приема начального параметра l, r, min_height, max_heigt:
def get_left_right( l , r , path_len , path):
right = path.bit_count() # Number of times we go right on this path
left = path_len - right # Number of times we go left on this path
return l + left, r + right
def iterative_travel( l , r , min_height , max_height):
def f(l, r):
return l*r*(l + r)
level = [0, 1] * ( 2 << ( max_height - min_height ) )
for depth in range(((max_height - 1) - min_height + 1) , -1, -1):
level = [
f( *get_left_right(l, r, depth, path) ) * (level[path*2] + level[path*2+1])
for path in range(1 << depth)
]
#print(level)
return level[0]
Мне очень жаль, но мой первоначальный рекурсивный код был неполным: в нем отсутствовал ключевой момент f(l,r) * (sum of the two recursion branches). Это немного меняет проблему (по крайней мере, на мой наивный взгляд), поэтому я спрашиваю, не могли бы вы помочь мне с исправленным кодом. Программирование без сна — это явно нехорошо!
Задача вашей рекурсивной функции — вычислить значение для каждого листа виртуального идеального двоичного дерева, а затем всплыть так, что родительский узел получит сумму двух дочерних узлов, умноженную на его собственное значение.
База рекурсии по-прежнему учитывает один дополнительный уровень виртуального дерева, поскольку вызывает f с увеличенным аргументом. Поэтому я бы переписал рекурсивную функцию, используя базовый вариант на один уровень глубже:
def recursive_travel(l, r, cur_height, max_height):
if cur_height == max_height + 1:
return f(l, r)
return f(l, r) * (recursive_travel(l + 1, r, cur_height + 1, max_height)
+ recursive_travel(l, r + 1, cur_height + 1, max_height))
Это лучше показывает, как каждый узел вносит свой «собственный» вклад (определяемый f). Это означает, что виртуальное двоичное дерево на самом деле находится на один уровень выше, чем max_height.
Другое наблюдение заключается в том, что l и r обозначают количество левых и правых поворотов на пути от корня к узлу. Это означает, что поддерево, имеющее те же номера l и r, что и другое поддерево, получит точно такое же значение. Было бы напрасной тратой пересчитывать все это поддерево более одного раза.
Таким образом, ваше дерево на самом деле представляет собой скорее сеть, которая выглядит следующим образом:
/\
//\
///\
////\
/////\
//////\
... ... ... ...
Это уменьшает количество узлов, которые вам фактически нужно оценить, делая количество вычислений O(𝑛²) вместо O(2𝑛).
Мы могли бы превратить это в итеративное решение восходящим способом: сначала вычислить результат для всех уникальных комбинаций l-r внизу, затем для всех родительских комбинаций... до тех пор, пока не будет определено значение корня, которое также является конечный результат.
Объединив все эти наблюдения, мы получаем этот код:
def iterative_travel(max_height):
level = [
f(left, max_height + 1 - left)
for left in range(max_height + 2)
]
for height in range(max_height, -1, -1):
for left in range(height + 1):
level[left] = f(left, height - left) * (level[left] + level[left + 1])
return level[0]
С вашим рекурсивным подходом не должно быть слишком большой разницы при условии, что вы применяете мемоизацию. Это можно сделать, например, с помощью functools.cache.
Например, я придумал простой f и запустил максимальную высоту 100:
from functools import cache
def f(l, r):
return 2 + l * 3 + r * 5 # Some example calculation
@cache
def recursive_travel(l, r, cur_height, max_height):
if cur_height == max_height + 1:
return f(l, r)
return f(l, r) * (recursive_travel(l + 1, r, cur_height + 1, max_height)
+ recursive_travel(l, r + 1, cur_height + 1, max_height))
def iterative_travel(max_height):
level = [
f(left, max_height + 1 - left)
for left in range(max_height + 2)
]
for height in range(max_height, -1, -1):
for left in range(height + 1):
level[left] = f(left, height - left) * (level[left] + level[left + 1])
return level[0]
n = 100
result = recursive_travel(0, 0, 0, n)
print(result)
result2 = iterative_travel(n)
print(result2)
Этот скрипт печатает результат за долю секунды для обоих подходов. Для больших высот, например 300, итеративная версия оказывается быстрее, но обе по-прежнему завершаются менее чем за 1 секунду.
Мне очень жаль, но мой первоначальный рекурсивный код был неполным: в нем отсутствовал ключевой момент f(l,r) * (sum of the two recursion branches). Это немного меняет проблему (по крайней мере, на мой наивный взгляд), поэтому я спрашиваю, не могли бы вы помочь мне с исправленным кодом. Программирование без сна — это явно нехорошо!
Конечно, я рассмотрю это. Можете ли вы просто дважды проверить, прежде чем я это сделаю, чтобы ваш вопрос теперь точно отражал то, что вы делаете?
Да, я уверен, что теперь рекурсивный код — это то, что мне нужно (я также исправил условие if, поскольку мы должны были остановиться на узле, предшествующем листьям, но на самом деле это мало что меняет). Спасибо!
Обновлено в соответствии с обновленным вопросом.
Я полностью переписал ответ после ваших разъяснений.
Не O(n), а O(n^2). Я также попробовал мемоизацию без functools и с некоторой оптимизацией, кажется, немного быстрее. Мне больше всего нравится минимизация кэша: сохранение только тех результатов, которые понадобятся снова, и извлечение каждого кэшированного результата при его использовании.
Да O(n^2). Исправленный.
В качестве промежуточного шага вы можете изменить свою рекурсивную функцию, чтобы она возвращала только одну переменную и выполняла вычисления в отдельных строках выше. Это должно помочь отделить рекурсивное поведение от вычислений и вызовов функций и сделать рефакторинг более понятным.