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

В двух словах суть проблемы описывается так: экспоненциальная зависимость. То есть выпуск нового типа монет несуществующего доселе номинала влечёт за собой увеличение количества комбинаций монет в два раза. Ещё один номинал – ещё в два раза, и так до условной бесконечности. При галопирующей инфляции, когда новые монеты/купюры выпускаются достаточно часто, для решения задачи придётся покупать более мощный компьютер. А где на это взять деньги при галопирующей то инфляции?

Итак, решение, которое на самом деле достаточно просто.
Если представить отрезок от нуля до С (сдача) с нанесёнными на ней точками, соответствующими номиналам монет, то любой интеллектуальный человек увидит примерно следующую картинку:
image
Ну, возможно в других цветах.
Итак, что мы можем сказать про красные точки? Конечно же то, что число, соответствующее любой этой точке, есть сумма, которую мы можем выдать в виде сдачи. Причём одной монетой. Возможно кто-то увидел что-то другое, но я, как художник, вкладывал именно этот смысл. И, как художник же, дорисую на этой картинке ещё одну точку, соответствующую сумме первой (слева направо) точки с этой же точкой (всё правильно: получится удвоенный номинал). Дорисую этим же цветом, ибо новая точка означает то же самое – эта сумма так же может быть сдачей.
image
Далее беру первую точку и суммирую со второй, даже если вторая точка это предыдущая сумма (это неважно, ибо все точки здесь равны). Новую точку опять нанесу на отрезок.
image
Если новая точка выходит за пределы отрезка, то ничего не рисуем, а возвращаемся к началу отрезка и берём следующую точку, то есть вторую. Далее то же самое (слева направо).
Собственно, когда точка С (напомню, это сдача) станет красной, можно считать, что решение найдено. А можно пройти полный цикл и найти оптимальное решение, что бы количество монет было минимальным.

С точки зрения программирования, здесь два цикла. Первый от 0 до С/2 (нет необходимости брать первую точку большей С/2, т.к. вторая точка всегда больше первой и в сумме они выйдут за границы отрезка). Второй цикл является встроенным в первый, он начинается с той же точки, на которую указывает внешний цикл и до момента, когда сумма покинет границы отрезка.
По сути это перебор: мы не теряем ни одного варианта, и гарантированно найдём оптимальное решение, или придём к выводу, что решения нет.

Давайте подсчитаем количество итерации внутри наших циклов. По внешнему циклу – это С/2, по внутреннему – где-то столько же. Умножим С/2 * С/2 = (С^2) / 4. Округлим до С в квадрате. Это наихудший вариант, когда весь наш отрезок просто состоит из красных точек. Если же между точками будут пробелы, количество итераций значительно уменьшится.
Как видим при определении сложности решения задачи мы не используем количество номиналов монет. Эта величина напрямую совсем никак не влияет на сложность решения. Влияют величины этих номиналов, скажем монета в 1 цент и сделает этот отрезок полностью красным. Поэтому эту монету лучше и не брать в расчёт, а в конце решения взять ближайшую к С красную точку и набросать сверху одноцентовиков. Но это уже момент оптимизации алгоритма, а она за рамками этой статьи.

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

1. В файле init.h задать COINS_NUMBER — количество номиналов монет, и AMOUNT — сумма сдачи.
2. В файле coinc.c указать номиналы монет в массиве coins.
3. Под Linux запустить make_sh.
4. Запустить программу app на выполнение
Note
На экран так же будет выведено время выполнения и количество используемой памяти. Я забыл упомянуть, что придётся использовать дополнительную память. Но её количество не зависит от количества номиналов, так что всё честно.


Приведу какой-нибудь забавный пример. Представим, что в какой-нибудь стране к власти пришли математики и ввели в обращения 32 номинала монет: 2, 3, 5, 7, 11, 13, 17, 19… 131. Для удобства счёта выбрали только простые числа (ну не смешно ли?). И, что бы убедиться, что денежная реформа прошла успешно, послали в магазине гонца разменять купюру 5333 (тоже простое число). Кассир на стареньком одноядернике решил задачу: 39 монет номиналом 131 пифагороцентов, одну монету 127 и одну 97. Расчёт занял 3 секунды и чуть больше мегабайта памяти. Правительству доложили, что народ реформой доволен, считает быстро.
Note
P.S. Кстати иметь номиналы монет в виде простых чисел – на самом деле хорошая идея, ибо любую сумму можно представить двумя или тремя монетами, и нет смысла в больших кошельках.


И пример, который чуточку сложнее проверить. Монеты, сто номиналов в такой вот странной последовательности: 0101, 0202, 0303… 9898, 9999, 100100. Сумма сдачи: 101010. Поиск решения занял 1 секунду и чуть больше мегабайта памяти. А решения, собственно, и нет, то есть нельзя такими монетами набрать такую сумму. С этими же монетами проверка суммы в 1 миллион займёт 26 мегабайт и сотни секунд, что говорит об экспоненциальной зависимости от суммы, но не количества номиналов монет.

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

