Хвост-рекурсивный треугольник Паскаля на схеме

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

Для «одномерных» ситуаций (линейных), таких как ряд Фибоначчи или вычисление факториала, выполнить преобразование несложно.

Например, как говорится в книге, мы можем переписать вычисление Фибоначчи следующим образом.

(define (fib n)
    (fib-iter 1 0 n))
(define (fib-iter a b count)
    (if (= count 0) 
        b 
        (fib-iter (+ a b) a (- count 1))))

И эта форма, очевидно, хвостовая рекурсия

Однако для «двумерной» ситуации, такой как вычисление треугольника Паскаля (пример 1.12 в SICP), мы все еще можем легко написать рекурсивное решение следующим образом.

(define (pascal x y) 
  (cond ((or (<= x 0) (<= y 0) (< x y )) 0)
        ((or (= 1 y) (= x y) ) 1)
        (else (+ (pascal (- x 1) y) (pascal (- x 1) (- y 1))))))

Вопрос в том, как преобразовать это в хвостовую рекурсивную форму?


person Junjie    schedule 02.08.2014    source источник
comment
Ваши закрывающие скобки раздражают людей. Пожалуйста, соберите их все в одну строку, чтобы кто-то начал отвечать ;)   -  person Phil    schedule 02.08.2014
comment
@Phil, я просто подумал, что выровнять скобки в одном столбце могут сделать код более понятным, поскольку объединение двух закрывающих скобок кажется немного сложным для их сопоставления.   -  person Junjie    schedule 02.08.2014
comment
Я предлагаю вам получить редактор, который сделает сопоставление для вас. Один специально для схемы, такой как DrRacket, также показывает, правильно идентифицируя ваш код.   -  person Sylwester    schedule 02.08.2014
comment
Вы всегда можете быть очень ленивыми и использовать тот факт, что любой элемент треугольника Паскаля задается строкой!/(столбец!(строка - столбец)!). Хотя это не ответ на ваш вопрос.   -  person Brendan Wilson    schedule 03.08.2014


Ответы (3)


Прежде всего, процедура рекурсивного процесса pascal может быть выражена более простым способом (предполагая неотрицательные, допустимые входные данные) - например, так:

(define (pascal x y) 
  (if (or (zero? y) (= x y))
      1
      (+ (pascal (sub1 x) y)
         (pascal (sub1 x) (sub1 y)))))

Теперь вопрос. Возможно преобразовать реализацию рекурсивного процесса в версию итеративного процесса, использующую хвостовую рекурсию. Но это сложнее, чем кажется, и чтобы полностью понять это, вы должны понять, как работает динамическое программирование. Подробное объяснение этого алгоритма см. в Руководстве по разработке алгоритмов Стивена Скиены, 2-е издание, стр. 278.

Это тот тип алгоритма, который не подходит для идиоматического решения в Scheme, поскольку требует, чтобы мы изменяли состояние как часть решения (в данном случае мы обновляем частичные результаты в векторе). Это довольно надуманное решение, и я оптимизировал использование памяти таблицы, так что за раз требуется только одна строка - и вот оно:

(define (pascal x y)
  (let ([table (make-vector (add1 x) 1)])
    (let outer ([i 1])
      (when (<= i x)
        (let inner ([j 1] [previous 1])
          (when (< j i)
            (let ([current (vector-ref table j)])
              (vector-set! table j (+ current previous))
              (inner (add1 j) current))))
        (outer (add1 i))))
    (vector-ref table y)))

На самом деле, в этом случае было бы естественнее написать прямую итерацию, попутно мутируя переменные. В Racket это выглядит так:

(define (pascal x y)
  (define current null)
  (define previous null)
  (define table (make-vector (add1 x) 1))
  (for ([i (in-range 1 (add1 x))])
    (set! previous 1)
    (for ([j (in-range 1 i)])
      (set! current (vector-ref table j))
      (vector-set! table j (+ (vector-ref table j) previous))
      (set! previous current)))
  (vector-ref table y))

Мы можем распечатать результаты и проверить, что все три показанные реализации работают. Опять же, в Racket:

(define (pascal-triangle n)
  (for ([x (in-range 0 n)])
    (for ([y (in-range 0 (add1 x))])
      (printf "~a " (pascal x y)))
    (newline)))

(pascal-triangle 5)

1 
1 1 
1 2 1 
1 3 3 1 
1 4 6 4 1 
person Óscar López    schedule 03.08.2014
comment
Это довольно просто изменить на идиоматическое решение схемы. Вам просто нужно остановиться на запрошенном номере, а не заполнять всю строку. Смотрите мой ответ. - person Sylwester; 05.08.2014
comment
@Sylwester Да, я изучил ваше решение. Мне нравится, что это в функциональном стиле (следовательно, более идиоматично), но мне также нравится тот факт, что мой собственный не создает промежуточных списков: P - person Óscar López; 05.08.2014

ОБНОВЛЕНИЕ: у этой задачи есть гораздо более простое математическое решение, которое можно перейти к O(row ), используя только факториал. Исходя из этого, это сводится к:

(define (pascal-on row col)
  (define (factorial from to acc)
    (if (> from to)
        acc
        (factorial (+ 1 from) to (* acc from))))

  (let* ((rmc (- row col))
         (fac-rmc (factorial 1 rmc 1))
         (fac-pos (factorial (+ rmc 1) col fac-rmc))
         (fac-row (factorial (+ col 1) row fac-pos)))
    (/ fac-row fac-pos fac-rmc)))

Старый ответ:

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

(define (pascal row col)
  (define (aux tr tc prev acc)
    (cond ((> tr row) #f)              ; invalid input

          ((and (= col tc) (= row tr)) ; the next number is the answer
           (+ (car prev) (cadr prev))) 

          ((= tc tr)                   ; new row 
           (aux (+ tr 1) 1 (cons 1 acc) '(1)))

          (else (aux tr               ; new column
                     (+ tc 1) 
                     (cdr prev)
                     (cons (+ (car prev) (cadr prev)) acc))))) 

  (if (or (zero? col) (= col row))
      1
      (aux 2 1 '(1 1) '(1))))
person Sylwester    schedule 03.08.2014

Чтобы добавить к ответ Оскара, мы можем использовать стиль передачи продолжения, чтобы преобразовать любую программу для использования хвостовых вызовов:

;; Int Int (Int → Int) → Int
(define (pascal/k x y k)
  (cond
   [(or (<= x 0) (<= y 0) (< x y)) (k 0)]
   [(or (= 1 y) (= x y)) (k 1)]
   [else (pascal/k (- x 1) y
                   (λ (a)
                     (pascal/k (- x 1) (- y 1)
                               (λ (b) (k (+ a b))))))]))

;; Int Int → Int
(define (pascal x y) (pascal/k x y (λ (x) x)))

Вы можете сказать, что эта программа не столь удовлетворительна, так как есть замыкание, которое «растет». Но они выделены в куче. В общем случае смысл хвостового вызова не столько в производительности, сколько в безопасности пространства: вы не разрушаете контекст оценки.

person Phil    schedule 03.08.2014