segunda-feira, 10 de janeiro de 2022

Encontrando números primos - parte III

Nas aulas anteriores, desenvolvemos um algoritmo capaz de encontrar números primos do número 2 a um número máximo fornecido. Também otimizamos nosso algoritmo, descartando operações desnecessárias. Se você não viu as aulas anteriores, recomendo fortemente que veja a Parte I e a Parte II.

Suponha que estamos interessados nos números primos de 2 a 100. Então, executamos nosso algoritmo passando como argumento o número 100 para a função calcula_primos2(). Mas, se quando obtermos o resultado, queiramos continuar encontrando os números primos até o valor 1000? Se executamos nosso algoritmo novamente, agora com o número 1000, ele vai calcular todos os primos de 2 a 100 novamente, o que é desnecessário, uma vez que já os encontramos. Então, como aproveitamos o primeiro resultado?

Vamos fazer o seguinte: após a finalização do algoritmo, perguntamos ao usuário se quer continuar o procedimento. Caso negativo, paramos o algoritmo e retornamos o resultado. Caso afirmativo, perguntamos o próximo valor máximo e continuamos a execução do algoritmo. Fazemos isso até que o usuário indique a finalização e retornamos o resultado.

Para isso, utilizaremos a função readline(). Essa função permite que o usuário entre com um valor (no caso, de texto) por linha de comando. Por exemplo, se queremos perguntar "Deseja continuar?", escrevemos

readline(prompt = "\nDeseja continuar? y/n ")
No caso, já informamos que a resposta deve ser yes (sim) ou no (não). Se executamos o código acima, obtemos


Digitando "y" na linha de comando, temos:


Observe que a função retorna a resposta do usuário. Logo, precisamos armazená-la em uma variável para verificar se a resposta é positiva ou negativa.

Vamos implementar uma função que pergunta ao usuário o número máximo para encontrar números primos, encontra os números primos até o valor fornecido e, ao final, pergunta se o usuário deseja continuar o procedimento. Vejamos:
encontra_primos = function(){
  para_alg  = F;
  num_ini   = 3;
  
  v_primos = NULL;
  v_primos[1] = 3;
  
  while(para_alg==F){
    var1 = readline(prompt = "\n Digite o valor do número máximo: ")
    num_max = as.numeric(var1);
    
    if(num_ini>num_max){
      cat("O valor máximo deve ser maior que ",num_ini,"\n");
    }else{
      v_num = seq(num_ini,num_max,by=2);
      num_max = v_num[length(v_num)];
      
      count = 1;
      while(count<=length(v_num)){
        testa_primo = T;
        for(i in 1:length(v_primos)){
          if(v_primos[i]>sqrt(v_num[count])){
            break;
          }
          if(v_num[count]%%v_primos[i]==0){
            testa_primo = F;
            break;
          }
          
        }
        if(testa_primo==T) v_primos = c(v_primos,v_num[count]);
        count = count + 1;
      }
      varc = readline(prompt = "\nDeseja continuar? y/n ")
      if(varc=='n'){ para_alg=T;
      }else{
        num_ini = num_max + 2;
      }
    }
  }
  v_primos[1] = 2;
  return(v_primos)
  
}
Vamos entender a função acima. Note que ela não precisa de argumentos, uma vez que o único argumento (o número máximo) é passado pelo usuário. 

Primeiro, definimos algumas variáveis. para_alg é uma variável booleana que determina se o algoritmo vai parar ou não e é iniciada com false. num_ini é o número inicial do nosso vetor de números ímpares.
  para_alg  = F;
  num_ini   = 3;
Enquanto não houver parada definida pelo usuário, o algoritmo será executado:
  while(para_alg==F){...}
Dentro do comando while, obtemos do usuário o valor máximo do nosso vetor de números ímpares para executar o algoritmo:
  var1 = readline(prompt = "\n Digite o valor do número máximo: ")
  num_max = as.numeric(var1);
