Matemática Computacional 03 – Crivo de Eratóstenes e o Menor Fator Primo

Escrito por Leonardo Paes e Lúcio Figueiredo

Na matemática computacional há diversas maneiras de checar se um número é primo ou não, com diferentes algoritmos que possuem seus pontos fortes e fracos. Na aula de hoje, aprenderemos um algoritmo chamado de Crivo de Eratóstenes. Como o próprio nome sugere, seu criador foi Eratóstenes (a.C. 285-194 a.C.), um matemático grego.

Motivação Inicial

Imagine que precisamos resolver o seguinte problema: dado um número inteiro $$N$$, indique quantos números primos existem no intervalo $$[1, N]$$ (intervalo de $$1$$ a $$N$$, inclusive). Para resolver este problema, podemos utilizar um algoritmo bastante simples: para cada número $$v$$ no intervalo $$[1, N]$$, realizamos um teste de primalidade em $$v$$ – se o teste indicar que $$v$$ é primo, a resposta do problema aumenta em 1; senão, continua a mesma. Se utilizarmos o teste de primalidade apresentando na Aula 2, obtemos uma solução com complexidade $$O(N \sqrt{n})$$, já que iteramos por $$N$$ números e realizamos um teste de primalidade em $$O(\sqrt{n})$$ em cada um.

Perceba, porém, que para valores de $$N$$ maiores ou iguais a $$10^8$$, o algoritmo acima é muito ineficiente, necessitando de pelo menos $$10^{12}$$ operações! Felizmente, uma solução mais eficiente existe: o Crivo de Eratóstenes, que será apresentado a seguir.

Crivo de Eratóstenes

O algoritmo em si é bem simples:

  1.  Escreva todos os números de $$2$$ até $$N$$. Inicialmente, nós não vamos considerá-los nem primos nem compostos (números que possuem mais que 2 divisores distintos).
  2.  Pegue o primeiro número que ainda não foi marcado como composto e diga que ele é primo.
  3.  A partir desse número primo, percorra todos os seus múltiplos até $$N$$ e marque-os como compostos. Eles necessariamente são compostos pois possuem como divisores, pelo menos, o número $$1$$, o primo atual e eles mesmos.
  4.  Volte para o passo 2 até que todos os números sejam marcados como primos ou compostos.

A ideia por trás do algoritmo é a seguinte: Um número $$X$$ só é primo se nenhum outro primo no intervalo $$[1; X-1]$$ divide $$X$$. Como estamos iterando pelos números primos da esquerda para a direita, estamos marcando como compostos todos os números que são seus múltiplos. Então, se chegarmos em um número que não foi dito como composto $$\implies$$ ele é primo!

Visualização do algoritmo

Gif utilizado de: Wikipedia.

Código do algoritmo

Complexidade do Crivo de Eratóstenes

Você deve ter percebido que até agora não foi dito nada sobre a complexidade do algoritmo do Crivo. Nesta aula, provaremos que a sua complexidade é $$O(N \log_{} N)$$.

Para simplificar os cálculos, vamos assumir que $$N$$ é uma potência de $$2$$, ou seja $$N = 2^k$$, $$k \in \mathbb{Z}$$. Seja $$f(N)$$ a complexidade do algoritmo. Note que $$f(N)$$ é igual ao somatório, para cada primo $$p \leq N$$, do número de múltiplos de $$p$$ menores ou iguais a $$N$$; em outras palavras, $$f(n)$$ é igual ao somatório de $$\lfloor \frac{N}{p} \rfloor$$ para todo número primo $$p \leq N$$. Portanto, perceba que se em nosso algoritmo, ao invés de iterarmos por todos os múltiplos dos números primos, iterássemos pelos múltiplos de todos os números menores ou iguais a $$N$$, a complexidade resultante do algoritmo seria maior ou igual à do Crivo – ou seja, maior ou igual a $$f(N)$$. Logo, temos que,

$$\lfloor \frac{N}{1} \rfloor + \lfloor \frac{N}{2} \rfloor + \lfloor \frac{N}{3} \rfloor + … + \lfloor \frac{N}{N} \rfloor \geq f(N)$$.

Vamos agora supor que a quantidade de operações é ainda maior (ou seja, utilizar um limite superior ainda mais “frouxo” para $$f(N)$$). Ao invés de somarmos $$\lfloor \frac{N}{i} \rfloor $$ para cada $$i \leq N$$, vamos somar $$\frac{N}{2^{\lfloor{log_{2} \ i} \rfloor}} $$. Desse modo, o número total de operações (que continua maior que $$f(N)$$) é:

$$\frac{N}{2^{\lfloor{log_{2} \ 1} \rfloor}} + \frac{N}{2^{\lfloor{log_{2} \ 3} \rfloor}} + … + \frac{N}{2^{\lfloor{log_{2} \ N} \rfloor}} \geq f(N)$$

Agora, note que $$\lfloor log_{2} \ 2^k \rfloor = \lfloor log_{2} \ (2^k + 1) \rfloor = \lfloor \log_{2} (2^k + 2) \rfloor \ … \ = \lfloor log_{2} \ (2^k + (2^k – 1)) \rfloor$$, já que estamos calculando a parte inteira de cada logaritmo. Utilizando este fato, podemos concluir que a expressão acima é equivalente a

