Как передать значения при преобразовании рекурсивной функции в итеративную со стеком и циклом while

Обновлено: Мой первоначальный рекурсивный код был неполным, так как я не умножал на 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

В качестве промежуточного шага вы можете изменить свою рекурсивную функцию, чтобы она возвращала только одну переменную и выполняла вычисления в отдельных строках выше. Это должно помочь отделить рекурсивное поведение от вычислений и вызовов функций и сделать рефакторинг более понятным.

101 30.04.2024 00:16

Какая у тебя причина это делать? Зачем усложнять, если у вас уже есть что-то простое и естественное?

no comment 30.04.2024 00:54

Не могли бы вы предоставить минимальный пример использования для проверки кода, предоставленного в ответе, на соответствие исходной рекурсивной версии?

oOosys 30.04.2024 03:32

Я предлагаю вам прочитать reddit.com/r/compsci/comments/5mpozl/…, чтобы увидеть, как классифицируются узлы бинарного дерева и что такое листовые узлы (чтобы понять мое редактирование вашего вопроса).

oOosys 30.04.2024 05:53

Вы говорите о двоичном дереве, но я не вижу в вашем коде узлов или другой древовидной структуры данных, только целые числа. Можете ли вы уточнить?

trincot 30.04.2024 07:03

Можете ли вы ответить на мой вопрос? В одном комментарии вы говорите «для быстрого доступа», и сложность вашего выполнения ужасна, поэтому, если вы пытаетесь сделать это ради скорости, вы пытаетесь пойти неправильным путем, и есть гораздо лучшее решение.

no comment 30.04.2024 12:00

@nocomment Извините, что отвечаю поздно: рекурсивный подход работает хорошо, но время его выполнения даже с мемоизацией довольно плохое в приложении, с которым я работаю. По этой причине я пытаюсь выжать из кода как можно больше эффективности, устраняя ненужные вызовы рекурсии. Я знаю, что я мог бы также переключиться на другой язык, например, Rust или C++, но даже там устранение рекурсии не снизит производительность.

Epsilon Away 30.04.2024 12:43

@trincot Забудьте мой предыдущий комментарий, он был неверным: пара (l, r) левого и правого поворотов сверху не характеризует однозначно узел в данном бинарном дереве, поскольку априори мы не указываем, в каком порядке мы выполняем повороты. Однако 1.) функция f и в этом случае, и в моем конкретном приложении зависит только от общего количества левых и правых поворотов, 2.) Меня волнует только общая сумма-произведение формы f(l, r) * (f(l + 1, r) + f(l, r+1)), поэтому по этой причине никакой дополнительной структуры данных не требуется (по крайней мере, на мой наивный взгляд).

Epsilon Away 30.04.2024 12:46

Хорошо, я так и думал. Вот только мемоизация должна очень помочь. И я не думаю, что вы сделаете это быстрее, моделируя рекурсию в коде Python с помощью собственного стека вместо использования обычной рекурсии, встроенной в Python. Обычно это делает процесс не только более сложным, но и медленным. Я думаю, вам лучше спросить о запоминаемом рекурсивном решении и предоставить информацию о времени выполнения и max_height, чтобы мы действительно могли сделать что-то продуктивное. Похоже на проблему XY.

no comment 30.04.2024 12:52

Спасибо за разъяснение, @Epsiloon. Это соответствует тому, что я понял, когда писал ответ. Было бы хорошо объяснить в своем вопросе, почему вы хотите перейти на итеративную версию кода. Просто ради вызова? Из соображений экономии времени? Из соображений эффективности памяти? В последних случаях, каковы входные данные и определение f, из-за которых у вас возникают проблемы?

trincot 30.04.2024 12:55

Позвольте мне перефразировать: мемоизация должна сделать его оптимальным (если только ваш настоящий f не обладает каким-то свойством, позволяющим что-то делать еще быстрее). И нет смысла пытаться повторить немемоизированную рекурсию с помощью итеративной версии. Чтобы решить вашу реальную проблему, вам нужно спросить о вашей реальной проблеме. Имея достаточно информации, чтобы понять/воспроизвести это.

no comment 30.04.2024 13:13

@nocomment Сначала я попробовал мемоизацию, и это очень помогло. Точная проблема, над которой я работаю, использует этот алгоритм с одним контекстно-зависимым f и другими значениями, которые зависят от некоторых других заданных параметров. К сожалению, я не могу описать конкретную проблему, с которой работаю. Сказав это, реализация предложенного Клаудио решения (к которому я стремился, но забыл, как это сделать - прошло несколько лет с момента моего урока DSA) значительно быстрее, чем решение с мемоизированной рекурсией. У меня еще не было времени проверить предложение Тринкота.

