Здравствуйте, сегодня я хотел бы рассказать об одном способе как можно упростить вычисления с длинной арифметикой. Допустим вам в задаче нужно посчитать некоторую величину, длину которой вы можете оценить сверху величиной len. Но возможно, чтобы посчитать эту величину требуется длинное умножение, сложение, вычитание, или еще какие-нибудь другие не простые операции. Предположим что ваша программа работает за T * len, тогда можно заменить это решение на другое, почти не использующее длинной арифметики и работающее за T * len / 9 + (len / 9) ^ 2. Чтобы сделать это воспользуемся Китайской теоремой об остатках. Выберем len / 9 некоторых различных простых чисел порядка 1000000000, и посчитаем ответ на нашу задачу по модулю каждого из данных простых за T * len / 9. Китайская Теорема об остатках утверждает, что существует единственное число меньшее чем произведение этих простых, у которого ровно такие остатки. Восстановить такое число не трудно, будем находить такое число по очереди для каждого префикса простых. На первом шаге наш ответ просто равен остатку. Далее на очередном шаге у нас есть некоторое число answer для предыдущего префикса и есть очередной остаток a и очередное простое число p[i]. Тогда достаточно решить уравнение: answer + x * p[0] * p[1] * ... * p[i — 1] = a(mod p[i]), такое x нетрудно найти, например, обратив p[0] * p[1] ... * p[i — 1] по модулю p[i]. Тогда новый answer будет равен answer + x * p[0] * p[1] * ... * p[i — 1]. Таким образом вместо всей длинной арифметики у нас осталось лишь умножение длинного числа на короткое. Более того этот способ может помочь если у вас были проблемы с памятью, так как если бы у вас раньше было памяти M * len, то сейчас можно обойтись M + len / 9. Сам я использовал этот способ в некоторых задачах и он действительно помогал мне избежать некоторых проблем. Ссылки на задачи: http://acm.timus.ru/problem.aspx?space=1&num=1467 http://acm.timus.ru/problem.aspx?space=1&num=1677
Звучит неплохо. Особенно часть про память.
Про время работы — в формальной оценке непонятно, откуда взялась константа 9. Если имеется в виду, что стандартная реализация длинной арифметики на соревновании держит по одной десятичной цифре в int32, то обычно это не так: можно хранить целых 9 цифр, код усложняется не сильно.
С другой стороны, код с умножением работал за T·len2, или (с Фурье) за , а станет за T·len без логарифма, так что ускорение действительно получится.
А есть какие-нибудь способы восстановить число по КТО за ? Или предпосылки к тому, что такого нет и быть не может?
Не понял вопроса, разве мое восстановление работает не за o(len^2)?
Да вроде за Ω(len2)...
«Разделяй и властвуй» плюс Фурье дает , если игнорировать необходимость вычислять обратные по модулю, для которых субквадратичные алгоритмы тоже вроде как существуют: Binary Recursive GCD.
Какие-то странные примеры. Я посмотрел свои решения, так вот в 1467 вообще нет длинной арифметики, а в 1677 и без КТО хватает умножения на короткое.
Я привел примеры где я лично использовал эту идею, очевидно можно обойтись и без неё, в 1677 у меня были проблемы как раз с памятью, а в 1467 я писал решение которое использовало длинную арифметику.
Только делить надо аккуратно. Например, если вам случайно захотелось поделить на ноль по модулю, то будут серьёзные проблемы (скажем, если знаменатель получился как сумма двух промежуточных результатов и оказался равен простому числу). Если есть только умножения и деления, то это лечится хранением степени модуля отдельно (т.е. x = mk·y хранится как — такие пары всегда можно умножать и делить, если делится).
У меня вроде происходит деление какого-то числа(возможно нулевого) на (p[0] * p[1] * ... * p[i — 1]) по модулю p[i], но произведение простых не может делиться на другое простое, или я что-то понял не так?
Вот было у вас число 3 хранилось как (3,3,3,3,3,...)
Теперь умножьте на p[0], будет (0, 3p[0]%p[1], 3p[0]%p[2], ...)
Теперь поделите на p[0]
Если бы вы это делали в длинке, получили бы 3
А можно конкретный пример, если не трудно, на котором мой алгоритм не работает? Если answer = 3, мой алгоритм это находит, на каждом шаге он решает уравнение 3 + x * p[0] * ... * p[i — 1] = 3(mod p[i]) <=> x * p[0] * p[1] * ... * p[i — 1] = 0(mod p[i]) <=> x = 0 <=> new_answer = answer + 0 * p[0] * ... * p[i — 1] = answer.
Еще раз, я не про вычисление по КТО в конце, я про то, что происходит при вычислениях по модулю.
В моем примере (я не понимаю куда конкретнее) (Берем 3, умножаем на p0, делим на p0) вы будете делить 0 на 0 по модулю p0
А, все понял, я просто думал, что проблема в последней части, да с этим действительно надо быть аккуратным.
Может быть можно набрать модулей чуть с запасом, и потом восстанавливать ответ по тем, по которым не было деления на 0?
При этом, видимо, стоит модули случайными брать, чтобы тест явным образом не крафтился
Не совсем понятно что вы и AlexDmitriev имеете в виду. Если есть в вычислениях деление, то в общем случае нельзя производить вычисления только с остатками, и не только если деление на сам модуль.
Можно, если делим на что-то, взаимно простое с модулем. Просто это надо воспринимать не как "деление", а как "домножение на обратный элемент", который много когда существует и единственен. Статья на e-maxx.
Понял. Но, похоже, это возможно только если деление без остатка.
Да, именно так.
Можно поподробнее про то, как осуществляется деление в этом представлении?
умножение на обратное
Это целочисленное деление по модулю, а как делать деление с остатком: нахождение модуля частного и остатка от деления?
Никак, остатка от деления на одно число не достаточно для того, чтобы что-то сказать об остатке/неполном частном при делении на другое
Это работает и КТО эффективнее обычной реализации? Совершенно не понял про выигрыш по памяти относительно длинной арифметики. Вы сравниваете с арифметикой, в которой десятичный разряд хранится в 4 байтах? Почему бы просто 9 десятичных разрядов не хранить в 4 байтах?
Кстати говоря, почему бы не считать в неизбыточных представлениях? Там делить не надо при умножении. Переводить технически квадрат, но на практике очень быстро.