Комментарии (23)


  1. slonopotamus
    18.09.2019 20:31
    +5

    Вы бы хоть написали в начале поста что за задачу решаете-то… Ну хотя бы чтобы слова про "знаменитой задаче с монетами, которыми необходимо отсчитать сдачу" были ссылкой на соответствующую статью в википедии.


    1. WhiteBlackGoose
      18.09.2019 23:02

      Согласен. @ТС, я думаю много кто топит вашу статью из-за такого маленького косячка


  1. halted
    18.09.2019 21:01
    +4

    Знаменитая задача с монетами, это когда надо за несколько взвешиваний определить фальшивую.


  1. Stecenko
    18.09.2019 21:10
    +1

    Задача о размене минимальным числом монет — не менее знаменита.
    Обычно решается жадным алгоритмом (если монетная система является канонической — дает оптимальное решение) или динамическим программированием с мемоизацией.


    1. Zenitchik
      18.09.2019 21:49

      Советская система 1,2,3,5 — не каноническая?


      1. Stecenko
        19.09.2019 05:42

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


        1. mayorovp
          19.09.2019 10:37

          А система 1, 30, 100 — каноническая или нет? Попробуйте разменять 120.


          1. Stecenko
            19.09.2019 11:56

            Жадно — 1 монета в 100 р, и 20 монет в 1р. Оптимально — 4 монеты по 30.
            Понял, намекаете, что жадина в этом случае не работает.
            Пойду поищу точное определение канонической системы.


            1. lair
              19.09.2019 12:08
              +1

              Пойду поищу точное определение канонической системы.

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


              1. Stecenko
                19.09.2019 12:36

                Да, уже нашел именно эту ссылку, спасибо.
                Кому интересно — есть алгоритм определяющий, является ли монетная система канонической, сложность O(n^3).
                Первые n членов степенного ряда 2^n образуют каноническую монетную систему. И у ряда Фибоначчи — тоже.


    1. neurocore
      18.09.2019 22:41

      Обычная динамика по таблице даёт O(kn), не нужно мемоизации, где k — число номиналов, n — сумма


    1. WhiteBlackGoose
      18.09.2019 23:01

      Мемоизация — меморизация наверное?)


      1. lair
        18.09.2019 23:42

        1. WhiteBlackGoose
          19.09.2019 07:12

          Не знал, что для динамики есть еще одно слово.


          1. lair
            19.09.2019 11:23
            +1

            Мемоизация — не то же самое, что динамика.


  1. FGV
    19.09.2019 07:02

    вторая – это количество разрядов числа, представляющего сдачу.

    обычный инт64 и цена мл. разряда = минимальной монете и никаких проблем.


  1. lexas
    19.09.2019 10:39

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


  1. toyban
    19.09.2019 10:40

    Боюсь, Вы неправильно считаете сложность своего алгоритма. Он никак не может быть O(C^2), где C — это количество разных номиналов. Или Вы утверждаете, будто взяв только константное количество номиналов (допустим, 2), Ваш алгоритм за константное время (то есть за 4 итерации) получит ответ? Конечно нет!


    Сложность Вашего алгоритма — как минимум ?(n^2). Самый худший для Вас вариант — это когда все номиналы — простые числа.


    Также Вы не показали, что результат алгоритма — это оптимальная по количеству монет сдача. Возможно, Вам это очевидно по какой-то логике. Мне же, к сожалению, нет. Это надо доказывать.


    Дальше также не понятно, с чего Вы взяли, будто сложность задачи растет экспоненциально от количества наличных номиналов. Это было бы так, если б Вы решали ее полнопереборным вариантом. Но как указал один из комментирующих здесь, эта задача — полиномиально решаема с помощью дин. программирования со сложностью O(Cn). Но C всегда не больше n, так что в самом худшем случае сложность будет O(n^2). Но в отличии от Вашего подхода, этот алгоритм гарантирует оптимальное решение.


    1. toyban
      19.09.2019 10:57

      Хотя нет, я неправ. Ваш алгоритм — это и есть то самое решение динпрограммированием. Только без рекурсии и как бы "инвертировано". Но сложность все равно не O(C^2), а как было замечено выше — O(Cn).


      1. kuchynski Автор
        19.09.2019 11:03

        Под С я понимаю сумму. Т.е. сложность, как вы справедливо заметили выше: «Сложность Вашего алгоритма — как минимум ?(n^2). Самый худший для Вас вариант — это когда все номиналы — простые числа. » Только «как максимум»
        И да, это вариация динамического программирования. Если для каждой точки запоминать результат, решение будет оптимальным.


  1. domix32
    19.09.2019 13:05

    А тесты будут?


    1. kuchynski Автор
      19.09.2019 13:17

      По ссыске на github есть исходники. Или напишите здесь исходные данные — рассчитаю.


      1. domix32
        19.09.2019 22:20

        Я к тому, что предлагается поверить что реализация верная.