Epsilon Away 30.04.2024 13:26

@trincot См. также мой последний ответ на комментарий nocomment. Запоминание решения рекурсии поначалу действительно помогло, но, по крайней мере, в моих тестах даже это немного медленнее, чем предложение Клаудио. У меня еще не было времени проверить ваши предложения, но заранее благодарю вас за помощь!

Epsilon Away 30.04.2024 13:30

Что такое max_height в вашем реальном сценарии?

trincot 30.04.2024 13:36

@trincot Надеюсь, как можно больше: запуск на моем обычном ноутбуке max_heightприблизительно 16-18 занимает в конкретном приложении около одной минуты с рекурсивным решением, в то время как итеративный подход занимает чуть больше одной секунды для max_height == 100.

Epsilon Away 30.04.2024 13:39

Кстати, я определил простой f, например 1 + r + l*2, украсил f и recursive_travel@cache , и ваша рекурсивная функция почти сразу реагирует параметром 30. Поэтому я не могу воспроизвести проблему эффективности, с которой вы столкнулись.

trincot 30.04.2024 13:57

Похоже, вы неправильно сделали мемоизацию. Не можем сказать/помочь, так как мы этого не видим. Не нужно описывать конкретную проблему, просто придумайте что-то сопоставимое.

no comment 30.04.2024 13:58
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
17
99
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы можете передавать значения, используя словарь, в котором хранится параметр вызова рекурсивной функции. См. комментарии в приведенном ниже коде, которые, надеюсь, ответят на заданные вами вопросы:

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). Это немного меняет проблему (по крайней мере, на мой наивный взгляд), поэтому я спрашиваю, не могли бы вы помочь мне с исправленным кодом. Программирование без сна — это явно нехорошо!

Epsilon Away 30.04.2024 09:56
Ответ принят как подходящий

Задача вашей рекурсивной функции — вычислить значение для каждого листа виртуального идеального двоичного дерева, а затем всплыть так, что родительский узел получит сумму двух дочерних узлов, умноженную на его собственное значение.

База рекурсии по-прежнему учитывает один дополнительный уровень виртуального дерева, поскольку вызывает 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). Это немного меняет проблему (по крайней мере, на мой наивный взгляд), поэтому я спрашиваю, не могли бы вы помочь мне с исправленным кодом. Программирование без сна — это явно нехорошо!

Epsilon Away 30.04.2024 09:55

Конечно, я рассмотрю это. Можете ли вы просто дважды проверить, прежде чем я это сделаю, чтобы ваш вопрос теперь точно отражал то, что вы делаете?

trincot 30.04.2024 09:56

Да, я уверен, что теперь рекурсивный код — это то, что мне нужно (я также исправил условие if, поскольку мы должны были остановиться на узле, предшествующем листьям, но на самом деле это мало что меняет). Спасибо!

Epsilon Away 30.04.2024 10:00

Обновлено в соответствии с обновленным вопросом.

trincot 30.04.2024 10:37

Я полностью переписал ответ после ваших разъяснений.

trincot 30.04.2024 15:25

Не O(n), а O(n^2). Я также попробовал мемоизацию без functools и с некоторой оптимизацией, кажется, немного быстрее. Мне больше всего нравится минимизация кэша: сохранение только тех результатов, которые понадобятся снова, и извлечение каждого кэшированного результата при его использовании.

no comment 30.04.2024 16:24

Да O(n^2). Исправленный.

trincot 30.04.2024 17:59

Другие вопросы по теме

Похожие вопросы

Создайте манекен для отсутствующих значений переменной в Python
Многопроцессорная обработка Python: AttributeError: невозможно получить атрибут <имя_функции> в модуле
Интеграция RFE с GBM для выбора функций и настройки гиперпараметров
Выбор параметра в зависимости от другого выбранного родительского параметра на той же странице
Как я могу улучшить RMSE при оценке цены моего автомобиля?
Лучший/самый чистый способ структурировать проверку ошибок в динамически выделяемых переменных
Как объединить/объединить две строки после того, как длинные столбцы были неправильно прочитаны в отдельные строки?
Деконструкция кортежа при использовании сортировки с лямбда-функцией по ключу
Поиск лиц с помощью Skimage.measure.marching_cubes возвращает странное смещение, основанное на ограничениях сетки
Вставка с помощью cx_Oracle внезапно занимает вечность, но в sql Developer все в обычное время