$$\underbrace{\frac{N}{2^{\lfloor{log_{} \ 2^0} \rfloor}}}_{2^0 \text{ vezes}} + \underbrace{\frac{N}{2^{\lfloor{log_{} \ 2^1} \rfloor}} + \frac{N}{2^{\lfloor{log_{} \ 2^1} \rfloor}}}_{2^1 \text{ vezes}} + \ … \ + \underbrace{\frac{N}{2^{\lfloor{log_{} \ N} \rfloor}} + \frac{N}{2^{\lfloor{log_{} \ N} \rfloor}} + \ … \ + \frac{N}{2^{\lfloor{log_{} \ N} \rfloor}}}_{2^{log_{} N} \text{ vezes}}$$

$$ = 2^0 \cdot \frac{N}{2^0} + 2^1 \cdot \frac{N}{2^1} + \ … \ + 2^{log_{} N} \cdot \frac{N}{2^{\log_{} N}} = \underbrace{N + N + \ … \ + N}_{\log_{} N \text{ vezes}} = N \cdot \log_{} N$$

Como vimos que $$f(N)$$ é menor ou igual à expressão calculada acima, obtemos que $$f(N) \leq N \log_{} N$$ $$\implies$$ complexidade do Crivo de Eratóstenes $$\in O(N \cdot \log_{} N)$$.

Obs.: Na demonstração acima assumimos que o valor $$N$$ era uma potência de $$2$$. Caso isto não fosse verdade, o resultado obtido seria bastante semelhante, com a diferença de que a expressão $$\frac{N}{2^{\lfloor{log_{2} \ 1} \rfloor}} + \frac{N}{2^{\lfloor{log_{2} \ 3} \rfloor}} + … + \frac{N}{2^{\lfloor{log_{2} \ N} \rfloor}}$$ seria igual a $$N \cdot \log_{} N + O(N)$$; ou seja, $$N \cdot \log_{} N$$ somado a um valor “de resto” menor ou igual a $$N$$. Portanto, a complexidade do algoritmo é a mesma em ambos os casos.

Apesar de termos provado que o Crivo possui complexidade $$O(N \log N)$$, é possível provar que o algoritmo é ainda mais eficiente, possuindo complexidade $$O(N \log_{} \log_{} N)$$. Como a demonstração deste fato requer conhecimentos de cálculo integral, não a mencionamos nesta aula; porém, caso queira conferi-la, leia este artigo (em inglês).

O Menor Fator Primo (SPF)

Uma das aplicações do Crivo de Eratóstenes é o cálculo do Menor Fator Primo de um inteiro, ou SPF (do inglês Shortest Prime Factor). Alguns exemplos de SPF:

$$SPF(10) = 2$$, já que $$10 = 2 \cdot 5$$.
$$SPF(39) = 3$$, já que $$39 = 3 \cdot 13$$.
$$SPF(5797) = 11$$, já que $$5797 = 11 \cdot 17 \cdot 31$$.

Para calcular o SPF de todos os números no intervalo $$[1, N]$$, basta fazer uma única modificação ao Crivo de Eratóstenes. Inicialmente, vamos declarar o vetor $$SPF[]$$, que indica o menor fator primo de um número, e vamos inicializá-lo com o valor $$0$$. Após isso, vamos executar o Crivo. Se no passo atual do algoritmo o primeiro número não marcado é o primo $$p$$, vamos identificar se $$p$$ é ou não o menor fator primo de cada um de seus múltiplos, conferindo para cada múltiplo $$j$$ de $$p$$ se $$SPF[j]$$ é igual a $$0$$; se sim, $$j$$ não foi marcado, e portanto $$p$$ é seu menor fator primo; caso contrário, seu menor fator primo já foi encontrado em um passo anterior do algoritmo. O código abaixo implementa esta modificação do Crivo para encontrar SPFs:

Fatoração em $$O(\log_{} N)$$

Imagine que queremos fatorar um número inteiro $$N$$ em primos, ou seja, encontrar todos os seus divisores primos. Um algoritmo que resolve este problema é aquele que foi apresentado na aula anterior, com complexidade $$O(\sqrt{N})$$. Porém, utilizando o Menor Fator Primo calculado pelo Crivo de Eratóstenes, conseguimos realizar fatorações em $$O(\log_{} N)$$.

Inicialmente, perceba que um número inteiro $$N$$ possui, no máximo, $$\log_{2} N$$ fatores primos, já que o menor número primo é $$2$$ e o maior expoente $$x$$ tal que $$2^x \leq N$$ é $$\log_{2} N$$. Utilizando o SPF, podemos iterar por todos os primos na fatoração de $$N$$ em ordem, da seguinte forma:

  1. Inicialmente, a lista de fatores primos de $$N$$ está vazia;
  2. Se $$N$$ for maior que $$1$$, insira $$SPF(N)$$ na lista de fatores primos; senão, termine o algoritmo;
  3. Repita o passo anterior fazendo $$N := \frac{N}{SPF(N)}.$$

Ou seja, primeiro inserimos $$SPF(N)$$ na lista, depois $$SPF(\frac{N}{SPF(N)})$$, e assim por diante, enquanto o número resultante for maior ou igual a $$1$$. Desta forma, conseguimos iterar por todos os fatores primos de $$N$$ em ordem. Como $$N$$ possui no máximo $$\log_{} N$$ fatores primos, a complexidade deste algoritmo de fatoração é $$O(\log_{} N)$$. O código abaixo implementa a fatoração de $$N$$ utilizando este método: