Эффективная факторизация простых чисел для больших чисел

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

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

Я заметил несколько алгоритмов вроде Сито Эратосфена и создания списка всех простых чисел до вычисления . Мне просто интересно, может ли кто-нибудь уточнить это. Например, у меня возникли проблемы с пониманием того, как внедрить «Сито Эратосфена» в мою программу, и может ли это вообще быть хорошей идеей. Все без исключения указания о том, как лучше подойти к этому, были бы действительно полезны!

Вот мой код:

#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

using namespace std;
using namespace std::chrono;

vector<thread> threads;
vector<long long> inputVector;
bool developer = false; 
vector<unsigned long long> factor_base;
vector<long long> primeVector;

class PrimeNumber
{
    long long initValue;        // the number being prime factored
    vector<long long> factors;  // all of the factor values
public:
    void setInitValue(long long n)
    {
        initValue = n;
    }
    void addToVector(long long m)
    {
        factors.push_back(m);
    }
    void setVector(vector<long long> m)
    {
        factors = m;
    }
    long long getInitValue()
    {
        return initValue;
    }
    vector<long long> getVector()
    {
        return factors;
    }
};

vector<PrimeNumber> primes;

// find primes recursively and have them returned in vectors
vector<long long> getPrimes(long long n, vector<long long> vec)
{
    double sqrt_of_n = sqrt(n);

    for (int i = 2; i <= sqrt_of_n; i++)
    {
        if (n % i == 0) 
        {
            return vec.push_back(i), getPrimes(n / i, vec); //cause recursion
        }
    }

    // pick up the last prime factorization number
    vec.push_back(n);

    //return the finished vector
    return vec;
}

void getUserInput()
{
    long long input = -1;
    cout << "Enter all of the numbers to find their prime factors. Enter 0 to compute" << endl;
    do
    {
        cin >> input;
        if (input == 0)
        {
            break;
        }
        inputVector.push_back(input);
    } while (input != 0);
}

int main() 
{

    vector<long long> temp1;   // empty vector
    vector<long long> result1; // temp vector

    if (developer == false)
    {
        getUserInput();
    }
    else
    {
        cout << "developer mode active" << endl;
        long long a1 = 771895004973090566;
        long long b1 = 788380500764597944;
        long long a2 = 100020000004324000;
        long long b2 = 200023423420000000;
        inputVector.push_back(a1);
        inputVector.push_back(b2);
        inputVector.push_back(b1);
        inputVector.push_back(a2);
    }

    high_resolution_clock::time_point time1 = high_resolution_clock::now();

    // give each thread a number to comput within the recursive function
    for (int i = 0; i < inputVector.size(); i++)
    {   
        PrimeNumber prime;
        prime.setInitValue(inputVector.at(i));
        threads.push_back(thread([&]{
            prime.setVector(result1 = getPrimes(inputVector.at(i), temp1));
            primes.push_back(prime);
        }));
    }

    // allow all of the threads to join back together.
    for (auto& th : threads)
    {
        cout << th.get_id() << endl;
        th.join();
    }

    high_resolution_clock::time_point time2 = high_resolution_clock::now();

    // print all of the information
    for (int i = 0; i < primes.size(); i++)
    {
        vector<long long> temp = primes.at(i).getVector();

        for (int m = 0; m < temp.size(); m++)
        {
            cout << temp.at(m) << " ";
        }
        cout << endl;
    }

    cout << endl;

    // so the running time
    auto duration = duration_cast<microseconds>(time2 - time1).count();

    cout << "Duration: " << (duration / 1000000.0) << endl;

    return 0;
}

