Самый быстрый алгоритм для нахождения множителей 2 и 5

Я прочитал этот вопрос: Какой самый быстрый алгоритм для поиска простые числа?, но я хотел бы сделать это только для простых чисел 2 и 5.

Например, число 42000 факторизуется как:

24 • 3153 • 71< / суп>

Меня интересует только нахождение сил 2 и 5: 4 и 3 в этом примере.

Мой наивный подход состоит в том, чтобы последовательно делить на 2, пока остаток равен 0, затем последовательно делить на 5, пока остаток равен 0.

Количество удачных делений (с нулевым остатком) равно степеням 2 и 5.

Это включает в себя выполнение (x + y + 2) делений, где x — степень числа 2, а y — степень числа 5.

Есть ли более быстрый алгоритм для нахождения степеней чисел 2 и 5?


person BenMorel    schedule 28.06.2015    source источник
comment
Вы можете сначала разделить на 4 или 8, а если это не удастся, попробуйте 2 или 2 и 4. Хотя это будет быстрее только для больших чисел. (идея, основанная на проблеме двух яиц — datagenetics.com/blog/july22012/index.html)   -  person shapiro yaacov    schedule 28.06.2015
comment
@ shapiro.yaacov Да, но это для библиотеки больших чисел, я бы тоже начал с 1048576, но в зависимости от размера числа невозможно узнать, будет ли это эффективно.   -  person BenMorel    schedule 28.06.2015
comment
Собираетесь ли вы получить какое-либо случайное число, или они генерируются и имеют более высокую вероятность. высших степеней для 2 и ``? Кроме того, что важнее: не тратить впустую проверки числа, которое не делится, или найти степень числа, которое делит?   -  person shapiro yaacov    schedule 28.06.2015
comment
Сколько битов вы используете для представления чисел? Если они действительно не велики, ваш текущий алгоритм прост и по существу оптимален: O(log n) для завершения. Я действительно сомневаюсь, что эта функция может быть узким местом в производительности.   -  person tucuxi    schedule 28.06.2015
comment
Это для библиотеки больших чисел общего назначения. Размер числа совершенно неизвестен и может быть огромным (сотни, тысячи цифр, без ограничений).   -  person BenMorel    schedule 28.06.2015
comment
@shapiro.yaacov Есть две важные вещи: найти степени 2 и 5, и знать, есть ли хотя бы один другой фактор (мне не нужно знать, какой именно, просто если есть любой).   -  person BenMorel    schedule 28.06.2015
comment
Тогда я думаю, что @tucuxi прав. Ваша идея в основном лучший путь.   -  person shapiro yaacov    schedule 28.06.2015
comment
@tucuxi: Когда вы говорите O (log n), вы имеете в виду, что n - это число, а не количество цифр, и вы предполагаете, что деление на 2 или 5 занимает постоянное время, а не растет с количеством цифр.   -  person Douglas Zare    schedule 29.06.2015
comment
@DouglasZare да - это верно для типичных целых чисел, до того, как OP пояснил, что библиотека предназначена для целых чисел произвольной длины. С такими целыми числами произвольной длины это действительно O (n ^ 2); где n = длина числа   -  person tucuxi    schedule 29.06.2015
comment
Каково представление вашего ввода и вывода? (OP, пожалуйста, не комментируйте этот комментарий: дополните вопрос.)   -  person greybeard    schedule 29.06.2015
comment
@greybeard Я не уверен, что вы имеете в виду под представлением ввода и вывода? Это целые числа произвольного размера, хранящиеся в виде строк цифр по основанию 10.   -  person BenMorel    schedule 29.06.2015
comment
[input and output are] arbitrary-size integers, stored as strings of digits in base 10 - именно то, что должно было быть указано с самого начала в самом вопросе. (Как кто-нибудь, кроме вас, может знать?)   -  person greybeard    schedule 29.06.2015
comment
На первый взгляд я не думал, что это будет иметь большое значение. Вернее, я не особо хотел акцентировать на этом внимание, так как хотя числа хранятся как строки в базе 10, все вычисления не выполняются над этими строками. Когда GMP доступен, они преобразуются в числа GMP, и GMP выполняет расчеты более низкого уровня. Так что в основном меня больше интересуют теоретические расчеты, которые могут привести к нахождению факторов с меньшим количеством шагов, независимо от деталей реализации.   -  person BenMorel    schedule 29.06.2015


