Преобразование нормальной рекурсии в хвостовую рекурсию
Мне было интересно, есть ли какой-нибудь общий метод для преобразования "нормальной" рекурсии с foo(...) + foo(...)
в качестве последнего вызова хвостовой рекурсии.
Например (scala):
def pascal(c: Int, r: Int): Int = {
if (c == 0 || c == r) 1
else pascal(c - 1, r - 1) + pascal(c, r - 1)
}
Общее решение для функциональных языков для преобразования рекурсивной функции в эквивалент хвостового вызова:
Простым способом является обертка не хвостовой рекурсивной функции в монаде Trampoline
.
def pascalM(c: Int, r: Int): Trampoline[Int] = {
if (c == 0 || c == r) Trampoline.done(1)
else for {
a <- Trampoline.suspend(pascal(c - 1, r - 1))
b <- Trampoline.suspend(pascal(c, r - 1))
} yield a + b
}
val pascal = pascalM(10, 5).run
Таким образом, функция pascal больше не является рекурсивной функцией. Тем не менее, монада Trampoline - это вложенная структура вычислений, которая должна быть выполнена. Наконец, run
является хвостовой рекурсивной функцией, которая проходит через древовидную структуру, интерпретирует ее и, наконец, в базовом случае возвращает значение.
Статья Рунара Бьянарсона по теме батут: без стека Scala со свободными монадами
Ответы
Ответ 1
В случаях, когда есть простая модификация значения рекурсивного вызова, эту операцию можно перенести в начало рекурсивной функции. Классическим примером этого является рекурсия хвоста по модулю минусов, где простая рекурсивная функция в этой форме:
def recur[A](...):List[A] = {
...
x :: recur(...)
}
который не является хвостовым рекурсивным, преобразуется в
def recur[A]{...): List[A] = {
def consRecur(..., consA: A): List[A] = {
consA :: ...
...
consrecur(..., ...)
}
...
consrecur(...,...)
}
Пример Alexlv - это вариант этого.
Это такая хорошо известная ситуация, что некоторые компиляторы (я знаю примеры Prolog и Scheme, но Scalac этого не делает) могут обнаруживать простые случаи и автоматически выполнять эту оптимизацию.
Проблемы, связанные с несколькими вызовами с рекурсивными функциями, не имеют такого простого решения. Оптимизатор TMRC бесполезен, так как вы просто перемещаете первый рекурсивный вызов на другую позицию без хвоста. Единственный способ достичь рекурсивного решения - удалить все, кроме одного из рекурсивных вызовов; как это сделать полностью зависит от контекста, но требует поиска совершенно другого подхода к решению проблемы.
Как это бывает, в некотором роде ваш пример похож на классическую проблему последовательности Фибонаси; в этом случае наивное, но элегантное двухрекурсивное решение может быть заменено на одно, которое переместится вперед от 0-го числа.
def fib (n: Long): Long = n match {
case 0 | 1 => n
case _ => fib( n - 2) + fib( n - 1 )
}
def fib (n: Long): Long = {
def loop(current: Long, next: => Long, iteration: Long): Long = {
if (n == iteration)
current
else
loop(next, current + next, iteration + 1)
}
loop(0, 1, 0)
}
Для последовательности Fibonnaci это наиболее эффективный подход (решение на основе потоков - это просто другое выражение этого решения, которое может кэшировать результаты для последующих вызовов). Теперь,
вы также можете решить свою проблему, перейдя по ссылке c0/r0 (ну, c0/r2) и вычислив каждую строку в последовательности - разница в том, что вам нужно кэшировать всю предыдущую строку. Таким образом, хотя это имеет сходство с fib, оно резко отличается по специфике и также значительно менее эффективно, чем ваше оригинальное, дважды рекурсивное решение.
Здесь приведен пример вашего примера треугольника pascal, который может эффективно вычислять pascal(30,60)
:
def pascal(column: Long, row: Long):Long = {
type Point = (Long, Long)
type Points = List[Point]
type Triangle = Map[Point,Long]
def above(p: Point) = (p._1, p._2 - 1)
def aboveLeft(p: Point) = (p._1 - 1, p._2 - 1)
def find(ps: Points, t: Triangle): Long = ps match {
// Found the ultimate goal
case (p :: Nil) if t contains p => t(p)
// Found an intermediate point: pop the stack and carry on
case (p :: rest) if t contains p => find(rest, t)
// Hit a triangle edge, add it to the triangle
case ((c, r) :: _) if (c == 0) || (c == r) => find(ps, t + ((c,r) -> 1))
// Triangle contains (c - 1, r - 1)...
case (p :: _) if t contains aboveLeft(p) => if (t contains above(p))
// And it contains (c, r - 1)! Add to the triangle
find(ps, t + (p -> (t(aboveLeft(p)) + t(above(p)))))
else
// Does not contain(c, r -1). So find that
find(above(p) :: ps, t)
// If we get here, we don't have (c - 1, r - 1). Find that.
case (p :: _) => find(aboveLeft(p) :: ps, t)
}
require(column >= 0 && row >= 0 && column <= row)
(column, row) match {
case (c, r) if (c == 0) || (c == r) => 1
case p => find(List(p), Map())
}
}
Это эффективно, но я думаю, что это показывает, насколько уродливыми сложными рекурсивными решениями могут стать, когда вы деформируете их, чтобы стать хвостом рекурсивным. На данный момент, возможно, стоит перейти к другой модели целиком. Continuations или монадическая гимнастика может быть лучше.
Вам нужен общий способ преобразования вашей функции. Нет. Есть полезные подходы, что все.
Ответ 2
Я не знаю, насколько теоретичен этот вопрос, но рекурсивная реализация не будет эффективной даже с хвостовой рекурсией. Например, попробуйте вычислить pascal(30, 60)
. Я не думаю, что вы получите переполнение стека, но будьте готовы сделать длинный перерыв на кофе.
Вместо этого рассмотрите возможность использования Stream
или memoization:
val pascal: Stream[Stream[Long]] =
(Stream(1L)
#:: (Stream from 1 map { i =>
// compute row i
(1L
#:: (pascal(i-1) // take the previous row
sliding 2 // and add adjacent values pairwise
collect { case Stream(a,b) => a + b }).toStream
++ Stream(1L))
}))
Ответ 3
Да, это возможно. Обычно это делается с помощью шаблона аккумулятора через некоторую внутренне определенную функцию, которая имеет один дополнительный аргумент с так называемой логикой аккумулятора, пример с подсчетом длины списка.
Например, нормальная рекурсивная версия будет выглядеть так:
def length[A](xs: List[A]): Int = if (xs.isEmpty) 0 else 1 + length(xs.tail)
который не является хвостовой рекурсивной версией, чтобы исключить последнюю операцию добавления, мы должны накапливать значения, пока как-то, например, с шаблоном аккумулятора:
def length[A](xs: List[A]) = {
def inner(ys: List[A], acc: Int): Int = {
if (ys.isEmpty) acc else inner(ys.tail, acc + 1)
}
inner(xs, 0)
}
немного дольше кода, но я думаю, что идея я поняла. Из-за этого вы можете сделать это без внутренней функции, но в таком случае вы должны указать начальное значение acc
вручную.
Ответ 4
Я уверен, что это возможно не, так как вы ищете общий случай, но это будет зависеть от того, насколько тщательно вы разрешаете изменения.
Рекурсивная функция хвоста должна быть перезаписана как цикл while, но попробуйте реализовать, например, фрактальное дерево, используя while-loop, Это возможно, но вам нужно использовать массив или коллекцию для хранения состояния для каждой точки, которая сулит для данных, которые иначе хранятся в стеке вызовов.
Также можно использовать trampolining.
Ответ 5
Это действительно возможно. То, как я это сделаю, - это
начните с списка (1) и продолжайте рекурсию, пока не дойдете до
вы хотите.
Стоит заметить, что вы можете его оптимизировать: если c == 0 или c == r, это значение равно единице, и для вычисления let say столбец 3 из 100-й строки вам нужно всего лишь вычислить первые три элемента предыдущих строк.
Рекурсивное решение для работы хвоста будет следующим:
def pascal(c: Int, r: Int): Int = {
@tailrec
def pascalAcc(c: Int, r: Int, acc: List[Int]): List[Int] = {
if (r == 0) acc
else pascalAcc(c, r - 1,
// from let say 1 3 3 1 builds 0 1 3 3 1 0 , takes only the
// subset that matters (if asking for col c, no cols after c are
// used) and uses sliding to build (0 1) (1 3) (3 3) etc.
(0 +: acc :+ 0).take(c + 2)
.sliding(2, 1).map { x => x.reduce(_ + _) }.toList)
}
if (c == 0 || c == r) 1
else pascalAcc(c, r, List(1))(c)
}
Аннотация @tailrec фактически позволяет компилятору проверить функцию
на самом деле является хвостом рекурсивным.
Вероятно, его можно было бы оптимизировать, поскольку с учетом того, что строки симметричны, если c > r/2, pascal (c, r) == pascal (r-c, r).. но оставлен читателю;)
Ответ 6
Подход аккумулятора
def pascal(c: Int, r: Int): Int = {
def pascalAcc(acc:Int, leftover: List[(Int, Int)]):Int = {
if (leftover.isEmpty) acc
else {
val (c1, r1) = leftover.head
// Edge.
if (c1 == 0 || c1 == r1) pascalAcc(acc + 1, leftover.tail)
// Safe checks.
else if (c1 < 0 || r1 < 0 || c1 > r1) pascalAcc(acc, leftover.tail)
// Add 2 other points to accumulator.
else pascalAcc(acc, (c1 , r1 - 1) :: ((c1 - 1, r1 - 1) :: leftover.tail ))
}
}
pascalAcc(0, List ((c,r) ))
}
Он не переполняет стек, а как на большой строке и столбце, но Аарон упомянул об этом не быстро.