person booky99    schedule 13.10.2014    source источник
comment
Этот вопрос лучше подходит для проверки кода.   -  person Cory Kramer    schedule 13.10.2014
comment
о, черт возьми, ха-ха, даже не знал, что у них есть раздел для этого. Благодарность!   -  person booky99    schedule 13.10.2014
comment
возможный дубликат проблем с простыми числами   -  person Sam Harwell    schedule 13.10.2014
comment
Хорошо, что вы перестаете искать факторы, когда достигаете sqrt (n), но не так хорошо, что при рекурсии вы начинаете снова с 2. Если никакое число меньше i не является множителем n, то не меньше i. также имеет коэффициент n/i, и нет необходимости проверять все это снова. В частности, если i является наименьшим множителем n и i больше, чем кубический корень n, тогда i и n/i являются единственными множителями n. Однако нет необходимости выполнять эту конкретную проверку; он будет автоматическим, если вы начнете следующий поиск с i.   -  person rici    schedule 13.10.2014
comment
Хм. Ваш первый тестовый номер a1 = 771895004973090566 можно разложить на множители менее чем за 1/2000 секунды (или лучше), потому что он равен 2 x 385947502486545283. Фактор 2, конечно, определяется мгновенно. Затем число 385947502486545283 легко определяется как простое с помощью Миллера – Рабина. Точно так же a2 = 788380500764597944 можно практически мгновенно разложить на множители до 2 x 2 x 2 x 7 x 14078223227939249. Задача состоит в том, чтобы разложить на множители такие жесткие полупериоды, как 18436839306515468081 = 2988873347 x 6168491323, и для этого вам нужны факторизация квадратной формы Шанкса, однострочная факторизация Hart, или Брент – Поллард Ро.   -  person Todd Lehman    schedule 06.06.2015


Ответы (6)


Пробное деление подходит только для факторинга небольших чисел. Для n до 2 ^ 64 вам понадобится лучший алгоритм: я рекомендую начать с факторизации колеса, чтобы получить малые множители, а затем алгоритм rho Полларда, чтобы получить остальное. Если пробное деление - O (sqrt (n)), rho - O (sqrt (sqrt (n))), так что это намного быстрее. Для 2 ^ 64 sqrt (n) = 2 ^ 32, но sqrt (sqrt (n)) = 2 ^ 16, что является огромным улучшением. Вы должны рассчитывать на факторизацию ваших чисел максимум за несколько миллисекунд.

У меня нет кода C ++ для факторинга, но у меня есть читаемый код Python. Дайте мне знать, если вы хотите, чтобы я опубликовал это. Если вы хотите узнать больше о факторизации колес и алгоритме rho, у меня есть много информации о простых числах в моем блоге.

person user448810    schedule 13.10.2014
comment
Хотя я согласен с этим ответом (+1), Поллард ро работает в ожидаемое время O (sqrt (p)), где p является основным фактором. Так почему бы просто не использовать Pollard rho на всем пути, чтобы упростить задачу? Он очень быстро найдет малые простые множители, поэтому, вероятно, не будет заметного замедления. Кроме того, если кому-то требуется гарантированное время работы, Pollard rho дает только ожидаемое время работы, и, что еще хуже, ожидаемое время основывается на предположении, что псевдослучайный полином по модулю n будет вести себя достаточно как случайная последовательность, чтобы случайное предположение было действительным для вычисления ожидаемое время работы. - person user2566092; 13.10.2014
comment
Чтобы использовать Pollard rho, вам нужно как минимум удалить множители 2. Pollard rho может дать составные множители, поэтому вам нужно проверить простоту, которая замедляет работу, а пробное деление позволяет вам получить маленькие простые множители без необходимости проверки на простоту. Я согласен с вами, что точка перехода от пробного деления к rho невысока; Обычно я использую колеса 2,3,5 до 10000, а затем переключаюсь на rho. - person user448810; 13.10.2014

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

Итак, я рекомендую разбить решето на сегменты, которые поместятся в кеш L3. Кроме того, если вы исключите из битового вектора числа, кратные 2, 3 и 5, тогда 8-битный байт может представлять 30 чисел в числовой строке, с 1 битом для каждого числа, которое равно 1, 7, 11, 13, 17, 19. , 23 или 29 по модулю 30 - так что битовая карта для простых чисел до 10 ^ 9 занимает ~ 32 МБ - 10 ^ 9 / (30 * 1024 * 1024). Это почти половина размера битовой карты, которая просто исключает кратные 2, что составляет ~ 60 МБ - 10 ^ 9 / (2 * 8 * 1024 * 1024).

Очевидно, что для запуска сита до 10 ^ 9 вам понадобятся простые числа до sqrt (10 ^ 9), что требует около 1055 байтов, из которых вы можете сгенерировать любую часть полного сита до 10 ^ 9.

FWIW, результаты, которые я получаю на скромном AMD Phenom II x6 1090T (кэш L3 8 МБ), для простых чисел до 10 ^ 9 следующие:

  1. 1 core,   1 segment    3.260 seconds elapsed
  2. 5 cores,  1 segment    1.830 seconds elapsed
  3. 1 core,   8 segments   1.800 seconds elapsed
  4. 5 cores, 40 segments   0.370 seconds elapsed

где под сегментом я подразумеваю часть сита. В этом случае размер сита составляет ~ 32 МБ, поэтому при наличии нескольких сегментов они одновременно используют около 4 МБ кеш-памяти третьего уровня.

Это время включает время, необходимое для сканирования заполненного сита и генерации всех простых чисел в виде массива целых чисел. Это занимает около 0,5 секунды процессора! Таким образом, для запуска сита без фактического извлечения из него простых чисел требуется 0,270 секунды в случае (4) выше.

FWIW, я получаю небольшое улучшение - до 0,240 секунды в случае (4) - за счет инициализации каждого сегмента с использованием предварительно вычисленного шаблона, который удаляет кратные 7, 11, 13 и 17. Этот шаблон составляет 17 017 байт.

Понятно, что для однократной факторизации за 0,06 секунды ... вам нужно, чтобы сито было предварительно вычислено!

person Community    schedule 14.10.2014
comment
Блестящая упаковка простых чисел по модулю 30 (без 2,3,5) в один байт. Вы это придумали? Очень хорошо. - person Todd Lehman; 06.06.2015

for(int i  = 2; i * i <= n; ++i) //no sqrt, please
{
    while(n%i == 0) //while, not if
    {
         factors.push_back(i);
         n/=i;
    }
}
if(n != 1)
{
    factors.push_back(n);
}

По сути, это более изящная реализация вашего алгоритма. Его сложность равна sqrt N. Он будет работать довольно быстро даже для 18-значного числа, но только если все простые множители малы. Если это произведение двух больших простых чисел или, что еще хуже, само простое число, это будет длиться примерно 10 секунд.

person Armen Tsirunyan    schedule 13.10.2014
comment
Быстрее вычислить sqrt один раз, чем повторно i*i. - person Mark Ransom; 13.10.2014
comment
@MarkRansom Лучше взять i * i, чем использовать sqrt, поскольку умножение происходит быстрее, чем извлечение квадратного корня. - person ABcDexter; 10.05.2015
comment
@ABcDexter это зависит. Если вам нужно сделать sqrt только один раз, но вам нужно сделать i*i 1000 раз, sqrt может быть быстрее. - person Mark Ransom; 10.05.2015
comment
@MarkRansom Не могли бы вы объяснить этот комментарий, пожалуйста? Чем они отличаются? Если вы возьмете sqrt (n) и проверите с помощью i - или если вы возьмете i ^ 2 и проверите с помощью n ... Математически это эквивалентно, я ошибаюсь? - person ; 21.11.2016
comment
@BeTa да они эквивалентны, в том-то и дело. Вопрос в том, что быстрее. Если вы посмотрите на построение цикла, i*i необходимо пересчитывать каждый раз, когда i изменяется, что является каждой итерацией цикла. С другой стороны, sqrt(n) нужно вычислять только один раз. Несмотря на то, что вычисление sqrt(n) медленнее, чем i*i, с учетом всех повторений sqrt в целом будет быстрее. - person Mark Ransom; 21.11.2016
comment
Очень аккуратный. Спасибо. - person Pangur; 24.09.2018

Простого ускорения в два раза можно легко добиться, изменив свой цикл:

if (n % 2) {
    return vec.push_back(i), getPrimes(n / i, vec);
}

for (int i = 3; i <= sqrt_of_n; i += 2)
{
    if (n % i == 0) 
    {
        return vec.push_back(i), getPrimes(n / i, vec); //cause recursion
    }
}

Сначала вы должны проверить число на два. Затем, начиная с 3, вы снова тестируете, увеличивая цикл на два за раз. Вы уже знаете, что 4, 6, 8, ... четные числа и имеют 2 в качестве множителя. Проверяя четные числа, вы вдвое уменьшаете сложность.

Чтобы разложить число N на множители, вам нужны только простые числа ‹= sqrt (N). Для 18-значного числа вам нужно только проверить все простые числа меньше 1e9, а поскольку имеется 98 миллионов простых чисел меньше чем 2e9, вы можете легко хранить 100 миллионов чисел на современных компьютерах и параллельно проводить факторинг. Если каждое число занимает 8 байтов ОЗУ (int64_t), 100 миллионов простых чисел потребуют 800 МБ памяти. Этот алгоритм является классическим решением проблемы № 2 SPOJ, Prime Generator.

Лучший способ составить список всех малых простых чисел, которые могут уместиться в 32-битном int, - это построить Sieve of Eratostenes. Я сказал вам, что нам нужны простые числа меньше sqrt (N), чтобы разложить на множители любое N, поэтому для разложения 64-битных целых чисел вам нужны все простые числа, которые подходят как 32-битное число.

person vz0    schedule 13.10.2014
comment
Вы предлагаете, чтобы я сгенерировал все простые числа меньше 2e9, прежде чем проверять, являются ли они простыми? - person booky99; 13.10.2014
comment
да. Сначала создайте или получите список всех простых чисел меньше 1e9. Затем используйте этот список простых чисел, чтобы разложить на множители каждое из ваших чисел. Для каждого простого P-теста, если он делит ваше N, сохраните результат где-нибудь и продолжайте снова с N '= N / P. Вы находитесь на пределе возможностей сегодняшних компьютеров, но я верю, что это может сработать. - person vz0; 13.10.2014
comment
Этот алгоритм способен давать правильные результаты, но это все равно, что предлагать кому-то использовать пузырьковую сортировку для сортировки списка. Если не считать определенных сценариев обучения, это просто неподходящий инструмент для работы. - person Sam Harwell; 13.10.2014
comment
@SamHarwell, ты ошибаешься. Прочтите исходный код OP о том, как он проверяет, является ли число простым. Он переходит от 2 к sqrt (N), что означает, что его bigoh - это O (sqrt (N)), а мой алгоритм решения переходит к sqrt (N), но только для числа простых чисел от 2 до sqrt (N), которое меньше, чем обычно. У нас есть ~ 50 миллионов простых чисел от 2 до sqrt (N), поэтому мой bigoh равен O (1/20 sqrt (N)). - person vz0; 13.10.2014
comment
O (1/20 sqrt (N)) = O (sqrt (N)). - person Yves Daoust; 13.10.2014
comment
Интересно, что для хранения списка простых чисел требуется гораздо больше памяти, чем для битовой маски, которая указывает на простоту каждого числа в диапазоне. - person Mark Ransom; 13.10.2014
comment
@ vz0 рекурсия увеличивает bigO цикла. - person Mark Ransom; 13.10.2014
comment
@YvesDaoust Да, конечно;) Но OP интересуется временем работы настенных часов. Он измеряет свою эффективность секундами, поэтому важны константы. - person vz0; 13.10.2014
comment
@MarkRansom Вы правы! Битовая маска на все 32-битные числа занимает ~ 536 МБ. Это также был бы неплохой трюк со скоростью, не только с использованием меньшего количества памяти, но и более быстрого для работы с местностью. Но опять же, нам нужно точно измерить выигрыш. - person vz0; 13.10.2014
comment
@ vz0: вот почему нотация bigO не подходит. - person Yves Daoust; 13.10.2014
comment
да, имея список простых чисел, это ускорение в 20 раз (то есть 10 секунд - ›0,5 секунды). Но для вычисления простых чисел потребуется 1-2 секунды с помощью достаточно простого кода (например, этот код занимает 10 secs на ideone, и он даже не сегментирован - хотя это только по размеру). Так что для 0,06 раза пробное деление и сито Эратосфена никуда не годятся. - person Will Ness; 13.10.2014
comment
Запуск Sieve of Eratosthenes на современном процессоре увеличивает объем кеш-памяти. Уловка состоит в том, чтобы разбить решето на куски, каждый из которых поместится в кеш L3 (оставляя место для других процессоров). Кроме того, если вы исключите кратные 2, 3 и 5 из битового вектора, тогда 8-битный байт может представлять кратные 1, 7, 11, 13, 17, 19, 23 и 29, так что битовая карта для простых чисел до 10 ^ 9 занимает ~ 32 МБ. - person ; 14.10.2014
comment
Извините ... не кратные 1, 7 и т. Д., А числа, которые равны 1, 7, 11, 13, 17, 19, 23 и 29 по модулю 30, поэтому каждый байт представляет 30 чисел в числовой строке, исключая все кратные 2, 3 и 5. Очевидно, что для запуска сита до 10 ^ 9 вам понадобятся простые числа до sqrt (10 ^ 9), что требует около 1055 байтов, из которых вы можете сгенерировать любую часть полного сита. до 10 ^ 9. Используя 5 ядер и ограничивая каждый сегментом сита ~ 800 Кбайт, я получаю простые числа до 10 ^ 9 за ~ 0,25 секунды прошедшего времени для сита. Итак, чтобы выполнить факторизацию за 0,06 секунды ... вам нужно, чтобы сито было предварительно вычислено! - person ; 14.10.2014
comment
@gmch Вы должны окончательно дать новый ответ на этот вопрос! - person vz0; 14.10.2014
comment
Хорошо ... Я расширил то, что нашел, играя с Решетом Эратосфена, в ответ. - person ; 14.10.2014

Алгоритм:

  1. Разделите число на 2, пока оно не станет на него делимым, сохраните результат и отобразите его.
  2. Разделите число на 3, пока оно не станет делимым на 3, и отобразите результат,
  3. Повторите тот же процесс для 5,7 ... и т. Д. До получения квадратного корня из n.
  4. Если в конце результирующее число является простым, отобразите его количество как 1.

    int main() {
    long long n;
    cin >> n;
    int count =0 ;
    while(!(n%2)){
        n = n / 2;
        count++;
    }
    if(count > 0) {
        cout<<"2^"<<count<<" ";
    }
    for(long long i=3;i<=sqrt(n) ; i+=2){
        count=0;
        while(n%i == 0){
            count++;
            n = n/i;
        }
        if(count){
            cout << i <<"^" <<count<<" ";
        }
    
    }
    if(n>2){
        cout<<n <<"^1";
    }
    
    
    
    return 0;
    }
    

Вход: 100000000 Выход 2 ^ 8 5 ^ 8

person anonymous    schedule 20.06.2018

Алгоритм факторизации целых чисел настолько прост, что его можно реализовать даже на счетах.

void start()
{ 
   int a=4252361;    //  integer to factorize
   int b=1,  c,  d=a-1;  

   while ((a > b) && (d != b))
  {
    if (d > b)
     {
       c=c+b;
       a=a-1;
       d=a-c-1;
     }
    if (d < b)
     {
       c=b-d;
       b=b+1;
       a=a-1;
       d=a-c-1;
     }         
  }
    if ((d == b)&&(a > b)) 
  Alert ("a = ",  a-1,  " * ", b+1); 

    if ((d < b)&&(a <= b)) 
  Alert ("a  is a prime");

  return;
}

Алгоритм составлен мной, Миляевым Виктором Константиновичем, 26 июля 1950 года рождения. Написан на языке MQL4 15.12. 2017. Электронная почта: [email protected]

person Viktor Miliaev    schedule 22.12.2017
comment
Из-за этого мои глаза кровоточат, когда я пытаюсь прочитать это. Можете ли вы использовать какие-нибудь значимые имена переменных вместо a, b, c и d? Или добавьте комментарии о том, что должно произойти в определенных частях. Это похоже на чтение миниатюрного кода. - person Lajos Mészáros; 17.03.2018
comment
Похоже, что в приведенном выше коде есть небольшая ошибка: c необходимо инициализировать нулем перед входом в цикл, иначе выражение c = c + b не определено. Когда я меняю код для инициализации c = 0, код ведет себя так, как ожидалось. Очень крутой маленький алгоритм! Это, конечно, не самый быстрый способ разложить число на множители (похоже, это время выполнения O (n)), но довольно интересно, что он может найти множитель, используя только сложение, вычитание и сравнения (без операций умножения, деления или модуля) . - person Todd Lehman; 14.07.2019
comment
Этот забавный фрагмент кода предполагает a >= 3. Он отвечает на вопрос о простом или составном, но не полностью разлагается на простые множители, если число имеет 3 или более простых множителя. например a=8 отображается как 4*2 - person Jesse Chisholm; 05.07.2020
comment
@ToddLehman re: appearance of O(n) Я вижу, что a отсчитывает от n до 1, но он идет только до sqrt (n), пока b поднимается до sqrt (n). Так будет ли это O(n - sqrt(n)) - и технически это отличается от O(n)? Обозначение большого O неестественно в моей голове. - person Jesse Chisholm; 06.07.2020