Есть вот такая, вроде бы, простая задача на литкоде: Дано три числа total
- сколько у вас есть денег, cost1
, cost2
- цены двух товаров. Надо подсчитать, сколько всего существует различных способов купить сколько-то этих двух товаров, не выходя из бюджета (значение имеет только общее количество покупок, а не порядок). Иными словами, сколько существет целых неотрицательных пар (x, y), таких что x*cost1+y*cost2 <= total
. Например, имея товары ценами {5, 10} и 20 денег на руках, есть 9 способов потратить деньги: 0, 5, 5+5, 5+5+5, 5+5+5+5, 10, 10+5, 10+5+5, 10+10.
Задача даже помечена как medium, и вообще почти в одну строчку решается, но это если допускать безумно медленное решение за O(total / max(cost1, cost2))
, т.е линейное от входных чисел. А сможете ли вы решить ее сильно быстрее - за O(log(max(cost1, cost2)))
? В этом случае задачка становится вполне себе hard и требует много математики, изобретательности и аккуратности. Если интересно решение, добро пожаловать под кат. Буду рад любым альтернативным решениям. Может кто-то сможет додуматься до похожего решения проще.
Примечание по оформлению
Я буду активно пользоваться "аббревиатурами" в этой статье: всякое нудное объяснение я буду прятать в них. Если вы видите подчеркнутое слово "очевидно", наведите туда мышку и более глубокое объяснение должно всплыть в подсказке.
Сводим задачу к сумме определенного вида
Итак, для экономии букв давайте введем обозначения: a
, b
- цены товаров, c
- сумма на руках. Во-первых, можно считать, что GCD(a,b)=1
, иначе очевидно можно просто сократить все три числа на наибольший общий делитель (в случае с c
, там будет деление нацело). Пусть x
и y
- переменные, сколько товаров первого и второго типа мы покупаем. Тогда у нас есть Диофантово неравенство:
Неравнества с двумя переменными решать вообще сложно. Давайте как-то получим одну переменную. Эталонное медленное решение предполагает, например, перебор всех возможных значений x
и дальше нахождение количества возможных значений y
. Но как это решение ускорять я не придумал. Давайте вместо этого перебирать потраченную сумму:
Это уже Диафантово уравнение с какими-то дополнительными условиями. Но оно весьма стандартное и, поскольку мы уже сократили наибольший общий делитель, можно найти общую формулу для всех его решений:
и тут - это какие-то константы, которые можно найти через расширенный алгоритм эвклида. - это свободная переменная, позволяющая прыгать между всеми целочисленными точками на прямой. Сдвиги на вектор между соседними целочисленными точками очевидны.
Теперь надо решить дополнительные ограничения на неотрицательность и . Они дают нам ограничения на :
Поскольку целое, можно расставить округления:
Чтобы не думать об отрицательных числах, давайте договоримся, что , . Этого можно очевидно добиться. Тут же стоит заметить, что очевидно , поэтому, округляя эти значения вверх и вниз мы в худшем случае получим нижнюю границу на 1 больше верхней границы (если вещественные границы оказались между одними и теми же целыми числами). Поэтому количество целых значений t, удовлетворяющие всем неравнествам (даже если их там таких нет), можно получить так:
А поскольку может принимать все значения до c
, формула для ответа будет:
Тут в перемешку округления вниз и вверх. Давайте выразим ceil через floor. Они обычно отличаются на 1, кроме случаев, когда значение под ceil целое. Поскольку и очевидно взаимнопросты, то выражение под ceil целое, только для делящихся на . Поэтому давайте там везде поставим ceil=floor+1, но запомним, что ровно в итерациях у нас вычитается лишняя единица, их надо будет после суммы назад прибавить. +1 от ceil сократиться с +1 уже под знаком суммы и мы получим:
Теперь разобьем сумму на две и введем вспомогательную функцию:
В итоге, ответ к задаче:
Выводим рекуррентную формулу для суммы
Теперь осталась самая малость: научиться считать сумму в SumFloor
за логарифм.
Боремся с граничными условиями
Во-первых, можно считать, что и взаимнопросты. Можно их на НОД сократить, вообще не поменяв ни одно слагаемое. Но вообще числа, которые в этой задаче получаются, уже взаимнопросты (см. "очевидно" выше). Но в общем случае один раз в начале подсчитать GCD за логарифм тоже можно - на оценку сложности это не повлияет.
Во-вторых, давайте считать, что . В противном случае можно выделить лишнюю часть, если подставитьв сумму и подсчитать все целое отдельно в арифметической прогрессии:
Или:
Вот мы и выразили сумму с через сумму c .
Во-третьих, можно считать, что . В противном случае, можно разбить сумму на куски, первый несколько раз проходит по чисел, второй проходит не больше чем m. Пусть , тогда:
Давайте отдельно рассмтрим левую и правую суммы. В левой сумме воспользуемся равенством и подсчитаем отдельно не-модули как арифметическую прогрессию:
В оставшейся сумме пробегает все возможные остатки по модулю ровно раз. А поскольку и взаимно просты, то очевидно тоже пробегает все возможные остатки по модулю ровнораз, но в каком-то другом порядке. Но порядок нам не важен, можно просто раз просуммировать все остатки от 0 до . Итак, левая сумма сокращается до:
В правой сумме заменим переменную :
Собирая все вместе, помня, что :
Вот мы и выразили сумму до через сумму до .
Выводим формулу для общего случая
Итак, мы знаем, что , , .
А теперь главная хитрость тут, это применить Hermite's identity. Я много с ним эксперементировал и самый простой способ получается, если взять зачисло , а за оставшееся :
Теперь, как обычно это бывает со вложенными суммами, интегралами или пределами, надо первым делом поменять их порядок:
Следующая догадка состоит в том, чтобы заметить, что под знаком округления вниз стоит сумма из двух чисел, меньших 1 каждое! Ведь и . Поэтому значение floor будет или 0 или 1. 1 только в случае . Отсюда можно вывести ограничение на , при котором что-то ненулевое вообще суммируется: . Т.к. целое, то можно переписать:
Но сумма изначальная по была от 0 до поэтому, чтобы подсчитать только единицы, надо посмотреть, сколько их от до . Когда эта нижняя граница не превосходит то количество единичных слагаемых будет . Если же эта нижняя граница больше то единиц в сумме вообще не будет, но формула выше даст отрицательное число. Поэтому эту формулу можно использовать, только когда она дает неотрицательное число. Теперь можно вывести условие на , при котором это условие выполняется:
Можно сгрупировать floor и целые числа по разные стороны. Дальше будет неравнество вида Его можно очевидно заменить на
Т.к. целое:
Это число, для сокращения выкладок ниже, назовем .
Итак, пробегает значения от до , а внутри происходит сумма скольки-то единиц. Их количество мы уже считали выше. Итоговая формула:
Полезно заметить, что (очевидно), поэтому запись выше имеет смысл. Немного спорный вопрос, а что происходит, если , но этот случай нам проблем не составит дальше и в итоге мы получим, что сумма 0 слагаемых равна 0.
Целую часть можно вынести за знак суммы, получив (и даже в случае все работает). Остается почти SumFloor(*, m, k), только вот там сумма не от 0 до какого-то числа, а от N' до какого-то большого значения. Эту сумму можно выразить, как .
Промежуточный итог:
Далее, вспоминая, как мы боролись с граничными случаями, можно заметить, что первая сумма сокращается до:
А вторая до:
В итоге получаем (напомниаю, что )
При этом, выше мы уже показали, что . Также, очевидно, что . Также, и взаимнопросты. Поэтому, после одного рекуррентного перехода мы всегда останемся в том же общем случае, поэтому опять проверять граничные условия не надо.
А база у рекурсии, если , то результат 0. Еще можно остановится, если и выдать сумму арифметической прогресси . Или, если , то сумма ничего - ноль.
Подобно алгоритму Эвклида для GCD, эта рекуррентная формула позволяет считать сумму за . Его еще можно итеративно реализовать и получить по памяти.
А вот и код:
// Расширенный алгоритм Эвклида
// Возвращает GCD(a,b) и переменные x, y, т.ч. a*x+b*y=GCD(a,b)
int GcdEx(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
if (b > a) return GcdEx(b, a, y, x);
int xx, yy;
int d = GcdEx(b, a % b, xx, yy);
y = xx - a/b*yy;
x = yy;
return d;
}
// Считает sum i=0.. n-1 floor(i*k/m)
// Ограничения:
// m > 0, 0 <= k < m, 0 <= n <= m, GCD(k, m) = 1
long long FloorSumInternal(long long n, long long k, long long m) {
if (k == 0 || n == 0) return 0;
if (m == 1) return k*n*(n-1)/2;
const long long n1 = ((m-n)*k + m - 1)/m;
long long ans = ((m-1)*(k-1) - (n1-1)*n1*(m/k))/2 + (n-m)*(k-n1);
ans -= FloorSumInternal(n1, m%k, k);
return ans;
}
// Считает sum i=0.. n-1 floor(i*k/m)
// Ограничения только естественные: m > 0, k >= 0, n >= 0
long long FloorSum(long long n, long long k, long long m) {
if (k == 0 || n == 0) return 0;
/*
В общем случае надо сократить GCD.
Но в этой задаче k и m, очевидно, будут взаимнопросты.
const long long d = Gcd(m, k);
m /= d;
k /= d;
*/
if (m == 1) return k*n*(n-1)/2;
if (k >= m) {
return FloorSum(n, k%m, m) + (k/m)*n*(n-1)/2;
}
if (n >= m) {
long long f = n/m;
return FloorSum(n%m, k, m) + f*(k*m*f-k-m+1) / 2 + k*f*(n%m);
}
return FloorSumInternal(n, k, m);
}
// Решение самой задачи.
long long waysToBuyPensPencils(int total, int cost1, int cost2) {
int x0, y0;
int d = GcdEx(cost1, cost2, x0, y0);
cost1 /= d;
cost2 /= d;
total /= d;
if (x0 >= 0) {
int t = x0/cost2 + 1;
x0 -= cost2*t;
y0 += cost1*t;
}
if (y0 <= 0) {
int t = -y0 / cost1 + 1;
x0 -= cost2*t;
y0 += cost1*t;
}
return FloorSum(total+1, y0, cost1) - FloorSum(total+1, -x0, cost2) + total / cost2 + 1;
}
Комментарии (44)
Sergey_Kovalenko
09.07.2023 07:13+3Сначала подумал: да ерунда, ровные ступеньки под прямой. Через секунду понял, что не прав... Да, классная ловушка для любителей математики, на досуге подумаю сам, потом посмотрю ваше решение. Спасибо за хорошую задачку!
thevlad
09.07.2023 07:13+2Фактически нам надо посчитать количество целочисленных точек для (n1*c1, n2*c2), где n1,n2 E Integer, внутри прямоугольного треугольника. Как мне кажется можно чисто из геометрических соображений, попробовать решить следующим образом. Рекурсивно бить на прямоугольник лежащий внутри треугольника на целочисленных координатах (0,0) - (x1*cost1, y1*cost2), и два новых прямоугольных треугольника ((x1+1)*cost1, 0) и (0, (y1 + 1)*cost2). Для двух новых треугольников спускаемся рекурсивно вниз. Но докрутить до рабочего решения по быстрому с наскоку не получилось, хотя кажется идея рабочая.
PS: хотя да, косяк, асимптота будет линейная 2^log2(N)
wataru Автор
09.07.2023 07:13Проблема в том, что так вы удваиваите количество треугольников. Тут, подобно QuickSort может даже O(n log n) получится, в зависимости от деталей реализации.
mentin
09.07.2023 07:13+1Под эталонным решением наверное имеется в виду не
O(min(cost1, cost2))
А O(total / max(cost1, cost2))?
wataru Автор
09.07.2023 07:13Да, вы правы, самое простое решение будет действительно за
O(total / max(cost1, cost2))
. Перепутал с другим решением.Исправил текст.
Но вообще, есть решение и за
O(min(cost1, cost2))
: можно перебрать значение x%B. После чего уравнение упрощается до x'A+y <= C', откуда можно подсчитать обычную арифметическую прогрессию, перебирая все возможные y.Но, в общем-то, оба этих решения линейны по максимуму: Если ограничить все входные числа неким N, то обе этих оценки будут O(N) в худшем случае.
mentin
09.07.2023 07:13+1Пока не понял решение через min, но выбрав минимум из total/max и min, уже уменьшили сложность до корня :)
wataru Автор
09.07.2023 07:13+1Отличная идея! Но логарифм, все-равно круче!
А решение за минимум вот такое:
Считаем решения Ax+By <= C. Переберем x%B: x = x'*B+i. Подставим в неравенство и упрастим: B(Ax'+y) <= C-iA. Теперь для фиксированного y существует ровно floor((C-iA)/B)-y решений. Просуммируем по всем y — это будет просто арифметическая прогрессия за O(1). Ну, еще аккуратно с отрицательными числами еще надо границы перебора i подобрать. В итоге один цикл по i до B.
mentin
09.07.2023 07:13Не упростит ли решение переход от решения диофантова неравенства к решению равенства?
Когда мы решаем ax + by < c - это число точек в прямоугольном треугольнике. Давайте достроим его до прямоугольника. Число точек в прямоугольнике найти легко, а с другой стороны оно = дважды число точек в треугольнике минус число точек на гипотенузе этого треугольника (они были учтены дважды). То есть достаточно найти число решений диофантова равенства ax + by = c.
wataru Автор
09.07.2023 07:13+1Давайте достроим его до прямоугольника.
Тут проблема в том, что треугольник может иметь не целые вершины. Например, при 5x+7y=100. Эта прямая отсекается осями координат в каких-то рациональных точках.
А если такой треугольник достраивать до прямоугольника, то там не будет симметрии.Вот если верширны целые (когда C делится на A и на B), то есть много способов. Например, через формулу Пика для площади.
Alexandroppolus
09.07.2023 07:13В качестве идеи: например, b = 3, а = 2. Total = 20. Считаем черные точки. На синих линиях количество этих точек - сумма арифметической прогрессии. На красных - тоже. Синих линий всего floor(total/b) + 1, красных - floor(total/a) - floor(total/b). Остальное вроде понятно. Выглядит подозрительно просто, может я где ошибся?
wataru Автор
09.07.2023 07:13+1У вас картинка — очень частный случай. Вершины треугольнка в целых точках. В общем случае это не так. Например, цены 7, 13, бюджет — 20. Зеленая линия тут не будет проходить через целые точки на осях.
А в этом случае длины красных линий будут менятся на нецелое количество "шагов". Соответственно, у вас получится там похожая на SumFloor сумма округленной арифметической прогрессии (Только еще и начало будет не с 0, а с произвольной точки. Но это тоже можно считать, сведя его к разности двух SumFloor).
А вот этот частный случай действительно легко считается. Или можно до прямоугольника достроить, как mentin предложил, или через формулу Пика площади многоугольника на целых вершинах. Надо только через GCD найти количество точек на зеленой прямой.
Edit: поздно заметил, что у вас на картинке только одна точка целая. Это действительно шаг впередНо этот случай тоже можно обработать. Плохой случай, когда обе вершины нецелые. И еще, под "целыми" точками, я имел ввиду черные.
Edit2: Осознал! Ваша картинка работает, потому что 20 делиться на 1=3-2 (цены товаров). Именно поэтому все красные линии имеют целую длину в шагах. В общем случае это не так.
Alexandroppolus
09.07.2023 07:13Зеленая линия тут не будет проходить через целые точки на осях
Под "целыми точками" вы подразумеваете черные кругляшки, которые считаем?
Если в моем рисунке взять бюджет не 20, а 19, то зеленая линия тоже не проходит через них на обеих осях, тем не менее по прежнему имеются арифметические прогрессии. На синих это всегда (1+2+3+...), на красных в данном случае, при total==19, будет (2+4+6+8+...). Красная прогрессия зависит от, но, кажется, нулевой элемент и шаг можно вычислить.
wataru Автор
09.07.2023 07:13Да, черные кругляшки. Надо не бюджет, а надо цены менять. Например, 7, 3 и бюджет 17. Главное, чтобы бюджет не делился на cost1-cost2 (ибо вот на эту величину меняется цена при движении по красной линии).
Alexandroppolus
09.07.2023 07:13Да, красные не образуют прогрессию в общем случае.. У них та же проблема, как и для горизонтальных/вертикальных линий
Firz
09.07.2023 07:13Никогда не понимал, почему составителям алгоритмических задачек так сложно явно указывать условия задачи, хорошо хоть в примере указано как условия трактовать. К примеру, You can spend part or all of your money я трактую как явное указание на 0 < spent <= total, раз прям or all отдельно указано, а про 0 нет. А оказывается нет, all не входит в part и указан отдельно, а вот 0 это оказывается включено в part. ¯\_(ツ)_/¯
iliazeus
09.07.2023 07:13+2Эта неоднозначность разрешается первым же примером входных-выходных данных в условии задачи. Решение от этого, кстати, принципиально не меняется. И в целом, условие - это компромисс.
Firz
09.07.2023 07:13Вот я про это и говорю, почему неявно(я бы даже сказал неправильно, в данном случае с нулем и "part") написанные условия нужно исправлять примерами.
Abs_Magica
09.07.2023 07:13total = 20 cost1 = 5 cost2 = 10 x1 = 0 n1 = total i1 = total // max(cost1, cost2) while i1 > 0: x1 += (n1 - max(cost1, cost2)) // min(cost1, cost2) n1 -= max(cost1, cost2) i1 -= 1 print(1 + total // cost1 + total // cost2 + x1)
Получилось как-то так.
wataru Автор
09.07.2023 07:13Ну так это то самое медленное линейное решение за O(total / max(cost1, cost2)), упомянутое в начале статьи. Сложность в задаче — это придумать более быстрый алгоритм.
BaLord
09.07.2023 07:13-1Автор чуть исказил условия, но с этим надо определиться четко, сколько комбинаций когда обе покупки присутствуют.
" Надо подсчитать, сколько всего существует различных способов купить сколько-то этих двух товаров, не выходя из бюджета (значение имеет только общее количество покупок, а не порядок). "
Т.е. обязательно должна быть пара товаров, а не 0 товаров какого то типа, следует из условий.
Соответственно Total - A - B = получаем свободный остаток C.
Потом С / A = C1, затем С / B = C2.
Потом соответственно комбинаторика обычная, количество комбинаций X = С1 * С2
И потом прибавить 2 это в одном случае когда Первая покупка = 1 и Вторая покупка = 1
Единственное с комбинаторикой надо проверить мог ошибиться и X = (C1+1) * (С2+1)
wataru Автор
09.07.2023 07:13Ну вы бы хоть на примере из условия свое решение прогнали.
Ваше решение вообще не работает, потому что остаток C делится между и покупками первого и второго товара. Поэтому нельзя просто брать "комбинацию" любое количество из С1 и любое из С2. Взяв сколько-то товаров первого типа (x <= C1), у вас остается остаток C-A*x, что меньше С2.
BaLord
09.07.2023 07:13если считать что 0 товаров возможно, получается целочисленное деление.
С1 = 20 / 5 = 4 и С2 = 20 / 2 = 2, и соответственно получается X = 4 * 2 = 8
wataru Автор
09.07.2023 07:13Нет, тут вы считатете, что 0 товаров невозможно. Чтобы считать с 0, вам надо сделать +1 везде, ведь можно брать от 0 до 4 первого товара (5 вариантов) или от 0 до 2 второго (3 варианта). В итоге получается (4+1)*(2+1) = 15. А правильный ответ 9. Если же считать без нуля товаров (4*2), то получается, что надо обязательно брать каждый товар хотя бы по разу, а значит там всего 2 варианта (5+10 и 5+5+10), а вы насчитали 8.
BaLord
09.07.2023 07:13-1правильный 8мь, 9ть если только 0 1го товара и 0 2го товара, а так всего 2 получается
BaLord
09.07.2023 07:13чтобы считать комбинаторику уточните все же 0 возможен или нет. если нет то получает 2*1 = 2 варианта если 0 возможен то 2*4 = 8 мь вариантов
BaLord
09.07.2023 07:13считается элементарно C1 = 20 / 5 и С2 = 20 / 10, если 0 возможен, т.е. 4 * 2 = 8
второй вариант остаток от вычитания C - A - B = 20 - 5 - 10 = 5
соответственно C1 = (20 - 15) / 5 = 1 и С2 = (20 - 15) / 10 = 0, т.е. (1+1) * (0 + 1) = 2, почему прибавляем по 1 это самые первые варинты когда и и первый и второй товар в единичном экземпляре
BaLord
09.07.2023 07:139й вариант у вас получается только из-за того что вы берете что вы вообще не покупаете, т.е. оба товара 0
BaLord
09.07.2023 07:13с 9-ым вариантом у вас как в задаче про орел / решку, но можно еще 2 варианта монета упала на ребро и вообще не упала на поверхность...
BaLord
09.07.2023 07:13-2по вам есть еще несколько вариантов пошел в магазин, а вместо пирожка с капустой или пирожка с картошкой, купил 4 бутылки пива
либо еще вариант пошел в магазин и украл 4 бутылки пива
либо еще вариант пошел в магазин украл и пирожки и пиво, и чтобы не нарушать закон выложил это до кассы
wataru Автор
09.07.2023 07:13Ох, у вас все так напутанно!
Давайте другой тест рассмотрим, а то в этом так совпало, что ваш неправильный ответ на неправильную задачу отличается от искомого ответа всего на 1, и вы, не формализуя и не доказывая ничего, просто списали эту единицу на вариант {0, 0}.
Вот, например, цены 3 и 5, а бюджет 13. Тут есть 10 вариантов: {0, 3, 3+3, 3+3+3, 3+3+3+3, 5, 5+3, 5+3+3, 5+5, 5+5+3}. Ваша первая формула дает C=13-3-5=5, C1=5/3 = 1, C2 = 5/5 = 1. В итоге (C1+1)*(C2+1)=4. Даже близко не 10.
Если же, как вы в конце сделали, не вычитать 3 и 5 из 13, а взять 13 сразу, то будет: C1=13/3 = 4, C2=13/5=2, (4+1)*(2+1)=15. Тоже вообще не 10. Или если +1 в скобках не прибавлять, то будет 4*2=8 — все равно не 10.
И даже если {0,0} не рассматривать, то нужно получить 9, а оно никак из ваших формул не вылезает.
BaLord
09.07.2023 07:13извините если каждый товар должен присутствовать единожды, откуда вы 0 выводите, т.е. нельзя покупать один товар, их должно быть 2, хотя бы 1 первый и хотя бы 1 второй.
wataru Автор
09.07.2023 07:13Так, тогда остется 3 варианта {5+3, 5+3+3, 5+5+3} Из ваших формул 3 никак нигде не получается.
BaLord
09.07.2023 07:13я может в формулах чуть ошибся был в туалете и мельком решал задачу, но она решается комбинаторикой.
wataru Автор
09.07.2023 07:13Нет. Вообще нет. Еще раз: перемножая какие-то два числа, вы считаете, что вы можете брать какое-то количество первого товара и какое-то количество второго товара независимо. Но у вас один кошелек, из которого вы платите за оба товара. Взяв сколько-то первого товара, вы уменьшаете оставшиеся деньги на второй товар. Вот так перемножать можно было бы, если бы у вас было 2 разных бюджета на каждый товар по отдельности.
MegaMANGO
09.07.2023 07:13+1Очень интересная и полезная (как минимум для меня) статья. Большое спасибо за пищу для размышлений ????
HappyLynx
09.07.2023 07:13+1Если существует аналитическое решение для суммы конечного ряда остатков от деления, то я, скорее всего, смогу предложить решение представленной задачи за О(1).
Нормализуем значения относительно цены более дешевого из товаров:
n = total / min(cost1, cost2)
c1 = 1
c2 = max(cost1, cost2) / min(cost1, cost2)
Решение задачи с total = n, cost1 = c1, cost2 = c2 эквивалентно решению исходной задачи.
Разобъем множество всех комбинаций товаров, удовлетворяющих условию задачи, следующим образом: сначала рассмотрим все комбинации, которые дают сумму, лежащую на полуинтервале (n-1;n], затем (n-2;n-1], затем (n-3;n-2] и т.д. (n-k;n-k+1] пока n-k+1 >= c1 (т.е. n-k+1 >= 1). Объединение всех этих комбинаций плюс пустая комбинация дадут нам исходное множество. Осталось их посчитать.
Количество комбинаций, сумма товаров в которых лежит на отрезке (x-1;x], равно одной комбинации исключительно из товаров c1 плюс floor(x / c2) комбинаций, включающих товары c2. Код на Java за О(n) исключительно для проверки утверждений выше (без нормализации, т.к. ошибки округления валят тесты)
int c1 = Math.min(cost1, cost2); int c2 = Math.max(cost1, cost2); long sum = 1; for (int i = total; i >= c1; i -= c1) { sum += 1 + (long) Math.floor(((double) i) / c2); } return sum;
Суть в том, что теперь мы можем эту сумму с округлениями вниз, которую считает цикл, представить как разнсть сумм без округлений: sum(1 + x / c2) - sum(x mod c2). Значение первой легко вычислимо за О(1), потому что существует аналитическое решение для суммы арифметической прогрессии. А вот существует ли аналитическое решение для второй суммы остатков от деления, я не знаю, к сожалению.
Итого, если кто-то знает аналитическое решение для суммы по k от 1 до n выражений ((k*a + b) mod c), буду благодарен подсказке.
wataru Автор
09.07.2023 07:13Нормализуем значения относительно цены более дешевого из товаров:
Так делать нельзя. Вот были цены 3 и 7. Вы поменяли их на 1 и 2. Но оно не эквивалентно, потому что в изначальной задаче 3 вторых товара позволяли купить 7 первого. А в новой задаче — только 6.
без нормализации, т.к. ошибки округления валят тесты
Это не ошибки округления, а именно неправильность такой "нормализации". Можно сократить GCD(c1, c2), да. Но не min(c1, c2).
А так сумма модулей тоже считается за логарифм, через сумму округлений вниз из статьи. Но за O(1) формулы, похоже, нет. В модулях тоже можно выбрасывать полные круги, но там все очень нерегулярно. Эта сумма состоит из кусков арифметической прогрессии, динами total/c1, округленной то вниз, то вверх. В зависимости от того, на каком остатке начинается эта прогрессия там будет на одно слагаемое больше. При чем каждое следующее начало сдвигается на total%c влево. Возможно там тоже можно вывести рекуррентную формулу уже для шага в total%c1 и модуля c1, просто аккуратно все нарисовав на бумаге — но там очень много деталей. Я не смог. Но в любом случае, эту рекурентную формулу можно вывести через функцию SumFloor из статьи, переведя сумму модулей в сумму округлений и обратно.
HappyLynx
09.07.2023 07:13Так делать нельзя. Вот были цены 3 и 7. Вы поменяли их на 1 и 2. Но оно не эквивалентно, потому что в изначальной задаче 3 вторых товара позволяли купить 7 первого. А в новой задаче — только 6.
Еще раз перечитайте формулы нормализации, что я привел, c2 и n не заявлены мной, как целые числа. На вашем примере:
cost1 = 3, cost2 = 7, total = 42
c1 = 1, c2 = 7/3, n = 42/3
Это не ошибки округления
Это именно ошибки округления, т.к. я переписывал этот код на BigDecimal с описанной нормализацией и он продолжал работать (в отличие от double), но выглядел существенно менее наглядно.
wataru Автор
09.07.2023 07:13По формулам не понятно было. Ну да, если к нецелым числам переходить, то, конечно, делить можно. Но зачем? У вас в сумме все-равно эти числа друг на друга делятся, так что смысла в этом особо нет.
Imp5
Линейное решение тоже может быть быстрым.
wataru Автор
Ну это потому что там тесты мелкие — очень мало больших чисел. Они же там рассчитывали на линейное решение. А на мелких числах, да, логарифмическое решение делает сильно больше арифметических операций.