Преобразование нормальной рекурсии в хвостовую рекурсию

мне было интересно, есть ли какой-то общий метод преобразования "нормальной" рекурсии с 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 не является рекурсивной функцией больше. Однако монада батута является вложенной структурой вычисления, которое необходимо выполнить. Наконец,run хвост-рекурсивной функции, которая проходит через древовидную структуру, интерпретируя ее, и, наконец, в базовом случае возвращает значение.

статья Рунара Бьянарсона на тему батутов:Stackless Scala Со Свободными Монадами

6 ответов


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

def recur[A](...):List[A] = {
  ...
  x :: recur(...)
}

это не хвост рекурсивной, превращается в

def recur[A]{...): List[A] = {
   def consRecur(..., consA: A): List[A] = {
     consA :: ...
     ...
     consrecur(..., ...)
   }
   ...
   consrecur(...,...)
}
Alexlv вариант это.

Это такая известная ситуация, что некоторые компиляторы (Я знаю примеры пролога и схемы, но Scalac этого не делает) может обнаруживать простые случаи и выполнять эту оптимизацию автоматически.

проблемы, объединяющие несколько вызовов рекурсивных функций, не имеют такого простого решения. Tmrc optimisatin бесполезен, так как вы просто перемещаете первый рекурсивный вызов в другую позицию без хвоста. Единственный способ достичь хвостового рекурсивного решения-удалить все, кроме одного из рекурсивных вызовов; как это сделать, полностью зависит от контекста, но требуется найти совершенно другой подход к решению проблемы.

как это происходит, в некотором роде ваш пример похож на классическую проблему последовательности Фибоннаси; в этом случае наивное, но элегантное дважды рекурсивное решение может быть заменено тем, которое петляет вперед от 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) и вычисляя каждую строку последовательно - разница в том, что вам нужно кэшировать всю предыдущую строку. Так что пока это имеет сходство с приврал, он резко отличается спецификой и также значительно менее эффективен, чем ваше оригинальное, дважды рекурсивное решение.

вот подход для вашего примера треугольника Паскаля, который можете вычислить 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())
  }
}

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

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


Я не знаю, как теоретической этот вопрос, но рекурсивная реализация не будет эффективной даже с хвостовой рекурсией. Попробуйте вычислить 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))
    }))

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

например, обычная рекурсивная версия будет выглядеть так:

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 начальное значение вручную.


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

хвостовая рекурсивная функция должна быть перезаписана как цикл while, но попробуйте реализовать, например,Фрактальное Дерево использование while-loops. Это possble, но вам нужно использовать массив или коллекцию для хранения состояния для каждой точки, которая susbstitutes для данных, иначе хранящихся в стек вызовов.

можно использовать вода.


подход аккумулятора

  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) ))
  }

он не переполняет стек, но как на большой строке и столбце, но Аарон упомянул, что это не быстро.


Это действительно возможно. Я бы сделал это так: начните с List (1) и продолжайте рекурсию, пока не доберетесь до гребите, как хотите. Стоит отметить, что вы можете оптимизировать его: если c==0 или c==r значение равно единице, и для вычисления, скажем, столбца 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's 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).. но оставлено читателю;)