Пример SICP: подсчет изменений, не могу понять

Я начинаю следующий курс SICP по MIT OpenCourseWare, используя как видео-лекции, так и книгу, доступную в Интернете. Вчера я наткнулся на пример, который спрашивает, можем ли мы написать процедуру, чтобы вычислить количество способов изменить любую заданную сумму денег.

Эта проблема имеет простое решение как рекурсивную процедуру:

(define (count-change amount)
  (cc amount 5))
(define (cc amount kinds-of-coins)
  (cond ((= amount 0) 1)
        ((or (< amount 0) (= kinds-of-coins 0)) 0)
        (else (+ (cc amount
                     (- kinds-of-coins 1))
                 (cc (- amount
                        (first-denomination kinds-of-coins))
                     kinds-of-coins)))))
(define (first-denomination kinds-of-coins)
  (cond ((= kinds-of-coins 1) 1)
        ((= kinds-of-coins 2) 5)
        ((= kinds-of-coins 3) 10)
        ((= kinds-of-coins 4) 25)
        ((= kinds-of-coins 5) 50)))

Если вы хотите узнать больше об этом, я взял его из здесь.

они вычисляют число (N) способов изменения кватити (A) с использованием K видов монет, добавляя:

  • количество способов (X) изменения A без монет первого типа.

  • Количество способов (Y) изменения (A - D), где D - номинал монеты fisrt, используя ВСЕ К-типы монет.

Проблема в том, что я просто этого не понимаю. Следуя, они пытаются объяснить, говоря:

"Чтобы понять, почему это так, обратите внимание, что способы внесения изменений можно разделить на две группы: те, которые не используют ни один из монет первого типа, и те, которые делают. Поэтому общее количество способов для внесения изменений для некоторой суммы равно количеству способов внести изменения в сумму без использования какого-либо первого монета, плюс количество способов сделать изменения, предполагая, что мы используем первый вид монеты. (Последнее предложение совпадает с добавлением N = X + Y?) Но последнее число равно числу способов внести изменения в сумму, оставшуюся после использования монеты первого рода. (После использования этой монеты они ссылаются на способы внесения изменений с или без монеты первого типа?)"

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

Ответ 1

Если мы слишком много думаем о рекурсии, мы уже терпим неудачу. Лично я использую две метафоры в мыслях рекурсии. Один из небольшой книги "Маленький интриган": The Seventh Commandment - Recur on the subparts that are of the same nature. Еще одна парадигма разделения-победителя для разработки алгоритмов. По сути, это то же самое, что мыслить рекурсивно.

  • Разделить на подчасти с одинаковой природой.

Задача состоит из двух переменных: числа (N) денег и видов (K) монет, поэтому любое деление должно соответствовать следующему: 1. reducing all variables: both N and K, 2. the subparts are the same nature so each subpart can be solved by the recursion process itself or be can solved directly. 3. all subparts together == the original one part, no more and no less.

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

  1. захват

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

  1. объединить

Как результаты объединяются при разрезе подчастей. В этом решении они просто +.

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

B.T.W, я бы очень рекомендовал the little schemer для изучения рекурсии. Насколько мне известно, это намного лучше, чем любые другие в этом конкретном пункте.

Ответ 2

"количество (N) способов... использующих N видов", эти два N явно не совпадают. так скажем, K видов монет.

У нас много монет, но каждая монета стоит 1, 5, 10, 25 или 50 центов, всего 5 видов монет. Нам нужно что-то купить за доллар, 100 центов. Предполагается неограниченная поставка каждого вида монет. Сколько у нас способов достичь общей суммы в 100?

Мы либо используем несколько монет (одну или несколько) по 50 центов, либо нет. Если нет, мы все равно должны добраться до 100 только с 4 видами монет. Но если мы это сделаем, то после использования одной монеты 50 центов общая сумма станет 100-50 = 50 центов, и мы все равно можем использовать все 5 видов монет, чтобы получить новую, меньшую общую сумму:

ways{ 100, 5 } = ways{ 100, 5 - 1 }      ;   never use any 50-cent coins
                 +                       ; OR
                 ways{ 100 - 50,  5 }    ;   may use 50-cent coins, so use one

Или вообще

ways( sum, k ) = ways( sum, k - 1 )
                 +
                 ways( sum - first_denomination(k),  k )

Это все, что нужно сделать. Увидеть? Обобщение происходит естественным образом с абстракцией (замена конкретных значений символами и превращение их в параметры в определении функции).


Тогда нам нужно позаботиться о базовых случаях. Если sum = 0, результат равен 1: есть один способ достичь общей суммы 0 (а это: не брать монет).

Если k = 0, это означает, что нам не разрешено использовать любые виды монет; другими словами, у нас нет никакого способа получить сумму, любую сумму, не используя хотя бы несколько монет (если только сумма не равна 0, но мы уже рассмотрели этот случай выше). Таким образом, результат должен быть 0.

То же самое, если sum < 0, конечно. Невозможно, то есть 0 способов подвести итог, используя любые монеты с любым положительным номиналом.


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

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


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

Другими словами, попытайтесь найти структуру в задаче так, чтобы она имела субструктуру (ы), похожую на целое (например, фракталы; или, например, суффикс списка также является списком и т.д.); тогда рекурсия это: если у нас уже есть решение; разбор экземпляра проблемы (в соответствии с тем, как мы структурировали нашу проблему); преобразование "меньшей" субструктуры решением; а затем объединить все это обратно некоторым простым способом (в соответствии с тем, как мы структурировали нашу проблему). Хитрость заключается в том, чтобы распознать существующую внутреннюю структуру вашей проблемы, чтобы решение было естественным.

Или в Прологе:

recursion( In,       Out) :- 
  is_base_case(  In), 
  base_relation( In, Out).

recursion( In,       Out) :- 
  not_base_case( In),
  constituents(  In,   SelfSimilarParts,       LeftOvers),  % forth >>>
  maplist( recursion,  SelfSimilarParts,
                             InterimResults),
  constituents(      Out,    InterimResults,   LeftOvers).  % and back <<<

Ответ 3

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

Ниже приведен график того, как алгоритм выполняется для простого случая. Сумма составляет 6 пенсов, и у нас есть два вида монет: пять пенсов (индекс 2) и копейка (индекс 1).

Обратите внимание, что все листовые узлы оцениваются до 0 или 1. Это очевидно, когда мы смотрим на условие в процедуре (возвращается одно из этих значений, иначе функция снова вызывает себя.) Только два листовых узла оценивают 1, поэтому есть два способа сделать 6 пенсов из этих двух видов монет, то есть 6 пенни, или пенни и пять пенсов.

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

                             (cc 6 2)
                                |
                 -----------------------------------
                 |                                 |
              (cc 6 1)                          (cc 1 2)
                 |                                 |
   ------------------                         --------------
   |                |                         |            |
(cc 6 0)=0       (cc 5 1)                  (cc 1 1)     (cc -4 2)=0
                    |                         |
             -------------             -------------
             |           |             |           |
          (cc 5 0)=0  (cc 4 1)      (cc 1 0)=0  (cc 0 1)=1
                         |
               --------------
               |            |
            (cc 4 0)=0   (cc 3 1)
                            |
                     --------------
                     |            |
                  (cc 3 0)=0   (cc 2 1)
                                  |
                           --------------
                           |            |
                        (cc 2 0)=0   (cc 1 1)
                                        |
                                 --------------
                                 |            |
                              (cc 1 0)=0   (cc 0 1)=1