Ответы (4)


После разговора я действительно думаю, что ваша идея — самый быстрый путь, за одним исключением:

Дивизия (в большинстве случаев) стоит дорого. С другой стороны, проверка последней цифры числа (обычно?) выполняется быстрее, поэтому перед делением я бы проверил последнюю цифру (0/5 и 0/2/4/6/8).

person shapiro yaacov    schedule 28.06.2015
comment
Вы можете легко проверить наличие концов в 2 в базе-2. Но ни один современный компьютер не любит хранить свои цифры в базе 5 или 10... - person tucuxi; 28.06.2015
comment
@tucuxi Несмотря на то, что то, что вы говорите, правда, на самом деле это имеет смысл в моем случае, когда моя библиотека написана на PHP, а число фактически хранится в виде строки в базе 10. Это действительно не самое эффективное, но это это то, что лучше всего работало в технических пределах языка. - person BenMorel; 28.06.2015

Я основываю это на этом комментарии ОП:

моя библиотека написана на PHP, и число на самом деле хранится в виде строки с основанием 10. Это действительно не самый эффективный способ, но это то, что лучше всего работает в рамках технических ограничений языка.

Если вы привержены strings-in-php, то следующий псевдокод ускорит работу по сравнению с фактическим повторяющимся модулем и делением общего назначения:

while the string ends in 0, but is not 0
  chop a zero off the end,
  increment ctr2 and ctr5
switch repeatedly depending on the last digit:
  if it is a 5,
     divide it by 5
     increment ctr5
  if it is 2, 4, 6, 8,
     divide it by 2
     increment ctr2
  otherwise
     you have finished

Это не требует никаких операций с модулем, и вы можете реализовать деление на 5 и деление на 2 дешевле, чем деление длинных чисел общего назначения.

С другой стороны, если вам нужна производительность, использование строковых представлений для целых чисел неограниченного размера равносильно самоубийству. Используйте gmpбиблиотекой php) для вашу математику и конвертировать в строки только при необходимости.

изменить:

вы можете повысить эффективность (и упростить свои операции), используя следующий псевдокод:

if the string is zero, terminate early
while the last non-zero character of the string is a '5',
   add the string to itself
   decrement ctr2
count the '0's at the end of the string into a ctr0
chop off ctr0 zeros from the string
ctr2 += ctr0
ctr5 += ctr0
while the last digit is 2, 4, 6, 8
   divide the string by 2
   increment ctr2

Вырезать сразу много нулей лучше, чем зацикливаться. А mul2 выигрывает у div5 по скорости (это можно реализовать, добавив число один раз).

person tucuxi    schedule 28.06.2015
comment
Это примерно то, что я делаю в данный момент, приятно видеть, что вы придумали похожее решение! Библиотека использует GMP или BCMath, если они доступны, но также может вернуться к собственному посимвольному расчету для наихудших сценариев. Вы можете найти его на GitHub: brick/math, если вам интересно! - person BenMorel; 28.06.2015
comment
Измените внутреннее представление native на массив целых чисел с основанием 10000 (если вам нравятся преимущества основания 10) или на 1e9 (если вам это нравится, но у вас 64-битные целые числа). Это должно ускорить ваши операции в 100–16 раз, особенно если вы решите реализовать такие вещи, как doMul и doDiv, используя то, как это делается в школе, и избежать karatsuba и других. - person tucuxi; 28.06.2015
comment
Это интересная идея для ускорения работы, хотя ускорение собственного калькулятора определенно не является приоритетом до сих пор: эта реализация действительно существует в качестве запасного варианта, чтобы гарантировать, что библиотека будет работать без проблем на любой установке PHP, но любой, у кого есть требования к вычислению Для больших чисел настоятельно рекомендуется установить расширение GMP или BCMath PHP! - person BenMorel; 28.06.2015

