Konwertuj rekurencję normalną na rekurencję ogonową

Zastanawiałem się, czy istnieje jakaś ogólna metoda konwersji "normalnej" rekurencji z foo(...) + foo(...) jako ostatnim wywołaniem na rekurencję ogonową.

Na przykład (scala):

def pascal(c: Int, r: Int): Int = {
 if (c == 0 || c == r) 1
 else pascal(c - 1, r - 1) + pascal(c, r - 1)
}

Ogólne rozwiązanie dla języków funkcyjnych do konwersji funkcji rekurencyjnej NA odpowiednik wywołania ogonowego:

Prostym sposobem jest zawinięcie funkcji nie-rekurencyjnej w monadzie 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
Zatem funkcja Pascala nie jest już funkcją rekurencyjną. Jednak trampolina monad jest zagnieżdżona struktura obliczeń, które należy wykonać. Wreszcie, run jest funkcją rekurencyjną ogona, która przechodzi przez strukturę podobną do drzewa, interpretując ją i wreszcie w przypadku podstawowym Zwraca wartość. W 2007 roku, po raz pierwszy w historii trampoliny pojawiły się w Polsce.]}
Author: DennisVDB, 2013-09-22

6 answers

W przypadkach, gdy istnieje prosta modyfikacja wartości wywołania rekurencyjnego, operacja ta może być przeniesiona na przód funkcji rekurencyjnej. Klasycznym tego przykładem jest rekurencja ogonowa modulo cons , gdzie prosta funkcja rekurencyjna w tej postaci:

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

Który nie jest rekurencyjny, przekształca się w

def recur[A]{...): List[A] = {
   def consRecur(..., consA: A): List[A] = {
     consA :: ...
     ...
     consrecur(..., ...)
   }
   ...
   consrecur(...,...)
}

Przykład Alexlv jest wariantem tego.

Jest to tak dobrze znana sytuacja, że niektóre Kompilatory (znam Prolog i Scheme przykłady, ale Scalac tego nie robi) może wykryć proste przypadki i wykonać tę optymalizację automatycznie.

Problemy łączące wiele wywołań funkcji rekurencyjnych nie mają tak prostego rozwiązania. TMRC optimisatin jest bezużyteczny, ponieważ po prostu przenosisz pierwsze wywołanie rekurencyjne na inną pozycję bez ogona. Jedynym sposobem osiągnięcia rekurencyjnego rozwiązania jest usunięcie wszystkich wywołań rekurencyjnych z wyjątkiem jednego; jak to zrobić jest całkowicie zależne od kontekstu, ale wymaga znalezienia zupełnie innego rozwiązania podejście do rozwiązania problemu.

Tak się składa, że pod pewnymi względami twój przykład jest podobny do klasycznego problemu sekwencji Fibonnaciego; w takim przypadku naiwne, ale eleganckie rozwiązanie podwójnie rekurencyjne może zostać zastąpione przez takie, które pętli do przodu od 0-tej liczby.

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

Dla ciągu Fibonnaciego jest to najbardziej efektywne podejście(rozwiązanie oparte na strumieniach jest tylko innym wyrażeniem tego rozwiązania, które może buforować wyniki dla kolejnych wywołań). Teraz, możesz również rozwiązać swój problem poprzez zapętlenie do przodu od c0 /r0 (dobrze, c0 / r2) i obliczanie każdego wiersza w kolejności - różnica polega na tym, że musisz buforować cały poprzedni wiersz. Tak więc, chociaż ma to podobieństwo do fib , różni się dramatycznie w specyfice i jest również znacznie mniej wydajne niż oryginalne, podwójnie rekurencyjne rozwiązanie.

Oto podejście do twojego przykładu trójkąta Pascala, który Może obliczyćpascal(30,60) efektywnie:

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())
  }
}
Jest wydajny, ale myślę, że pokazuje, jak brzydkie złożone rozwiązania rekurencyjne mogą stać się, gdy je deformujesz, aby stały się rekurencyjne. W tym momencie może warto przejść do zupełnie innego modelu. kontynuacje lub gimnastyka monadyczna może być lepsza.

Chcesz ogólnego sposobu na przekształcenie swojej funkcji. Nie ma żadnego. Są pomocne podejścia, to wszystko.

 23
Author: itsbruce,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-24 22:21:11

Nie wiem jak teoretyczne jest to pytanie, ale rekurencyjna implementacja nie będzie skuteczna nawet z rekurencją ogonową. Spróbuj na przykład obliczyć pascal(30, 60). Nie wydaje mi się, że dostaniesz przepełnienie stosu, ale przygotuj się na długą przerwę na kawę.

Zamiast tego rozważ użycie Stream lub 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))
    }))
 9
Author: Aaron Novstrup,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:00

Tak, to możliwe. Zwykle odbywa się to za pomocą wzorca akumulatora poprzez jakąś wewnętrznie zdefiniowaną funkcję, która ma jeden dodatkowy argument z tzw. logiką akumulatora, przykład z liczeniem długości listy.

Na przykład normalna rekurencyjna wersja wyglądałaby tak:

def length[A](xs: List[A]): Int = if (xs.isEmpty) 0 else 1 + length(xs.tail)

To nie jest rekurencyjna wersja ogona, aby wyeliminować ostatnią operację dodawania musimy gromadzić wartości, podczas gdy w jakiś sposób, na przykład z wzorem akumulatora:

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

Trochę dłużej kodować, ale myślę, że pomysł Rozumiem. Przyczyny można to zrobić bez wewnętrznej funkcji, ale w takim przypadku należy podać acc wartość początkową ręcznie.

 3
Author: 4lex1v,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-22 16:19:19

Jestem prawie pewien, że jest to nie możliwe w prosty sposób, w jaki szukasz ogólnej sprawy, ale zależy to od tego, jak rozbudowane pozwolisz na zmiany.

Funkcja rekurencyjna ogona musi być ponownie zapisywalna jako pętla while, ale spróbuj zaimplementować na przykład fraktalne drzewo używając pętli while. Jest możliwe, ale trzeba użyć tablicy lub kolekcji, aby zapisać stan dla każdego punktu, który jest przechowywany dla danych przechowywanych w stos wywołań.

Możliwe jest również użycie trampoliningu .

 3
Author: Luigi Plinge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-22 17:03:06

Podejście akumulatorowe

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

Nie przepełnia stosu, ale jak na dużym wierszu i kolumnie, ale Aaron wspomniał, że nie jest szybki.

 3
Author: Alex des Pelagos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-22 00:23:58

To jest rzeczywiście możliwe. / Align = "left" / zacznij od List (1) i kontynuuj rekursowanie aż do chcesz wiosłować. Warto zauważyć, że można go zoptymalizować: Jeśli c = = 0 lub c = = r wartość jest jedna, a aby obliczyć powiedzmy kolumnę 3 z 100 wiersza, nadal wystarczy obliczyć trzy pierwsze elementy poprzednich wierszy. Działającym rozwiązaniem rekurencyjnym ogona byłoby:

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

Adnotacja @tailrec faktycznie sprawia, że kompilator sprawdza funkcję jest rzeczywiście ogon rekurencyjny. Może być prawdopodobnie bardziej zoptymalizowany, ponieważ biorąc pod uwagę, że wiersze są symetryczne, Jeśli C > R/2, pascal ( c,r) == pascal (R-C,R).. ale pozostawione czytelnikowi;)

 2
Author: Roberto Congiu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-24 13:50:00