Сдача

Как нам уже известно, естественный «жадный» подход к этой задаче работает неправильно при любом наборе номиналов. Например, при номиналах , и «жадный» алгоритм разменяет центов тремя монетами (), хотя это возможно сделать всего лишь двумя (). Ваша цель — использовать динамическое программирование для решения задачи «Сдача» с номиналами , и .

  • Входные данные: Целое число .
  • Выходные данные: Минимальное количество монет номиналами , , , чтобы выдать сдачу с .
  • Ограничения: .

Пример 1

Ввод

Вывод

34

9

  • .

Для оптимального варианта сдачи с необходимо семь монет. Рассмотрим произвольный поднабор оптимального решения — например, если сложить четыре монеты из приведённого ниже прямоугольника, то получится .

💡 Остановитесь и подумайте:
Сможете разменять центов тремя монетами?

Ответ на этот вопрос: «Нет». Если бы размен был возможен тремя монетами, то можно было бы заменить выделенные четыре монеты и получить сдачу с шестью монетами, а не семью.

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

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

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

Тогда

Следовательно, . Таким образом, для решения задачи при достаточно решить её при и добавить единицу.

💡 Остановитесь и подумайте:
Мы закончили?

Проблема в том, что мы не знаем значение . Тем не менее мы знаем, что равняется или , или , или . Так равно одному из следующих вариантов: , и .

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

При небольших аргументах это соотношение выражает значение рекурсивным образом через собственные значения. Для такой нисходящей рекурсии нам необходимо указать базовый случай. У нас это будет : .

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

Change(money):
    if money=0:
        return 0
    else:
        result = +infinity
        for c=1,3,4:
        if c <= money:
            result = min(result,1+Change(money-c))
        return result

У этого алгоритма есть серьёзная проблема: он становится крайне медленным, потому что вызывает Change(money) снова и снова для одного и того же значения .

Мемоизация — стандартный способ избежать этого: при вычислении Change(money) мы можем использовать сохранение в таблице и тогда нам не придётся делать перевычисление.

table=associative array
    
Change(money):
    if table[money] is not yet computed:
    if money=0:
        table[money]←0
    else:
        result = +infinity
        for c=1,3,4:
        if c <= money:
            result = min(result,1+Change(money-c))
        table[money] = result
    return table[money]

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

Change(money):
    table[0..money] = [+infinity,…,+infinity]
    table[0] = 0
    
    for m from 1 to money:
        for c=1,3,4:
        if c <= m:
            table[m] = min(table[m],1+table[m-c])
    return table[money]

Время выполнения этого алгоритма составляет , так как каждая итерация внешнего цикла for проходит за постоянное время.

💡 Остановитесь и подумайте:
Обратите внимание, что описанный выше алгоритм неявно находит кратчайший путь в ориентированном ациклическом графе, приведённом ниже (при ). Рассчитайте время выполнения алгоритма для самого короткого пути в аналогичном графе при произвольном значении .

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф8.1. Принципы построения алгоритмов
Следующий параграф8.3. Задача «Простой калькулятор»