Crivo de Eratóstenes

Video do conteúdo

  • Code Marathon
Video no YouTube

Problema

Dado um valor inteiro N queremos saber quais valores entre 1 e N são primos.

Solução Trivial: O(N \cdot \sqrt N)

Usando o que aprendemos na aula sobre primos podemos descobrir se um número é primo ou não em O(\sqrt n). Assim, podemos fazer isso para cada valor i de 1 até N.

bool ehPrimo(int n){
    if(n<2)
        return false;
    for(int i=2; i<=n/i; i++){
        if(n%i == 0)
            return false;
    }
    return true;
}
vector<bool> primosAteN(int n){
    vector<bool> eh_primo(n+1);
    for(int i=0; i<=n; i++)
        eh_primo[i] = ehPrimo(i);
    return eh_primo;
}

Usando o Crivo: O(N \cdot log(log(N)))

A ideia por trás do Crivo de Eratóstenes é bastante simples. Para verificar se um número x não é primo então basta verificar se ele possue um divisor entre 2 e x-1. Então, para cada número i iremos marcar os seus múltiplos(j = i*k) como não primo. Assim, no final, quem não for marcado é declarado como primo.

Implementação O(N \cdot log(N))

vector<bool> crivo(int n){
    vector<bool> eh_primo(n+1, true); // Iniciando todos como verdadeiro
    eh_primo[0] = eh_primo[1] = false;
    for(int i=2; i<=n; i++){
        for(int j=i+i; j<=n; j+=i){
            eh_primo[j] = false;
        }           
    }
    return eh_primo;
}

A função de complexidade f pode ser descrita como:

f(N) = \sum_{i=2}^{N} \frac{N}{i} = \frac{N}{1} + \frac{N}{2} + \frac{N}{3} + \frac{N}{4} + \frac{N}{5} + \frac{N}{6} + \frac{N}{7} + \frac{N}{8} +\cdots + \frac{N}{N} f(N) \le \frac{N}{1} + \frac{N}{2} + \frac{N}{2} + \frac{N}{4} + \frac{N}{4} + \frac{N}{4} + \frac{N}{4} + \frac{N}{8} +\cdots + \frac{N}{N} = N \cdot log_2(N) Logo, podemos afirmar que o algoritmo acima é O(N \cdot log(N)).

Implementação O(N \cdot log(log(N)))

Fazendo apenas algumas observações simples melhoramos a complexidade do algoritmo. Primeira observação é que só precisamos marcar os múltiplos usando valores primos, pois todo número composto tem um divisor primo. A segunda modificação é que só é necessário fazer isso até o primo que seja menor ou igual a \sqrt N.

vector<bool> crivo(int n){
    vector<bool> eh_primo(n+1, true);
    eh_primo[0] = eh_primo[1] = false;
    for(int i=2; i<=n/i; i++){
        if(eh_primo[i]){
            for(int j=i+i; j<=n; j+=i){
                eh_primo[j] = false;
            }           
        }
    }
    return eh_primo;
}

Adaptando para outros problemas

Quantidade de divisores

Dado um valor inteiro N queremos saber quantos divisores cada valor entre 1 e N possui.

Para esse problema podemos usar um algoritmo semelhante ao crivo e conseguir um algoritmo O(N \cdot log(N)).

vector<int> qtdDivisoresDe1AteN(int n){
    vector<int> qtd_divisores(n+1, 0);
    for(int i=1; i<=n; i++){
        for(int j=i; j<=n; j+=i){
            qtd_divisores[j]++;
        }           
    }
    return qtd_divisores;
}

Soma dos divisores

Dado um valor inteiro N queremos saber qual a soma dos divisores de cada valor entre 1 e N.

Para esse problema podemos usar um algoritmo semelhante ao crivo e conseguir um algoritmo O(N \cdot log(N)).

vector<long long> somaDivisoresDe1AteN(int n){
    vector<long long> soma_divisores(n+1, 0);
    for(int i=1; i<=n; i++){
        for(int j=i; j<=n; j+=i){
            soma_divisores[j]+=i;
        }           
    }
    return soma_divisores;
}

Outros

Além dos mostrados aqui podemos adaptar o crivo para resolver vários outros problemas como, por exemplo, phi de Euler e fatores primos.

Material Complementar

Crivo de Erastótenes (Neps Academy)

Sieve of Eratosthenes (CP-Algorithm)

Sieve of Eratosthenes Having Linear Time Complexity (CP-Algorithm)