No código acima, armazenamos o valor que o usuário digita na variável var1, que está como texto, transformamos o valor em número e armazenamos na variável num_max. Se o número informado pelo usuário for menor que o valor de num_ini, printamos uma mensagem e perguntamos novamente o valor desejado. Caso contrário, executamos o algoritmo:
    if(num_ini>num_max){
      cat("O valor máximo deve ser maior que ",num_ini,"\n");
    }else{
      [...]
    }
Assim que a execução entra no bloco else, criamos nosso vetor de números ímpares. É importante armazenar o maior número do nosso vetor, pois ele será referência para construir o próximo, caso o usuário deseje continuar o algoritmo.
    v_num = seq(num_ini,num_max,by=2);
    num_max = v_num[length(v_num)];
As demais linhas já foram discutidas. Elas definem nosso algoritmo de encontrar números primos. Assim que essa execução termina, perguntamos ao usuário se deseja continuar executando o algoritmo. Caso negativo ("n"), o algoritmo deve parar e para_alg é igual a true. Caso contrário, o novo número inicial é o último número do vetor de ímpares acrescido de 2, ou seja, o próximo número ímpar. Isso é feito nas seguintes linhas de código:
    varc = readline(prompt = "\nDeseja continuar? y/n ")
    if(varc=='n'){ para_alg=T;
    }else{
        num_ini = num_max + 2;
    }
Ao final, quando o usuário decide parar a execução do algoritmo, devemos retornar o resultado do vetor de números primos acrescido do número 2 (que não utilizamos no nosso método):
  v_primos[1] = 2;
  return(v_primos)
Note que, no código acima, em vez de concatenar o número 2 ao vetor v_primos, estamos alterando a primeira posição do vetor pelo número 2. Isso deve ser feito porque o vetor v_primos possui o número 3 na primeira e na segunda posição. Por quê? Quando mudamos a estratégia para parar a verificação quando o número do vetor é maior que a raiz quadrada do número testado, o número 3 (que já estava no vetor) é colocado novamente no vetor v_primos, pois 3 > 1,73. Teste na sua máquina e veja o resultado. Um exemplo de saída da função encontra_primos() é a seguinte:
> encontra_primos()

 Digite o valor do número máximo: 10

Deseja continuar? y/n y

 Digite o valor do número máximo: 100

Deseja continuar? y/n y

 Digite o valor do número máximo: 1000

Deseja continuar? y/n n
  [1]   2   3   5   7  11  13  17  19  23  29  31  37  41  43  47  53  59  61
 [19]  67  71  73  79  83  89  97 101 103 107 109 113 127 131 137 139 149 151
 [37] 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251
 [55] 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359
 [73] 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463
 [91] 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593
[109] 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701
[127] 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827
[145] 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953
[163] 967 971 977 983 991 997
Em questão de execução e retorno do resultado, nossa função está completa. Em relação a interação com o usuário, ela ainda apresenta alguns problemas que devem ser tratados:

1) Se o usuário digita algo que não seja número, o algoritmo dará erro, pois não tem como transformar em numeric um texto (e digo texto no sentido de digitar letras ou caracteres especiais).

2) O usuário pode digitar qualquer coisa além de "y" ou "n" na pergunta de continuar a execução do algoritmo. Ou seja, qualquer coisa que seja diferente de "n" significa que o usuário deseja continuar.

Esses tratamentos são simples e não serão abordados aqui. Deixo como exercício para você realizar.

Na próxima aula vou falar sobre um algoritmo bastante famoso para encontrar números primos dado um número máximo: o crivo de Eratóstenes. Vamos implementá-lo e compará-lo com o nosso algoritmo desenvolvido aqui.

Então, até a próxima aula!

sexta-feira, 7 de janeiro de 2022

Encontrando números primos - parte II

Na aula passada (Parte I), desenvolvemos um algoritmo para encontrar números primos a partir do número 2. Fornecemos um número máximo a ser testado e encontramos todos os números primos que são menores que o número fornecido. Abaixo está o código do nosso algoritmo:

