Самый эффективный код для первых 10000 простых чисел? - PullRequest
54 голосов
/ 03 августа 2008

Я хочу напечатать первые 10000 простых чисел. Кто-нибудь может дать мне самый эффективный код для этого? Разъяснения:

  1. Неважно, если ваш код неэффективен для n> 10000.
  2. Размер кода не имеет значения.
  3. Вы не можете просто жестко закодировать значения любым способом.

Ответы [ 28 ]

3 голосов
/ 21 августа 2008

Сито Эратосфена - это путь, благодаря своей простоте и скорости. Моя реализация в C

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

int main(void)
{
    unsigned int lim, i, j;

    printf("Find primes upto: ");
    scanf("%d", &lim);
    lim += 1;
    bool *primes = calloc(lim, sizeof(bool));

    unsigned int sqrtlim = sqrt(lim);
    for (i = 2; i <= sqrtlim; i++)
        if (!primes[i])
            for (j = i * i; j < lim; j += i)
                primes[j] = true;

    printf("\nListing prime numbers between 2 and %d:\n\n", lim - 1);
    for (i = 2; i < lim; i++)
        if (!primes[i])
            printf("%d\n", i);

    return 0;
}

CPU Время нахождения простых чисел (на Pentium Dual Core E2140 1,6 ГГц, с использованием одноядерного процессора)

~ 4 с для лим. = 100 000 000

2 голосов
/ 19 апреля 2016

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

Основная идея, лежащая в основе алгоритма решетчатого сита, состоит в том, чтобы использовать небольшое скользящее сито, которое является достаточно большим, чтобы содержать хотя бы одно отдельное кратное число для каждого из «активных» простых множителей в настоящее время - то есть тех простых чисел, чье квадратное число не превышает наименьшее число в настоящее время представлено движущимся ситом. Другое отличие от SoE состоит в том, что сито deque сохраняет фактические факторы в ячейках композитов, а не логических значений.

Алгоритм расширяет размер окна сита по мере необходимости, что приводит к довольно равномерной производительности в широком диапазоне, пока сито не начнет заметно превышать емкость кэша L1 ЦП. Последнее простое число, которое подходит полностью, составляет 25 237 523 (1579 791-е простое число), что дает приблизительную приблизительную цифру для разумного рабочего диапазона алгоритма.