Я думаю, что ваш алгоритм будет самым быстрым. Но у меня есть пара предложений.

Одна альтернатива основана на наибольшем общем делителе. Возьмите gcd вашего входного числа с наименьшей степенью 2 больше, чем ваше входное число; это даст вам все множители 2. Разделите на gcd, затем повторите ту же операцию с 5; это даст вам все множители числа 5. Разделите снова на НОД, и остаток скажет вам, есть ли какие-либо другие множители.

Другая альтернатива основана на бинарном поиске. Разделите двоичное представление вашего входного числа пополам; если правая половина равна 0, двигайтесь влево, иначе двигайтесь вправо. Получив множители 2, разделите, а затем примените тот же алгоритм к остатку, используя степени 5.

Я оставлю вам реализовать и рассчитать время этих алгоритмов. Но мое внутреннее чувство состоит в том, что повторное деление будет трудно превзойти.

Я только что прочитал ваш комментарий о том, что ваш входной номер хранится в базе 10. В этом случае многократно делите на 10, пока остаток равен 0; это дает коэффициенты как 2, так и 5. Затем примените свой алгоритм к уменьшенному числу.

person user448810    schedule 28.06.2015
comment
Алгоритм на основе GCD интересен, но вам потребуется много умножений, чтобы построить начальную следующую наибольшую степень двойки и следующую наибольшую степень пяти. Вычисление НОД пары больших чисел также требует O(n^2) вычитаний, где n — длина наименьшего из пары. - person tucuxi; 29.06.2015
comment
Очень интересные подходы, спасибо! Действительно, вычисление НОД само по себе требует нескольких потенциально дорогостоящих операций, поэтому оно может быть не быстрее (не могу сказать точно: как вы говорите, его нужно реализовать и рассчитать по времени). Что касается базы 10, поскольку числа на самом деле хранятся в виде строк, я могу просто обрезать конечные нули один за другим. Только после этого разделите на 2, пока последняя цифра четная, затем на 5, пока последняя цифра 5. - person BenMorel; 29.06.2015
comment
@tucuxi: Re: следующая наибольшая степень .... Вычисление логарифма по основанию 2 может быть выполнено очень быстро, это просто количество битов в двоичном представлении числа, а затем это легко чтобы вычислить следующую наибольшую степень числа 2 как 1-бит, за которым следуют 0-биты. Вычисление логарифма по основанию 5 сложнее. Может быть, гибридный подход: gcd для степеней 2 и пробное деление для степеней 5. НОД может быть быстрее, чем вы думаете, учитывая, что одно из чисел имеет простую форму. Единственный способ узнать это реализовать и измерить. - person user448810; 29.06.2015
comment
@user448810 user448810, пожалуйста, предоставьте алгоритм вычисления следующей степени двойки для заданного числа в рамках ограничений числа — это строка в базе 10. В базе 2 это действительно тривиально; но я понимаю, что ОП делает все сложно. - person tucuxi; 29.06.2015
comment
@tucuxi: Инициализируйте x равным 1. Повторно удваивайте x, пока оно не станет больше введенного числа. Возьмите gcd x и ввод, чтобы получить коэффициенты 2. - person user448810; 29.06.2015
comment
@ user448810 этому алгоритму требуется O (n ^ 2), где n - количество цифр во входном числе, для генерации следующей по величине степени 2 (потому что добавление двух строк из n цифр равно O (n), и вам нужно сделать это O(n) раз). Нахождение НОД также составляет O(n^2) (O(n) шагов, с вычитанием n-значной строки в каждом) . Следовательно, с представлением OP этот алгоритм не может конкурировать с другими алгоритмами O (n ^ 2). - person tucuxi; 29.06.2015

