Вы простое число

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

Мой вопрос: можете ли вы предложить улучшение (без ссылки на какую-либо другую ссылку в сети, я ищу фактический код C)? Что я пытаюсь получить от этого, так это лучшее понимание определения сложности производительности решения, подобного этому.

Правильно ли я делаю вывод, что сложность этого решения равна O(n^2)?

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

/* isprime                                                           */
/* Test if each number in the list from stdin is prime.              */
/* Output will only print the prime numbers in the list.             */

int main(int argc, char* argv[]) {

    int returnValue = 0;
    int i;
    int ceiling;
    int input = 0;
    int factorFound = 0;

    while (scanf("%d", &input) != EOF) {

        ceiling = (int)sqrt(input);
        if (input == 1) {
            factorFound = 1;
        }

        for (i = 2; i <= ceiling; i++) {
            if (input % i == 0) {
                factorFound = 1;
            } 
        }

        if (factorFound == 0) {
            printf("%d\n", input);
        }

        factorFound = 0;    
    } 

    return returnValue;
}

person dvanaria    schedule 25.09.2010    source источник
comment
На самом деле это O(n). Алгоритм O(n^0.5), но цикл while делает его O(m*n^0.5) или O(m).   -  person Ishtar    schedule 25.09.2010
comment
Это форма самого простого генератора простых чисел. Проблема в том, что когда N становится очень большим, этот алгоритм работает очень медленно. Посмотрите тест AKS, упомянутый ниже, а также теорию сита в целом. Я бы также использовал разные подходы для разных целей, в зависимости от того, что вы пытаетесь сделать: вам нужны маленькие или большие простые числа? Вероятно, простые или настоящие простые числа? Алгоритм и реализация будут варьироваться в зависимости от требований.   -  person    schedule 25.09.2010
comment
В идеале, поскольку вы зацикливаетесь на N (ввод), вы должны построить таблицу с известным PN и использовать ее для деления чисел, это будет все быстрее и быстрее, каждый раз, когда вы пробуете новое число. Если вы хотите узнать, сколько PN находится между 1 и N, primes.utm.edu/howmany. html   -  person Breaking not so bad    schedule 25.09.2010
comment
Имейте в виду, что это область давнего, интенсивного академического контроля. Если вы делаете это для развлечения или в качестве упражнения по программированию, прекрасно. Если вы серьезно, вам предстоит немало почитать...   -  person dmckee --- ex-moderator kitten    schedule 25.09.2010
comment
Я действительно проверил свой SO userId... это не простое число :(   -  person st0le    schedule 26.09.2010


Ответы (10)


   for (i = 2; i <= ceiling; i++) {
        if (input % i == 0) {
            factorFound = 1;
            break;
        } 
    }

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

Кроме того, как только вы увидите, что input не делится на 2, нет необходимости проверять 4, 6, 8 и т. д. Если бы любое четное число делилось на input, то 2 обязательно делилось бы на 2, потому что оно делит все четные числа.

Если вы хотите немного выйти за рамки алгоритма, вы можете использовать цикл, подобный тому, что Шелдон Л. Купер дает в своем ответе. (Это просто проще, чем заставить его исправлять мой код из комментариев, хотя его усилия очень ценятся)

это использует тот факт, что каждое простое число, отличное от 2 и 3, имеет форму n*6 + 1 или n*6 - 1 для некоторого положительного целого числа n. Чтобы убедиться в этом, просто заметьте, что если m = n*6 + 2 или m = n*6 + 4, то n делится на 2. если m = n*6 + 3 то делится на 3.

На самом деле, мы можем пойти дальше. Если p1, p2, .., pk являются первыми k простыми числами, то все целые числа, взаимно простые со своим произведением, обозначают «слоты», в которые должны поместиться все оставшиеся простые числа.

чтобы увидеть это, пусть k# будет произведением всех простых чисел до pk. тогда, если gcd(k#, m) = g, g делит n*k# + m, и поэтому эта сумма тривиально составная, если g != 1. поэтому, если вы хотите выполнить итерацию с точки зрения 5# = 30, тогда ваши взаимно простые целые числа равны 1, 7, 11, 13, 17, 19, 23 и 29.


технически я не доказал свое последнее утверждение. Это не намного сложнее

если g = gcd(k#, m), то для любого целого числа n, g делит n*k# + m, потому что оно делит k#, поэтому оно должно также делить n*k#. Но он также делит m, поэтому должен делить сумму. Выше я доказал это только для n = 1. моя вина.


Кроме того, я должен отметить, что ничто из этого не меняет фундаментальную сложность алгоритма, он по-прежнему будет O (n ^ 1/2). Все, что он делает, это значительно уменьшает коэффициент, который используется для расчета фактического ожидаемого времени выполнения.

person aaronasterling    schedule 25.09.2010
comment
В целом пост хороший, но есть ошибки. Ваш второй фрагмент кода не работает. Кроме того, вы написали n*5 - 1 вместо n*6 - 1. И m = n*6 + 3 всегда делится на 3, а не на 6. - person Sheldon L. Cooper; 25.09.2010
comment
@ Шелдон. Спасибо. Все неловкие ошибки, и я рад, что они исправлены. - person aaronasterling; 26.09.2010
comment
ваш код все еще неверен. Должно быть input % (i - 1) == 0, а не (input - 1). И условие цикла while должно быть (i-1) <= ceiling. - person Sheldon L. Cooper; 26.09.2010
comment
Может быть, я педантичен, но теперь первый фрагмент кода неверен. Я имел в виду цикл while в моем предыдущем комментарии. Этот код завершится ошибкой при вводе = 2. В этом случае условие должно быть просто i <= ceiling. - person Sheldon L. Cooper; 26.09.2010
comment
@ Шелдон, я знаю, что ты имел в виду. Моего редактирования-фу сегодня серьезно не хватает. - person aaronasterling; 26.09.2010

Временная сложность каждого теста на простоту в вашем алгоритме равна O(sqrt(n)).

Вы всегда можете использовать тот факт, что все простые числа, кроме 2 и 3, имеют форму: 6*k+1 или 6*k-1. Например:

int is_prime(int n) {
  if (n <= 1) return 0;
  if (n == 2 || n == 3) return 1;
  if (n % 2 == 0 || n % 3 == 0) return 0;
  int k;
  for (k = 6; (k-1)*(k-1) <= n; k += 6) {
    if (n % (k-1) == 0 || n % (k+1) == 0) return 0;
  }
  return 1;
}

Эта оптимизация не улучшает асимптотическую сложность.

ИЗМЕНИТЬ

Учитывая, что в вашем коде вы многократно проверяете числа, вы можете предварительно вычислить список простых чисел. Существует только 4792 простых числа, меньших или равных квадратному корню из INT_MAX (при условии, что 32-битные целые числа).

Кроме того, если исходные числа относительно малы, вы можете попробовать вычислить решето.

Вот комбинация обеих идей:

#define UPPER_BOUND 46340  /* floor(sqrt(INT_MAX)) */
#define PRIME_COUNT 4792  /* number of primes <= UPPER_BOUND */

int prime[PRIME_COUNT];
int is_prime_aux[UPPER_BOUND];

void fill_primes() {
  int p, m, c = 0;
  for (p = 2; p < UPPER_BOUND; p++)
    is_prime_aux[p] = 1;
  for (p = 2; p < UPPER_BOUND; p++) {
    if (is_prime_aux[p]) {
      prime[c++] = p;
      for (m = p*p; m < UPPER_BOUND; m += p)
        is_prime_aux[m] = 0;
    }
  }
}

int is_prime(int n) {
  if (n <= 1) return 0;
  if (n < UPPER_BOUND) return is_prime_aux[n];
  int i;
  for (i = 0; i < PRIME_COUNT && prime[i]*prime[i] <= n; i++)
    if (n % prime[i] == 0)
      return 0;
  return 1;
}

Вызовите fill_primes в начале вашей программы перед началом обработки запросов. Он работает довольно быстро.

person Sheldon L. Cooper    schedule 25.09.2010
comment
Временная сложность для каждого теста на простоту составляет O(sqrt(n)) — не так уж и близко! Пробное деление (алгоритм OP) такое медленное (с точностью до логарифмических множителей), но APR или AKS асимптотически быстрее. - person Charles; 26.09.2010
comment
@Charles: ОК, добавлено уточнение. - person Sheldon L. Cooper; 26.09.2010

Ваш код имеет только сложность O (sqrt (n) lg (n)). Если вы предполагаете, что базовые математические операции являются O (1) (верно, пока вы не начнете использовать большие числа), то это просто O (sqrt (n)).

Обратите внимание, что проверка простоты может быть выполнена быстрее, чем O(sqrt(n)lg(n)) за время. На этом сайте есть несколько реализаций тест на простоту AKS, который, как было доказано, работает за время O((log n)^12).

Есть также несколько очень-очень быстрых вероятностных тестов — хотя они и быстрые, но иногда дают неверный результат. Например, тест Ферма на простоту:

Учитывая число p, которое мы хотим проверить на простоту, выбираем случайное число a и проверяем, является ли a^(p-1) mod p = 1. Если false, p определенно не простое число. Если это правда, p вероятно является простым числом. Повторяя тест с разными случайными значениями a, можно уменьшить вероятность ложного срабатывания.

Обратите внимание, что у этого конкретного теста есть некоторые недостатки — подробности см. на странице Википедии и других вероятностных тестах на простоту, которые вы можете использовать.

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

person bdonlan    schedule 25.09.2010

Простым улучшением было бы изменить цикл for, чтобы он прерывался, когда он находит фактор:

   for (i = 2; i <= ceiling && !factorFound; i++) {
        if (input % i == 0) {
            factorFound = 1;

Другой возможностью было бы увеличить счетчик на 2 (после проверки самого 2).

person Mark Wilkins    schedule 25.09.2010

Вы можете предложить улучшение

Вот так... не за алгоритм, а за саму программу :)

  • Если вы не собираетесь использовать argc и argv, избавьтесь от них.
  • Что, если я введу «сорок два»? Сравните scanf() == 1, а не != EOF
  • Не нужно приводить значение sqrt()
  • returnValue не нужно, можно вернуть константу: return 0;
  • Вместо того, чтобы иметь всю функциональность внутри функции main(), разделите свою программу на столько функций, сколько сможете придумать.
person pmg    schedule 25.09.2010

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

if (input < 2 || (input != 2 && input % 2 == 0))
  factorFound = 1;

if (input > 3)
  for (i = 3; i <= ceiling && !factorFound; i += 2)
    if (input % i == 0)
      factorFound = 1;

Что касается сложности, если n является вашим входным числом, не будет ли сложность O (sqrt (n)), поскольку вы делаете примерно не более sqrt (n) делений и сравнений?

person Hugo Peixoto    schedule 25.09.2010

Четные числа (кроме 2) не могут быть простыми числами. Итак, как только мы узнаем, что число нечетное, мы можем просто проверить, являются ли нечетные числа его факторами.

for (i = 3; i <= ceiling; i += 2) {
        if (input % i == 0) {
            factorFound = 1;
            break;
        } 
    }
person letronje    schedule 25.09.2010
comment
-1 четные числа не могут быть простыми числами — ложное утверждение. Существует ровно одно четное простое число, а именно 2. - person Andreas Rejbrand; 25.09.2010

Временная сложность вашей программы O(n*m^0.5). С n количество простых чисел на входе. И m размер самого большого простого числа во входных данных, или MAX_INT, если хотите. Таким образом, сложность также может быть записана как O(n) с n количеством простых чисел для проверки.

С Big-O n (обычно) - это размер ввода, в вашем случае это будет количество простых чисел для проверки. Если я сделаю этот список в два раза больше (например, продублирую его), он займет (+-) ровно вдвое больше, таким образом, O(n).

person Ishtar    schedule 25.09.2010

Вот мой алгоритм, сложность остается O(n^0.5), но мне удалось удалить некоторые дорогостоящие операции в коде...

Самая медленная часть алгоритма — операция modulus, мне удалось исключить sqrt или выполнить i * i <= n

Таким образом я экономлю драгоценные циклы... это основано на том факте, что sum of odd numbers is always a perfect square.

Поскольку мы все равно перебираем odd numbers, почему бы не использовать его? :)

int isPrime(int n)
{
    int squares = 1;
    int odd = 3;

    if( ((n & 1) == 0) || (n < 9)) return (n == 2) || ((n > 1) && (n & 1));
    else
    {
        for( ;squares <= n; odd += 2)
        {
            if( n % odd == 0) 
                return 0;
            squares+=odd;
        }
        return 1;
    }
}
person st0le    schedule 25.09.2010

Нет возможности улучшить алгоритм. Могут быть крошечные способы улучшить ваш код, но не базовую скорость (и сложность) алгоритма.

РЕДАКТИРОВАТЬ: Конечно, поскольку ему не нужно знать все факторы, просто является ли это простым числом. Отличное место.

person Alexander Rafferty    schedule 25.09.2010
comment
Это неверно. Существуют алгоритмы O((log n)^12) для проверки простоты. en.wikipedia.org/wiki/Primality_test#Fast_deterministic_tests Некоторые из этих алгоритмов могут не практично на практике, но, безусловно, есть алгоритмы лучше, чем O (n ^ 2). - person bdonlan; 25.09.2010
comment
Нет возможности улучшить ЭТОТ алгоритм, но он может заменить весь алгоритм. Так что ответ правильный. - person ; 25.09.2010