— первая – это количество номиналов монет,
— вторая – это количество разрядов числа, представляющего сдачу.
И обе эти величины оказывают экспоненциальное воздействие на нагрузку машины Тьюринга, которая собственно и занимается подсчётом.
Признавать, что человек имеет сразу две зависимости, не принято даже в обществе алкоголиков. Поэтому я решил подстраховаться и избавиться от одной из этих проблем заранее. Путь это будет количество номиналов монет.
В двух словах суть проблемы описывается так: экспоненциальная зависимость. То есть выпуск нового типа монет несуществующего доселе номинала влечёт за собой увеличение количества комбинаций монет в два раза. Ещё один номинал – ещё в два раза, и так до условной бесконечности. При галопирующей инфляции, когда новые монеты/купюры выпускаются достаточно часто, для решения задачи придётся покупать более мощный компьютер. А где на это взять деньги при галопирующей то инфляции?
Итак, решение, которое на самом деле достаточно просто.
Если представить отрезок от нуля до С (сдача) с нанесёнными на ней точками, соответствующими номиналам монет, то любой интеллектуальный человек увидит примерно следующую картинку:
Ну, возможно в других цветах.
Итак, что мы можем сказать про красные точки? Конечно же то, что число, соответствующее любой этой точке, есть сумма, которую мы можем выдать в виде сдачи. Причём одной монетой. Возможно кто-то увидел что-то другое, но я, как художник, вкладывал именно этот смысл. И, как художник же, дорисую на этой картинке ещё одну точку, соответствующую сумме первой (слева направо) точки с этой же точкой (всё правильно: получится удвоенный номинал). Дорисую этим же цветом, ибо новая точка означает то же самое – эта сумма так же может быть сдачей.
Далее беру первую точку и суммирую со второй, даже если вторая точка это предыдущая сумма (это неважно, ибо все точки здесь равны). Новую точку опять нанесу на отрезок.
Если новая точка выходит за пределы отрезка, то ничего не рисуем, а возвращаемся к началу отрезка и берём следующую точку, то есть вторую. Далее то же самое (слева направо).
Собственно, когда точка С (напомню, это сдача) станет красной, можно считать, что решение найдено. А можно пройти полный цикл и найти оптимальное решение, что бы количество монет было минимальным.
С точки зрения программирования, здесь два цикла. Первый от 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 на выполнение
Приведу какой-нибудь забавный пример. Представим, что в какой-нибудь стране к власти пришли математики и ввели в обращения 32 номинала монет: 2, 3, 5, 7, 11, 13, 17, 19… 131. Для удобства счёта выбрали только простые числа (ну не смешно ли?). И, что бы убедиться, что денежная реформа прошла успешно, послали в магазине гонца разменять купюру 5333 (тоже простое число). Кассир на стареньком одноядернике решил задачу: 39 монет номиналом 131 пифагороцентов, одну монету 127 и одну 97. Расчёт занял 3 секунды и чуть больше мегабайта памяти. Правительству доложили, что народ реформой доволен, считает быстро.
И пример, который чуточку сложнее проверить. Монеты, сто номиналов в такой вот странной последовательности: 0101, 0202, 0303… 9898, 9999, 100100. Сумма сдачи: 101010. Поиск решения занял 1 секунду и чуть больше мегабайта памяти. А решения, собственно, и нет, то есть нельзя такими монетами набрать такую сумму. С этими же монетами проверка суммы в 1 миллион займёт 26 мегабайт и сотни секунд, что говорит об экспоненциальной зависимости от суммы, но не количества номиналов монет.
Комментарии (23)
halted
18.09.2019 21:01+4Знаменитая задача с монетами, это когда надо за несколько взвешиваний определить фальшивую.
Stecenko
18.09.2019 21:10+1Задача о размене минимальным числом монет — не менее знаменита.
Обычно решается жадным алгоритмом (если монетная система является канонической — дает оптимальное решение) или динамическим программированием с мемоизацией.Zenitchik
18.09.2019 21:49Советская система 1,2,3,5 — не каноническая?
Stecenko
19.09.2019 05:42Нет. Каноническая — если каждый номинал по меньшей мере в два раза больше предыдущего.
mayorovp
19.09.2019 10:37А система 1, 30, 100 — каноническая или нет? Попробуйте разменять 120.
Stecenko
19.09.2019 11:56Жадно — 1 монета в 100 р, и 20 монет в 1р. Оптимально — 4 монеты по 30.
Понял, намекаете, что жадина в этом случае не работает.
Пойду поищу точное определение канонической системы.lair
19.09.2019 12:08+1Пойду поищу точное определение канонической системы.
Будете удивлены. Каноническая монетная система — это такая, для которой жадный алгоритм дает оптимальное решение задачи размена для любого целого числа.
Stecenko
19.09.2019 12:36Да, уже нашел именно эту ссылку, спасибо.
Кому интересно — есть алгоритм определяющий, является ли монетная система канонической, сложность O(n^3).
Первые n членов степенного ряда 2^n образуют каноническую монетную систему. И у ряда Фибоначчи — тоже.
neurocore
18.09.2019 22:41Обычная динамика по таблице даёт O(kn), не нужно мемоизации, где k — число номиналов, n — сумма
WhiteBlackGoose
18.09.2019 23:01Мемоизация — меморизация наверное?)
FGV
19.09.2019 07:02вторая – это количество разрядов числа, представляющего сдачу.
обычный инт64 и цена мл. разряда = минимальной монете и никаких проблем.
lexas
19.09.2019 10:39Это, в каком то смысле, переоткрытие приближенного решения задаче о рюкзаке с помощью динамического программирования.
toyban
19.09.2019 10:40Боюсь, Вы неправильно считаете сложность своего алгоритма. Он никак не может быть O(C^2), где C — это количество разных номиналов. Или Вы утверждаете, будто взяв только константное количество номиналов (допустим, 2), Ваш алгоритм за константное время (то есть за 4 итерации) получит ответ? Конечно нет!
Сложность Вашего алгоритма — как минимум ?(n^2). Самый худший для Вас вариант — это когда все номиналы — простые числа.
Также Вы не показали, что результат алгоритма — это оптимальная по количеству монет сдача. Возможно, Вам это очевидно по какой-то логике. Мне же, к сожалению, нет. Это надо доказывать.
Дальше также не понятно, с чего Вы взяли, будто сложность задачи растет экспоненциально от количества наличных номиналов. Это было бы так, если б Вы решали ее полнопереборным вариантом. Но как указал один из комментирующих здесь, эта задача — полиномиально решаема с помощью дин. программирования со сложностью O(Cn). Но C всегда не больше n, так что в самом худшем случае сложность будет O(n^2). Но в отличии от Вашего подхода, этот алгоритм гарантирует оптимальное решение.
toyban
19.09.2019 10:57Хотя нет, я неправ. Ваш алгоритм — это и есть то самое решение динпрограммированием. Только без рекурсии и как бы "инвертировано". Но сложность все равно не O(C^2), а как было замечено выше — O(Cn).
kuchynski Автор
19.09.2019 11:03Под С я понимаю сумму. Т.е. сложность, как вы справедливо заметили выше: «Сложность Вашего алгоритма — как минимум ?(n^2). Самый худший для Вас вариант — это когда все номиналы — простые числа. » Только «как максимум»
И да, это вариация динамического программирования. Если для каждой точки запоминать результат, решение будет оптимальным.
slonopotamus
Вы бы хоть написали в начале поста что за задачу решаете-то… Ну хотя бы чтобы слова про "знаменитой задаче с монетами, которыми необходимо отсчитать сдачу" были ссылкой на соответствующую статью в википедии.
WhiteBlackGoose
Согласен. @ТС, я думаю много кто топит вашу статью из-за такого маленького косячка