Подтвердить что ты не робот

Не знаю, как решить упражнение SICP 1.11

Упражнение 1.11:

Функция f определяется по правилу, что f(n) = n если n < 3 и f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) если n > 3. Напишите процедуру, которая вычисляет f посредством рекурсивного процесса. Напишите процедуру, которая вычисляет f с помощью итеративного процесса.

Реализация его рекурсивно достаточно проста. Но я не мог понять, как это сделать итеративно. Я попытался сравнить с приведенным примером Фибоначчи, но я не знал, как использовать его в качестве аналогии. Поэтому я отказался (позор мне) и Гуглед за объяснение, и я нашел это:

(define (f n)
   (if (< n 3)
       n
       (f-iter 2 1 0 n)))

(define (f-iter a b c count)
   (if (< count 3)
       a
       (f-iter (+ a (* 2 b) (* 3 c))
               a
               b
               (- count 1))))

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

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

4b9b3361

Ответ 1

Вам нужно зафиксировать состояние в некоторых аккумуляторах и обновить состояние на каждой итерации.

Если у вас есть опыт в императивном языке, представьте, что вы пишете цикл while и информацию отслеживания в переменных во время каждой итерации цикла. Какие переменные вам нужны? Как бы вы их обновили? Это то, что вам нужно сделать, чтобы сделать итеративный (хвостовой рекурсивный) набор вызовов в Схеме.

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


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

Каждый рекурсивный шаг отслеживает три вещи:

f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) 

Поэтому мне нужно три части состояния для отслеживания текущего, последнего и предпоследнего значений f. (т.е. f(n-1), f(n-2) and f(n-3).) Назовите их a, b, c. Я должен обновить эти фрагменты внутри каждого цикла:

for _ in 2..n:
    a = NEWVALUE
    b = a
    c = b
return a

Итак, что NEWVALUE? Итак, теперь, когда мы имеем представления f(n-1), f(n-2) and f(n-3), это просто рекурсивное уравнение:

for _ in 2..n:
    a = a + 2 * b + 3 * c
    b = a
    c = b
return a

Теперь все, что осталось, - это выяснить начальные значения a, b and c. Но это легко, так как мы знаем, что f(n) = n if n < 3.

if n < 3: return n
a = 2 # f(n-1) where n = 3
b = 1 # f(n-2)
c = 0 # f(n-3)
# now start off counting at 3
for _ in 3..n:
    a = a + 2 * b + 3 * c
    b = a
    c = b
return a

Это все еще немного отличается от итеративной версии Scheme, но я надеюсь, что теперь вы сможете увидеть мыслительный процесс.

Ответ 2

Я думаю, вы спрашиваете, как можно найти алгоритм, естественно, вне "шаблона дизайна".

Мне было полезно посмотреть на разложение f (n) при каждом значении n:

f(0) = 0  |
f(1) = 1  | all known values
f(2) = 2  |

f(3) = f(2) + 2f(1) + 3f(0)
f(4) = f(3) + 2f(2) + 3f(1)
f(5) = f(4) + 2f(3) + 3f(2)
f(6) = f(5) + 2f(4) + 3f(3)

Приближаясь к f (3), мы видим, что мы можем сразу вычислить его из известных значений. Что нам нужно для вычисления f (4)?

Нам нужно, по крайней мере, рассчитать f (3) + [остальное]. Но, как мы вычисляем f (3), мы также вычисляем f (2) и f (1), которые нам понадобятся для вычисления [остальной] функции f (4).

f(3) = f(2) + 2f(1) + 3f(0)
            ↘       ↘
f(4) = f(3) + 2f(2) + 3f(1)

Итак, для любого числа n я могу начать с вычисления f (3) и повторно использовать значения, которые я использую для вычисления f (3), для вычисления f (4)... и шаблон продолжается...

f(3) = f(2) + 2f(1) + 3f(0)
            ↘       ↘
f(4) = f(3) + 2f(2) + 3f(1)
            ↘       ↘
f(5) = f(4) + 2f(3) + 3f(2)

Так как мы будем их повторно использовать, дайте им имя a, b, c. подстроенный с шагом, на котором мы находимся, и проведем вычисление f (5):

  Step 1:    f(3) = f(2) + 2f(1) + 3f(0) or f(3) = a1 + 2b1 +3c1

где

a 1= f (2) = 2,

b 1= f (1) = 1,

c 1= 0

так как f (n) = n для n < 3.

Таким образом:

f (3) = a 1 + 2b 1 + 3c 1= 4

  Step 2:  f(4) = f(3) + 2a1 + 3b1

Итак:

a 2= f (3) = 4 (вычислено выше на шаге 1),

b 2= a 1= f (2) = 2,

c 2= b 1= f (1) = 1

Таким образом:

f (4) = 4 + 2 * 2 + 3 * 1 = 11

  Step 3:  f(5) = f(4) + 2a2 + 3b2

Итак:

a 3= f (4) = 11 (вычислено выше на шаге 2),

b 3= a 2= f (3) = 4,

c 3= b 2= f (2) = 2

Таким образом:

f (5) = 11 + 2 * 4 + 3 * 2 = 25

В течение всего вышеприведенного расчета мы фиксируем состояние в предыдущем вычислении и передаем его на следующий шаг, particularily:

a step= результат шага - 1

b step= a step - 1

c step= b step -1

Как только я увидел это, то итеративная версия была простой.

Ответ 3

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

Здесь вы пытаетесь определить хвост-рекурсивную функцию в Схеме, учитывая (не хвостовое) рекурсивное определение.

Основной случай рекурсии (f (n) = n, если n < 3) обрабатывается обеими функциями. Я не совсем уверен, почему автор делает это; первая функция может быть просто:

(define (f n)
   (f-iter 2 1 0 n))

Общая форма:

(define (f-iter ... n)
   (if (base-case? n)
       base-result
       (f-iter ...)))

Примечание. Я еще не заполнял параметры для f-iter, потому что вам сначала нужно понять, какое состояние нужно передавать из одной итерации в другую.

Теперь посмотрим на зависимости рекурсивной формы f (n). Он ссылается на f (n - 1), f (n - 2) и f (n - 3), поэтому нам нужно поддерживать эти значения. И, конечно, нам нужно значение n, поэтому мы можем остановить его итерацию.

Итак, как вы придумываете хвостовой рекурсивный вызов: мы вычисляем f (n) для использования в качестве f (n - 1), поворачиваем f (n - 1) на f (n - 2) и f (n - 2) до f (n - 3) и счетчика декремента.

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

Ответ 4

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

Проблема с Билл-подход, цитируемый в вашем вопросе, заключается в том, что он не сразу понимает, какой смысл передают переменные состояния, a, b и c. Их имена не передают никакой информации, а статья Билла не описывает никакого инварианта или другого правила, которым они подчиняются. Мне легче сформулировать и понять итеративные алгоритмы, если переменные состояния подчиняются некоторым документальным правилам, описывающим их отношения друг с другом.

С учетом этого рассмотрим эту альтернативную формулировку того же самого алгоритма, который отличается от Билла только наличием более значимых имен переменных для a, b и c и переменной счетчика вместо декрементирующего один:

(define (f n)
    (if (< n 3)
        n
        (f-iter n 2 0 1 2)))

(define (f-iter n 
                i 
                f-of-i-minus-2
                f-of-i-minus-1
                f-of-i)
    (if (= i n)
        f-of-i
        (f-iter n
                (+ i 1)
                f-of-i-minus-1
                f-of-i
                (+ f-of-i
                   (* 2 f-of-i-minus-1)
                   (* 3 f-of-i-minus-2)))))

Вдруг правильность алгоритма - и мыслительный процесс за его созданием - просты и понятны. Чтобы вычислить f(n):

  • У нас есть переменная счетчика i, которая начинается с 2 и поднимается до n, увеличивая на 1 при каждом вызове f-iter.
  • На каждом шаге по пути мы отслеживаем f(i), f(i-1) и f(i-2), что достаточно для вычисления f(i+1).
  • Как только i=n, мы закончили.

Ответ 5

Функция f определяется по правилу: f(n) = n, if n<3 и f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3), if n > 3. Напишите процедуру, которая вычисляет f посредством рекурсивного процесса.

Уже написано:

f(n) = n,                                  (* if *)  n < 3
     = f(n - 1) + 2f(n - 2) + 3f(n - 3),   (* if *)  n > 3

Верьте или нет, когда-то был такой язык. Чтобы записать это на другом языке, это всего лишь вопрос синтаксиса. И, кстати, определение, поскольку вы (ошибочно) цитируете его, имеет ошибку, которая сейчас очень очевидна и понятна.

Напишите процедуру, которая вычисляет f с помощью итеративного процесса.

Итерация означает движение вперед (есть ваше объяснение!), В отличие от рекурсии, идущей назад сначала:

f(n)   =  f(n - 1) + 2f(n - 2) + 3f(n - 3) 
       =  a        + 2b        + 3c
f(n+1) =  f(n    ) + 2f(n - 1) + 3f(n - 2)
       =  a'       + 2b'       + 3c'          a' = a+2b+3c, b' = a, c' = b
......

Это, таким образом, описывает переходы состояний задачи как

 (n, a, b, c)  ->  (n+1, a+2*b+3*c, a, b)

Мы могли бы кодировать его как

g (n, a, b, c) = g (n+1, a+2*b+3*c, a, b)

но, конечно, это никогда не остановится. Поэтому мы должны вместо этого

f n = g (2, 2, 1, 0)
  where
  g (k, a, b, c) = g (k+1, a+2*b+3*c, a, b),    (* if *) k < n
  g (k, a, b, c) = a,                           otherwise 

и это уже точно так же, как и код, о котором вы просили, вплоть до синтаксиса.

Подсчет до n здесь более естественный, следуя нашей парадигме "идти вперед", но отсчет до 0, поскольку код, который вы цитируете, конечно, полностью эквивалентен.

Угловые случаи и возможные ошибки "один за другим" не учитываются при выполнении неинтересных технических проблем.