Алгоритм довольно прост и надежен, и его производительность даже в гораздо более широком диапазоне, чем у несегментированного сита Эратосфена. Последний работает намного быстрее, пока его сито полностью помещается в кэш, то есть до 2 ^ 16 для сита, рассчитанного только на шансы, с размерами в байтах. Затем его производительность падает все больше и больше, хотя он всегда остается значительно быстрее, чем deque, несмотря на помехи (по крайней мере, в скомпилированных языках, таких как C / C ++, Pascal или Java / C #).

Вот рендеринг алгоритма deque sieve в C #, потому что я нахожу этот язык - несмотря на многие его недостатки - гораздо более практичным для прототипирования алгоритмов и экспериментов, чем чрезвычайно громоздкий и педантичный C ++. (Sidenote: я использую бесплатную LINQPad , которая позволяет без проблем погружаться в настройку проектов, make-файлов, каталогов или еще много чего, и дает мне ту же степень интерактивности как приглашение Python).

C # не имеет явного типа deque, но обычный List<int> работает достаточно хорошо для демонстрации алгоритма.

Примечание: эта версия не использует deque для простых чисел, потому что просто не имеет смысла выталкивать sqrt (n) из n простых чисел. Что хорошего в том, чтобы удалить 100 простых чисел и оставить 9900? По крайней мере, таким образом все простые числа собраны в аккуратный вектор, готовый для дальнейшей обработки.

static List<int> deque_sieve (int n = 10000)
{
    Trace.Assert(n >= 3);

    var primes = new List<int>()  {  2, 3  };
    var sieve = new List<int>()  {  0, 0, 0  };

    for (int sieve_base = 5, current_prime_index = 1, current_prime_squared = 9; ; )
    {
        int base_factor = sieve[0];

        if (base_factor != 0)
        {
            // the sieve base has a non-trivial factor - put that factor back into circulation
            mark_next_unmarked_multiple(sieve, base_factor);
        }
        else if (sieve_base < current_prime_squared)  // no non-trivial factor -> found a non-composite
        {
            primes.Add(sieve_base);

            if (primes.Count == n)
                return primes;
        }
        else // sieve_base == current_prime_squared
        {
            // bring the current prime into circulation by injecting it into the sieve ...
            mark_next_unmarked_multiple(sieve, primes[current_prime_index]);

            // ... and elect a new current prime
            current_prime_squared = square(primes[++current_prime_index]);
        }

        // slide the sieve one step forward
        sieve.RemoveAt(0);  sieve_base += 2;
    }
}

Вот две вспомогательные функции:

static void mark_next_unmarked_multiple (List<int> sieve, int prime)
{
    int i = prime, e = sieve.Count;

    while (i < e && sieve[i] != 0)
        i += prime;

    for ( ; e <= i; ++e)  // no List<>.Resize()...
        sieve.Add(0);

    sieve[i] = prime;
}

static int square (int n)
{
    return n * n;
}

Вероятно, самый простой способ понять алгоритм - это представить его как специальное сегментированное сито Эратосфена с размером сегмента 1, сопровождаемое областью переполнения, где простые числа останавливаются, когда они пересекают конец сегмента. За исключением того, что одна ячейка сегмента (a.k.a. sieve[0]) уже была просеяна, когда мы до нее добрались, потому что она переехала, когда была частью области переполнения.

Число, представленное sieve[0], содержится в sieve_base, хотя sieve_front или window_base также могут быть хорошими именами, которые позволяют проводить параллели с кодом Бена или реализациями сегментированных / оконных сит.

Если sieve[0] содержит ненулевое значение, то это значение имеет коэффициент sieve_base, который, таким образом, может быть распознан как составной. Поскольку ячейка 0 кратна этому коэффициенту, легко вычислить ее следующий переход, который просто равен 0 плюс этот коэффициент. Если эта ячейка уже занята другим фактором, мы просто добавляем фактор снова и так далее, пока не найдем множитель фактора, где в данный момент нет другого фактора (расширяя сито при необходимости). Это также означает, что нет необходимости хранить текущие рабочие смещения различных простых чисел от одного сегмента к другому, как в обычном сегментированном сите. Всякий раз, когда мы находим коэффициент в sieve[0], его текущее рабочее смещение равно 0.

Текущее простое число вступает в игру следующим образом. Простое может стать текущим только после своего вхождения в поток (то есть, когда оно было обнаружено как простое, потому что не помечено каким-либо фактором), и оно будет оставаться текущим до того момента, когда sieve[0] достигнет своего квадрата. Все более низкие кратные этого простого числа должны были быть исключены из-за действий меньших простых чисел, как в обычном SoE. Но ни одно из меньших простых чисел не может оторваться от квадрата, поскольку единственным фактором квадрата является само простое число, и на данный момент он еще не находится в обращении. Это объясняет действия, предпринятые алгоритмом в случае sieve_base == current_prime_squared (что, кстати, подразумевает sieve[0] == 0).

Теперь случай sieve[0] == 0 && sieve_base < current_prime_squared легко объясним: это означает, что sieve_base не может быть кратным любому из простых чисел, меньших текущего простого числа, иначе он был бы помечен как составной. Я также не могу быть большим кратным текущего простого числа, поскольку его значение меньше квадрата текущего простого числа. Следовательно, это должно быть новое простое число.

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

Вот простое несегментированное сито эратосфена, которое я обычно использую для просеивания простых чисел фактора в пределах короткого диапазона, т.е. до 2 ^ 16. Для этого поста я изменил его для работы за пределами 2 ^ 16, заменив int на ushort

static List<int> small_odd_primes_up_to (int n)
{
    var result = new List<int>();

    if (n < 3)
        return result;

    int sqrt_n_halved = (int)(Math.Sqrt(n) - 1) >> 1, max_bit = (n - 1) >> 1;
    var odd_composite = new bool[max_bit + 1];

    for (int i = 3 >> 1; i <= sqrt_n_halved; ++i)
        if (!odd_composite[i])
            for (int p = (i << 1) + 1, j = p * p >> 1; j <= max_bit; j += p)
                odd_composite[j] = true;

    result.Add(3);  // needs to be handled separately because of the mod 3 wheel

    // read out the sieved primes
    for (int i = 5 >> 1, d = 1; i <= max_bit; i += d, d ^= 3)
        if (!odd_composite[i])
            result.Add((i << 1) + 1);

    return result;
}

При просеивании первых 10000 простых чисел будет превышен типичный кэш L1 объемом 32 КиБайта, но функция все еще очень быстра (доля миллисекунды даже в C #).

Если вы сравните этот код с ситом deque, то легко заметить, что операции сита deque намного сложнее, и он не может эффективно амортизировать свои накладные расходы, потому что он всегда делает кратчайшие возможные отрезки подряд (ровно одно вычеркивание после пропуска всех уже вычеркнутых кратных значений).

Примечание: код C # использует int вместо uint, потому что новые компиляторы имеют привычку генерировать некачественный код для uint, вероятно, для того, чтобы подтолкнуть людей к целым числам со знаком ... В версии C ++ код выше я использовал unsigned везде, естественно; эталонный тест должен был быть в C ++, потому что я хотел, чтобы он базировался на предположительно адекватном типе deque (std::deque<unsigned>; при использовании unsigned short не было никакого увеличения производительности). Вот цифры для моего ноутбука Haswell (VC ++ 2015 / x64):

deque vs simple: 1.802 ms vs 0.182 ms
deque vs simple: 1.836 ms vs 0.170 ms 
deque vs simple: 1.729 ms vs 0.173 ms

Примечание: время C # почти в два раза больше времени C ++, что довольно хорошо для C #, и это показывает, что List<int> не является дураком, даже если его используют как деку.

Простой ситовый код по-прежнему уносит ветвь из воды, даже если он уже работает за пределами своего нормального рабочего диапазона (размер кэш-памяти L1 превышен на 50%, при этом происходит перегрузка кеша). Доминирующая часть здесь - это чтение просеянных простых чисел, и это не сильно влияет на проблему с кешем. В любом случае функция была разработана для просеивания факторов факторов, то есть уровня 0 в трехуровневой иерархии сит, и обычно она должна возвращать только несколько сотен факторов или небольшое количество тысяч. Отсюда и его простота.

Производительность может быть улучшена более чем на порядок при использовании сегментированного сита и оптимизации кода для извлечения просеянных простых чисел (пошаговый мод 3 и развернутый дважды, или мод 15 и развернутый один раз), и все же можно повысить производительность. вне кода, используя колесо мод 16 или мод 30 со всеми обрезками (то есть полное развертывание для всех остатков). Нечто подобное объясняется в моем ответе на Найдите простое простое число в Code Review, где обсуждалась аналогичная проблема. Но трудно понять смысл улучшения времени за миллисекунды для одноразовой задачи ...

Для краткости рассмотрим следующие моменты времени C ++ для просеивания до 100 000 000:

deque vs simple: 1895.521 ms vs 432.763 ms
deque vs simple: 1847.594 ms vs 429.766 ms
deque vs simple: 1859.462 ms vs 430.625 ms

В отличие от этого, сегментированное сито в C # с несколькими наворотами выполняет ту же самую работу за 95 мс (нет доступных временных интервалов C ++, поскольку в настоящее время я выполняю вызовы кода только в C #).

В интерпретируемом языке, таком как Python, все может выглядеть совершенно иначе ops (умножение и, возможно, даже деление). Это неизбежно подорвет преимущество простоты Решета Эратосфена, и это может сделать решение для настила немного более привлекательным.

Кроме того, во многих случаях, сообщаемых другими респондентами в этой теме, вероятно, преобладает время вывода . Это совершенно другая война, где моим основным оружием является простой класс, подобный этому:

class CCWriter
{
    const int SPACE_RESERVE = 11;  // UInt31 + '\n'

    public static System.IO.Stream BaseStream;
    static byte[] m_buffer = new byte[1 << 16];  // need 55k..60k for a maximum-size range
    static int m_write_pos = 0;
    public static long BytesWritten = 0;         // for statistics

    internal static ushort[] m_double_digit_lookup = create_double_digit_lookup();

    internal static ushort[] create_double_digit_lookup ()
    {
        var lookup = new ushort[100];

        for (int lo = 0; lo < 10; ++lo)
            for (int hi = 0; hi < 10; ++hi)
                lookup[hi * 10 + lo] = (ushort)(0x3030 + (hi << 8) + lo);

        return lookup;
    }

    public static void Flush ()
    {
        if (BaseStream != null && m_write_pos > 0)
            BaseStream.Write(m_buffer, 0, m_write_pos);

        BytesWritten += m_write_pos;
        m_write_pos = 0;
    }

    public static void WriteLine ()
    {
        if (m_buffer.Length - m_write_pos < 1)
            Flush();

        m_buffer[m_write_pos++] = (byte)'\n';
    }

    public static void WriteLinesSorted (int[] values, int count)
    {
        int digits = 1, max_value = 9;

        for (int i = 0; i < count; ++i)
        {
            int x = values[i];

            if (m_buffer.Length - m_write_pos < SPACE_RESERVE)
                Flush();

            while (x > max_value)
                if (++digits < 10)
                    max_value = max_value * 10 + 9;
                else
                    max_value = int.MaxValue;               

            int n = x, p = m_write_pos + digits, e = p + 1;

            m_buffer[p] = (byte)'\n';

            while (n >= 10)
            {
                int q = n / 100, w = m_double_digit_lookup[n - q * 100];
                n = q;
                m_buffer[--p] = (byte)w;
                m_buffer[--p] = (byte)(w >> 8);
            }

            if (n != 0 || x == 0)
                m_buffer[--p] = (byte)((byte)'0' + n);

            m_write_pos = e;
        }
    }
}

Для записи 10000 (отсортированных) чисел требуется менее 1 мс. Это статический класс, потому что он предназначен для текстового включения в заявки на кодирование, с минимумом суеты и нулевых накладных расходов.

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

2 голосов
/ 22 февраля 2010

В Python

import gmpy
p=1
for i in range(10000):
    p=gmpy.next_prime(p)
    print p 
2 голосов
/ 19 июня 2009

Сито, кажется, неправильный ответ. Сито дает вам простые числа до число N , а не первые N простые числа. Запустите @Imran или @Andrew Szeto, и вы получите простые числа до N.

Сито может все еще использоваться, если вы продолжаете пробовать сита для все больших чисел, пока не достигнете определенного размера набора результатов, и используете некоторое кэширование уже полученных чисел, но я считаю, что оно все равно будет не быстрее, чем решение как @ Pat's.

2 голосов
/ 16 февраля 2009

Адаптация и последующая от GateKiller , вот последняя версия, которую я использовал.

    public IEnumerable<long> PrimeNumbers(long number)
    {
        List<long> primes = new List<long>();
        for (int i = 2; primes.Count < number; i++)
        {
            bool divisible = false;

            foreach (int num in primes)
            {
                if (i % num == 0)
                    divisible = true;

                if (num > Math.Sqrt(i))
                    break;
            }

            if (divisible == false)
                primes.Add(i);
        }
        return primes;
    }

Это в основном то же самое, но я добавил предложение "break on Sqrt" и изменил некоторые переменные, чтобы он лучше подходил для меня. (Я работал над Эйлером и мне нужно 10001-е простое число)

1 голос
/ 11 марта 2011

Вот мой код VB 2008, который находит все простые числа <10 000 000 за 1 минуту 27 секунд на моем рабочем ноутбуке. Он пропускает четные числа и ищет только простые числа, которые являются <квадратом тестового числа. Он предназначен только для поиска простых чисел от 0 до часового значения. </p>

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles 
Button1.Click

    Dim TestNum As Integer
    Dim X As Integer
    Dim Z As Integer
    Dim TM As Single
    Dim TS As Single
    Dim TMS As Single
    Dim UnPrime As Boolean
    Dim Sentinal As Integer
    Button1.Text = "Thinking"
    Button1.Refresh()
    Sentinal = Val(SentinalTxt.Text)
    UnPrime = True
    Primes(0) = 2
    Primes(1) = 3
    Z = 1
    TM = TimeOfDay.Minute
    TS = TimeOfDay.Second
    TMS = TimeOfDay.Millisecond
    For TestNum = 5 To Sentinal Step 2
        Do While Primes(X) <> 0 And UnPrime And Primes(X) ^ 2 <= TestNum
            If Int(TestNum / Primes(X)) - (TestNum / Primes(X)) = 0 Then
                UnPrime = False
            End If
            X = X + 1

        Loop
        If UnPrime = True Then
            X = X + 1
            Z = Z + 1
            Primes(Z) = TestNum
        End If
        UnPrime = True
        X = 0
    Next
    Button1.Text = "Finished with " & Z
    TM = TimeOfDay.Minute - TM
    TS = TimeOfDay.Second - TS
    TMS = TimeOfDay.Millisecond - TMS
    ShowTime.Text = TM & ":" & TS & ":" & TMS
End Sub
1 голос
/ 12 мая 2016

Используя сито Эратосфена, вычисления выполняются намного быстрее по сравнению с «общеизвестным» алгоритмом простых чисел.

Используя псевдокод из вики (https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes),, я смогу найти решение на C #.

/// Get non-negative prime numbers until n using Sieve of Eratosthenes.
public int[] GetPrimes(int n) {
    if (n <= 1) {
        return new int[] { };
    }

    var mark = new bool[n];
    for(var i = 2; i < n; i++) {
        mark[i] = true;
    }

    for (var i = 2; i < Math.Sqrt(n); i++) {
        if (mark[i]) {
            for (var j = (i * i); j < n; j += i) {
                mark[j] = false;
            }
        }
    }

    var primes = new List<int>();
    for(var i = 3; i < n; i++) {
        if (mark[i]) {
            primes.Add(i);
        }
    }

    return primes.ToArray();
}

GetPrimes (100000000) занимает 2 с и 330 мс.

ПРИМЕЧАНИЕ : значение может отличаться в зависимости от технических характеристик оборудования.

1 голос
/ 16 апреля 2016

Вот решение C ++, использующее форму SoE:

#include <iostream>
#include <deque>

typedef std::deque<int> mydeque;

void my_insert( mydeque & factors, int factor ) {
    int where = factor, count = factors.size();
    while( where < count && factors[where] ) where += factor;
    if( where >= count ) factors.resize( where + 1 );
    factors[ where ] = factor;
}

int main() {
    mydeque primes;
    mydeque factors;
    int a_prime = 3, a_square_prime = 9, maybe_prime = 3;
    int cnt = 2;
    factors.resize(3);
    std::cout << "2 3 ";

    while( cnt < 10000 ) {
        int factor = factors.front();
        maybe_prime += 2;
        if( factor ) {
            my_insert( factors, factor );
        } else if( maybe_prime < a_square_prime ) {
            std::cout << maybe_prime << " ";
            primes.push_back( maybe_prime );
            ++cnt;
        } else {
            my_insert( factors, a_prime );
            a_prime = primes.front();
            primes.pop_front();
            a_square_prime = a_prime * a_prime;
        }
        factors.pop_front();
    }

    std::cout << std::endl;
    return 0;
}

Обратите внимание, что эта версия сита может вычислять простые числа бесконечно.

Также обратите внимание, что STL deque занимает O(1) время для выполнения push_back, pop_front и произвольного доступа через подписку.

Операция resize занимает O(n) время, где n - количество добавляемых элементов. Из-за того, как мы используем эту функцию, мы можем рассматривать эту небольшую константу.

Тело цикла while в my_insert выполняется O(log log n) раз, где n равно переменной maybe_prime. Это связано с тем, что выражение условия while будет оцениваться как истинное один раз для каждого простого множителя maybe_prime. Смотрите " Функция делителя " в Википедии.

Умножение на количество вызовов my_insert показывает, что для перечисления n простых чисел требуется O(n log log n) времени, что неудивительно, что временная сложность, которую должен иметь Сито Эратосфена .

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

1 голос
/ 02 марта 2014

Следующий код Mathcad вычисляет первый миллион простых чисел менее чем за 3 минуты.

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

enter image description here

0 голосов
/ 16 ноября 2018

Поскольку вам нужны только первые 10000 простых чисел, а не кодирование сложного алгоритма, я предлагаю следующие

boolean isPrime(int n){
//even but is prime
    if(n==2)
        return true;
//even numbers filtered already 
    if(n==0 || n==1 || n%2==0)
        return false;

// loop for checking only odd factors
// i*i <= n  (same as i<=sqrt(n), avoiding floating point calculations)
    for(int i=3 ; i*i <=n ; i+=2){
    // if any odd factor divides n then its not a prime!
        if(n%i==0)
            return false;
    }
// its prime now
    return true;
}

теперь вызов прост, как вам нужно

for(int i=1 ; i<=1000 ; i++){
    if(isPrime(i)){
       //do something
    }
}
...