calcula_primos = function(num_max){
  v_num = seq(3,num_max,by=2);
  v_primos = NULL;
  v_primos[1] = 3;
  
  count = 1;
  while(count<=length(v_num)){
    testa_primo = T;
    for(i in 1:length(v_primos)){
      if(v_num[count]%%v_primos[i]==0){
        testa_primo = F;
        break;
      }
      
    }
    if(testa_primo==T) v_primos = c(v_primos,v_num[count]);
    count = count + 1;
  }
  
  v_primos = c(2,v_primos)
  return(v_primos)
}
Vamos relembrar: criamos um vetor de números ímpares (v_num) até o valor máximo fornecido e verificamos se cada um deles é primo. Isto é, para cada número testado, verificamos a divisão por cada número primo no vetor de números primos v_primos. Caso haja alguma divisão exata, o número testado não é primo, caso contrário, é primo e o acrescentamos no vetor v_primos.

Porém, nosso algoritmo tem um ponto ineficiente: o teste de divisão é realizado por todos os números do vetor v_primos para todo número testado. Mas isso não é necessário. De fato, podemos parar a verificação no número primo mais próximo da raiz quadrada do número testado. Vamos entender.

Suponha que queremos testar se o número 36 é primo. Começamos pela divisão por 2, por 3 e assim por diante. Porém, não precisamos testar a divisão por 7, por 11 e nenhum número maior que a raiz quadrada de 36, que é 6. Por que isso acontece? Vamos chamar nosso número de n. Se n é um número composto e realizamos sua decomposição, haverá pelo menos um fator menor ou igual que a raiz quadrada de n. No caso do 36, podemos escrevê-lo como 6*6 = 2*3*2*3. Outro exemplo: 64 = 8*8 = 2^6. E se o número não for escrito da forma m^2, em que m é um número inteiro que representa a raiz quadrada de n? Podemos pegar o menor inteiro mais próximo de m. Por exemplo, o número 35 tem como raiz quadrada um valor aproximado de 5,91. Decompondo 35 em fatores primos, temos 35 = 5*7. Se verificamos a divisão por 5, concluímos que 35 não é número primo. 

Em outras palavras, se não há divisão exata de um número por fatores primos menores que sua raiz quadrada, então esse número é primo.

Dessa forma, evitamos (e muito) a computação do nosso algoritmo quando um número é primo. Por exemplo, o número 997 é primo e, para concluir esse fato pelo nosso algoritmo, testamos a divisão de 997 por 167 números primos abaixo dele. Porém, a raiz quadrada de 997 é aproximadamente 31,57, o que nos indica que a verificação é necessária até o número 31, que corresponde a 11 números primos. Percebe a diferença? Estamos excluindo muitas verificações desnecessárias, tornando o algoritmo mais eficiente.