Если у вас есть миллиардное число, вы не хотите делать на нем деления, если это действительно необходимо. Если у вас нет оснований полагать, что это 1/2 ^ 1000 чисел, делящихся на 2 ^ 1000, то имеет смысл использовать гораздо более быстрые тесты, которые рассматривают только последние несколько цифр. Вы можете определить, делится ли число на 2, взглянув на последнюю цифру, делится ли оно на 4, взглянув на последние 2 цифры, и на 2^n, взглянув на последние n цифр. Точно так же вы можете определить, делится ли число на 5, взглянув на последнюю цифру, делится ли оно на 25, взглянув на последние 2 цифры, и на 5^n, взглянув на последние n цифр.

Я предлагаю вам сначала подсчитать и удалить конечные 0, а затем решить по последней цифре, проверяете ли вы степени двойки (последняя цифра 2, 4, 6 или 8) или степени 5 (последняя цифра 5).

Если вы проверяете степень двойки, возьмите последние 2, 4, 8, 16,... 2^i цифры и умножьте их на 25, 625,... 5^2^i, считая конечные 0 до 2^i (но не более). Если вы получаете меньше 2 ^ i завершающих нулей, остановитесь.

Если вы проверяете степень числа 5, возьмите последние 2, 4, 8, 16,... 2^i цифры и умножьте их на 4, 16,... 2^2^i, считая конечные 0 до 2^i (но не более). Если вы получаете меньше 2 ^ i завершающих нулей, остановитесь.

Например, предположим, что число, которое вы анализируете, равно 283 795 456. Умножьте 56 на 25, вы получите 1400 с 2 конечными нулями, продолжайте. Умножьте 5 456 на 625, вы получите 3 410 000 с 4 нулями в конце, продолжайте. Умножьте 83 795 456 на 5 ^ 8 = 390 625, вы получите 32 732 600 000 000, в котором 8 завершающих нулей, продолжайте. Умножьте 283 795 456 на 5 ^ 16, чтобы получить 43 303 750 000 000 000 000, в котором всего 13 завершающих нулей. Это меньше 16, так что стоп, степень двойки в простой факторизации равна 2^13.

Я надеюсь, что для больших умножений вы реализуете алгоритм n log n для умножения n-значных чисел, но даже если это не так, этот метод должен превзойти все, что связано с делением типичных больших чисел.


Давайте посмотрим на среднюю временную сложность различных алгоритмов, предполагая, что каждое n-значное число равновероятно.

Сложение или вычитание двух n-значных чисел занимает тета(n) шагов.

Деление n-значного числа на небольшое число, например 5, требует тета(n) шагов. Деление по основанию равно O(1).

Деление n-значного числа на другое большое число требует шагов тета (n log n) с использованием БПФ или тета (n ^ 2) с помощью наивного алгоритма. То же верно и для умножения.

Алгоритм многократного деления числа с основанием 10 на 2 имеет среднюю временную сложность тета(n): для первого деления требуется тета(n) время, и в среднем вам нужно сделать только O(1) делений.

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

Проверка последней цифры на делимость на 2 или 5 перед выполнением более дорогостоящих расчетов — это хорошо, но это приводит только к постоянному улучшению коэффициента. Применение исходного алгоритма после этого по-прежнему требует в среднем тета(n) шагов.

Проверка последних d цифр на делимость на 2 ^ d или 5 ^ d занимает время O (d ^ 2), O (d log d) с помощью БПФ. Очень вероятно, что нам нужно сделать это только тогда, когда d мало. Доля n-значных чисел, делящихся на 2^d, равна 1/2^d. Таким образом, среднее время, затрачиваемое на эти проверки, равно O(sum(d^2 / 2^d)) и эта сумма ограничена и не зависит от n, поэтому в среднем требуется тета(1) время. Когда вы используете последние цифры для проверки на делимость, вам обычно не нужно выполнять какие-либо операции над цифрами, близкими к n.

person Douglas Zare    schedule 29.06.2015