Então, vamos implementar essa melhoria. E será fácil: basta implementar uma condição em que o algoritmo para quando a verificação considerar um número primo maior que a raiz quadrada do número testado. Vejamos:
    for(i in 1:length(v_primos)){
      if(v_primos[i]>sqrt(v_num[count])) break;
      if(v_num[count]%%v_primos[i]==0){
        testa_primo = F;
        break;
    }
Perceba que há uma condição após o comando for que verifica se o número primo do vetor v_primos é maior que a raiz quadrada do número testado, que é o v_num[count]. Se for maior, o for é parado. 

Em termos de tempo, será que nossa otimização fez diferença? Vamos testar. Primeiro, vamos usar um número pequeno: 100, lembrando que o algoritmo vai encontrar todos os números primos de 2 a 100. Para análise de tempo, utilizaremos a função benchmark() do pacote rbenchmark
library(rbenchmark)

numero = 100;
benchmark(calcula_primos(numero),calcula_primos2(numero),replications = 1000,
          columns = c("test", "replications", "elapsed","relative"))

> benchmark(calcula_primos(numero),calcula_primos2(numero),replications = 1000,
+           columns = c("test", "replications", "elapsed","relative"))
                     test replications elapsed relative
1  calcula_primos(numero)         1000    0.20    1.176
2 calcula_primos2(numero)         1000    0.17    1.000

A função benchmark() fez 1000 replicações das funções desejadas e calculou o tempo médio de execução. Podemos ver que nossa otimização melhorou o algoritmo, sendo o tempo relativo da primeira função 1,176 vezes o tempo da segunda função. O tempo apresentado está em segundos.

Mas para números pequenos, ambas funções são rápidas. O que acontece se aumentamos o número de teste? Vamos realizar o teste com os números 1000 e 9000.
numero = 1000;
> benchmark(calcula_primos(numero),calcula_primos2(numero),replications = 1000,
+           columns = c("test", "replications", "elapsed","relative"))
                     test replications elapsed relative
1  calcula_primos(numero)         1000    4.81    2.628
2 calcula_primos2(numero)         1000    1.83    1.000

numero = 9000;
> benchmark(calcula_primos(numero),calcula_primos2(numero),replications = 1000,
+           columns = c("test", "replications", "elapsed","relative"))
                     test replications elapsed relative
1  calcula_primos(numero)         1000  184.79    8.294
2 calcula_primos2(numero)         1000   22.28    1.000
Perceba que, quanto maior for o número, mais tempo nosso algoritmo precisa para executar. Isso ocorre porque o vetor de números primos aumenta e, assim, a verificação da divisão por cada um demorará mais. Note que quando executamos o algoritmo com o número 9000, tivemos uma redução de cerca de 8 vezes de tempo com a função otimizada, passando de 184,79 segundos para 22,28 segundos.

Esse resultado nos mostra a importância da boa programação e de não realizar etapas desnecessárias. Tornar os algoritmos mais eficientes significa obter resultados em menor custo, seja ele de tempo e/ou de memória. Então, apesar de duas funções retornarem o mesmo resultados, é importante escolher a mais eficiente. Sempre tenham isso em mente.

Na próxima aula, vamos implementar um algoritmo interativo, que pergunta ao usuário se, quando chega no final do número fornecido, ele deseja continuar encontrando números primos até um próximo número máximo fornecido. Assim, poderemos encontrar números primos além do primeiro valor máximo fornecido sem ter que recalcular os números primos já encontrados.

Então, até a próxima aula!

quarta-feira, 5 de janeiro de 2022

Encontrando números primos - parte I

Um número é primo se tem como divisor o número 1 e ele mesmo. Apesar de a definição ser simples, encontrar números primos é um processo complicado. Por exemplo, pense em um número primo que você conhece. Provavelmente você pensou em alguns dos seguintes números: 2, 3, 5, 7, 11, 13. Mas você conseguiria pensar em um número primo de 3 dígitos? E de 6 dígitos? Complicado, não? Nessas horas, o computador é uma ferramenta não apenas útil mas necessária. Com o avanço da computação, podemos realizar testes exaustivos, ou seja, verificar se uma gama de números é primo ou não.

Verificar se um número primo é fácil computacionalmente. Precisamos verificar se algum número, a partir de 2, divide o número desejado. Por exemplo, se queremos saber se 35 é primo, testamos a divisão de 35 por 2, por 3, por 5 e assim sucessivamente. Se achamos um número que divide 35 (no caso, o número 5), então 35 não é primo. Se o único número que divide 35 é ele mesmo (não testamos a divisão por 1, obviamente), então 35 é número primo. Na verdade, podemos parar essa verificação na raiz quadrada desse número, mas falaremos disso mais tarde.

Não se tem, até hoje, uma fórmula para calculá-los. Então, como encontramos os números primos? Simples: verificamos se cada número dado é primo. Começamos pelo número 2, por exemplo. É primo? Sim. Anotamos esse número. Próximo: 3 é primo? Sim e anotamos. Próximo: 4 é primo? Não e não anotamos. Repetimos esse processo até um número máximo a ser testado. Computacionalmente falando, cada número primo é armazenado em um vetor e, para cada número novo a ser testado, verificamos a divisão pelos números do vetor de números primos.

Uma dica: não precisamos analisar se os números pares são primos porque sabemos que são divisíveis por 2. Logo, verificamos apenas os números ímpares. Vamos exemplificar  esse método com o número máximo 35. Escrevemos um vetor de números ímpares de 3 a 35:

3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35

Não é necessário escrever o número 2, pois sabemos que é primo e não precisamos testar divisão sobre ele, pois já excluímos todo número par. Vamos começar a verificação:

_3 é número primo? Sim. Armazenamos no vetor de número primos.

3
_5 é número primo? Vamos verificar: testa a divisão por 3 - não é divisível. Então 5 é primo e armazenamos no vetor de número primos.

3 5
_7 é número primo? Vamos verificar: testa a divisão por 3 - não é divisível. Testa a divisão por 5 - não é divisível. Então 7 é primo e armazenamos no vetor de número primos.

3 5 7
_9 é número primo? Vamos verificar: testa a divisão por 3 - é divisível. Então 9 não é primo e passamos para o próximo número (11).

O processo é repetido até acabar o vetor de números a serem testados. Esse processo é bem trabalhoso à mão, mas computacionalmente é bem rápido. 

Vamos implementá-lo no R. Começamos criando o vetor de número a serem testados. Vamos criar um vetor de números ímpares de 3 a 101, como vemos abaixo.
v_num = seq(3,101,by=2);
> v_num
 [1]   3   5   7   9  11  13  15  17  19  21  23  25  27  29  31  33  35  37  39
[20]  41  43  45  47  49  51  53  55  57  59  61  63  65  67  69  71  73  75  77
[39]  79  81  83  85  87  89  91  93  95  97  99 101
Vamos criar também o vetor que armazenará os números primos:

v_primos = NULL;
v_primos[1] = 3;
Para facilitar nossa programação, já inseri o número 3 no vetor. Como não há numeros pares, é desnecessário armazenar o 2 nesse vetor. Ao final do algoritmo, retornamos para o usuário esse vetor mais o número 2. (na verdade, estamos otimizando o algoritmo, uma vez que evitamos fazer o cálculo da divisão por 2 no teste de cada número). 

Agora, para cada número no vetor v_num, verificamos a divisão por cada número do vetor v_primos. Se há alguma divisão exata, o número testado não é primo. Caso contrário, é primo e o armazenamos no vetor v_primos. Fazemos isso com comandos de repetição:
count = 1;
while(count<=length(v_num)){
   testa_primo = T;
   for(i in 1:length(v_primos)){
      if(v_num[count]%%v_primos[i]==0){
         testa_primo = F;
         break;
      }
   }
   if(testa_primo==T) v_primos = c(v_primos,v_num[count]);
   count = count + 1;
}
Note que consideramos, inicialmente, que o número testado é primo (testa_primo = T). Caso haja alguma divisão exata do número por algum dos números do vetor v_primos, então o número testado não é primo, computamos testa_primo = F e saímos do loop, pois já verificamos que o número não é primo (e, portanto, não é necessário mais testes de divisão). Caso não haja divisão exata por nenhum número do vetor v_primos, então o número testado é primo e o acrescentamos no vetor v_primos.

Se executarmos o código acima, calculamos os números primos de 3 a 101. Ao final, acrescentamos o número 2 ao vetor v_primos para retornar o resultado ao usuário, como vemos abaixo:
v_primos = c(2,v_primos)
v_primos

> v_primos
 [1]   2   3   5   7  11  13  17  19  23  29  31  37  41  43  47  53  59  61  67
[20]  71  73  79  83  89  97 101
Pronto! Temos um algoritmo para calcular números primos de 2 a um número máximo. Porém, nosso código está meio solto. Vamos construir uma função:
calcula_primos = function(num_max){
  v_num = seq(3,num_max,by=2);
  v_primos = NULL;
  v_primos[1] = 3;
  
  count = 1;
  while(count<=length(v_num)){
    testa_primo = T;
    for(i in 1:length(v_primos)){
      if(v_num[count]%%v_primos[i]==0){
        testa_primo = F;
        break;
      }
      
    }
    if(testa_primo==T) v_primos = c(v_primos,v_num[count]);
    count = count + 1;
  }
  
  v_primos = c(2,v_primos)
  return(v_primos)
}
O argumento num_max da função é o valor máximo do vetor de números ímpares. Vamos testar nossa função com num_max igual a 50.
> calcula_primos(50)
 [1]  2  3  5  7 11 13 17 19 23 29 31 37 41 43 47
Temos então todos os números primos de 2 a 50. 

 No próximo post vamos otimizar esse código e torná-lo interativo com o usuário. 

Até a próxima aula!