Chilled Classics Primeira parte, Factorial 1. Neste guião vamos programar um conjunto de problemas clássicos de inspiração matemática, começando pelo factorial. O factorial de um número inteiro positivo x, você deve lembrar-se, é o produto de todos os números inteiros entre 1 e x. Por exemplo, o factorial de 5, que em matemática se escreve 5!, é 120 (pois 1·2·3·4·5 vale 120). 2. Em vez de usar um ciclo for, vejamos a formulação recursiva do factorial, que é exemplar: ⎧ x ( x − 1)!, se x > 0 x! = ⎨ ⎩ 1, se x = 0 3. No Eclipse acrescente um novo projecto denominado ChilledClassics_1. Não se esqueça de seleccionar em baixo a opção “Create separate source and output folders”. Crie a classe Classics e nela programe a função recursiva int factorial, recorrendo ao operador de avaliação condicional “?”. 4. Na classe Main escreva uma função de teste testFactorial interactiva, que termina o processamento com Ctlr-Z. Experimente: factorial(1), factorial(5), factorial(10), factorial(20), etc. Qual é o maior número com o qual a nossa função não disparata ao calcular o factorial. Que explicação encontra para este comportamento? 5. E já agora, experimente calcular factorial(-1). O que é que acontece? Qual é a causa disso? Talvez seja melhor incluir um assert que limite o domínio de x a valores não negativos, não é? 6. Como na verdade não conseguimos calcular o factorial de números grandes por esta via, mudemos a função, de maneira a que o resultado seja double em vez de int. Será a nova função factorialDouble. O argumento é int, o resultado é que é double. Assim, conseguimos calcular factorial de números maiores, mas também rapidamente atingimos o máximo dos número double. Descubra então qual é o maior número inteiro cujo factorial conseguimos calcular por esta via. Na função de teste, escreva os resultados em formato de ponto fixo (com o carácter de formatação “f”) e em notação científica (com o carácter de formatação “e”). 7. Repare que, a partir de certa altura, o resultado da função factorialDouble dá “Infinity”. Na verdade, o que acontece é que não é possível representar computacionalmente números arbitrariamente grandes, uma vez que internamente é usado um número finito de bits para representar números double. Que altura é essa, isto é, qual é o maior número cujo factorial conseguimos calcular exactamente? 8. Nas aplicações, quando é preciso o factorial de um número grande, por vezes usa-se a função de Sterling, que aproxima muito bem o factorial. Eis a definição da função de Sterling: Sterling ( x ) = 2π x ⋅ ( x / e) x POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 1 9. Programe a função sterling na classe Classics e experimente na função de teste. Para ter a maior precisão possível, recorra às constantes Math.PI para o “π” e Math.E para o número “e”, base dos logaritmos naturais. 10. Escreva uma função de teste compareFactorialStirling para fazer uma tabela dos valores das duas funções, lado a lado, com uma coluna adicional para o erro relativo da função de Sterling em relação ao factorial calculado da forma tradicional. Calcule a tabela até ao valor considerado no ponto 7. O erro relativo de x em relação a y é o quociente da divisão por y do valor absoluto da diferença entre x e y. 11. Vamos agora à Tarefa A para o Mooshak, que se destina a validar automaticamente todo o trabalho anterior. O seu programa deve ler da consola uma sequência de números inteiros, um em cada linha e escrever na consola, para cada um deles, o factorial tal como calculado pela função factorialDouble, a aproximação dada pela função de Sterling e o erro relativo. Os dois primeiros números são de tipo double e devem ser escritos em notação científica com quatro casas decimais; o erro também é um número double e deve ser escrito formato de ponto fixo, também com quatro casas decimais. Os três números escritos vêm separados por um espaço. Neste exercício todos os números lidos são números entre 0 e 170, inclusive. 12. Como exemplo, considere o seguinte ficheiro de teste: 1 2 4 8 16 32 64 128 13. Neste caso, o ficheiro de saída é o seguinte: 1.0000e+00 9.2214e-01 0.0779 2.0000e+00 1.9190e+00 0.0405 2.4000e+01 2.3506e+01 0.0206 4.0320e+04 3.9902e+04 0.0104 2.0923e+13 2.0814e+13 0.0052 2.6313e+35 2.6245e+35 0.0026 1.2689e+89 1.2672e+89 0.0013 3.8562e+215 3.8537e+215 0.0007 POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 2 Segunda parte, Combinações 14. O factorial é uma função muito importante em matemática, mais concretamente, em estatística e em análise combinatória. Por exemplo, o número de permutações de um conjunto com x elementos é dado por x! Eis, como exemplo, as 24 permutações do conjunto {0, 1, 2, 3}: 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 1 1 2 2 3 3 0 0 2 2 3 3 0 0 1 1 3 3 0 0 1 1 2 2 2 3 1 3 1 2 2 3 0 3 0 2 1 3 0 3 0 1 1 2 0 2 0 1 3 2 3 1 2 1 3 2 3 0 2 0 3 1 3 0 1 0 2 1 2 0 1 0 15. Mais tarde aprenderemos a gerar esta sequência de permutações. Por enquanto sabemos apenas calcular quantas há, usando o factorial. 16. Um outro problema interessante em análise combinatória é o das combinações de n elementos tomados k a k. A fórmula “fechada” para o número de combinações de n, k a k também é conhecida: ⎛n⎞ n! ⎜⎜ ⎟⎟ = ⎝ k ⎠ k!( n − k )! 17. Programe esta fórmula, numa função int combinations(int n, int k). , também na classe Classics e experimente com uma nova função de teste testCombinations, na classe Main. Claro que não podemos calcular para conjuntos grandes, pois estamos limitados pelo factorial de n, mesmo que o resultado para as combinações não seja muito grande /. 18. Existe uma outra fórmula para as combinações, que todos devemos conhecer, e é uma fórmula recursiva: ⎛ n ⎞ ⎛ n − 1 ⎞ ⎛ n − 1⎞ ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟ + ⎜⎜ ⎟⎟, se k > 0 k k k − 1 ⎝ ⎠ ⎝ ⎠ ⎝ ⎠ 19. Para k = 0 ou para n = k, o valor é 1. POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 3 20. Esta fórmula tem uma interpretação muito interessante, que nos poderá servir de inspiração em muitos problemas de programação: queremos formar todas as combinações de n elementos tomados k a k. Pois bem: suponhamos que já resolvemos o problema para os n-1 primeiros elementos, isto é, já sabemos como calcular as combinações de n-1 elementos k a k, para todos os valores de k. Nas combinações de n, k a k, nalgumas entra o n-ésimo elemento, nas outras não entra. Aquelas em que não entra são precisamente as combinações dos n-1 primeiros elementos tomados k a k. Daquelas em que entra, se retirarmos de lá o n-ésimo elemento, obtemos precisamente as combinações dos n-1 primeiros elementos tomados k-1 a k1. O caso de base para a recursividade é o caso em que k é zero: só há uma maneira de escolher zero elementos de um conjunto com n elementos. 21. Programe então a nova função combinationsRecursive usando a fórmula recursiva. Mude o nome da anterior para combinationsClassic. Experimente na função de teste. Para valores mediamente grandes, a função demora muito tempo: vendo bem, a razão é simples. O resultado, qualquer que seja, é calculador somando uma unidade de cada vez, nos casos em que k é zero. Por exemplo, para calcular as combinações de 5, 2 a 2, que são 10, a função é chamada exactamente 10 vezes com k valendo zero ou com k valendo n. 22. Num problema que faça uso intensivo de combinações, o melhor é calcular as combinações todas de uma vez, até um certo valor de n, guardar esses valores num vector, mais exactamente, num vector de vectores, tal que, se o vector se chamar combinations, combinations[x][y] é o número de combinações de x, y a y. Pois bem: declare este vector e programe uma função void computeCombinations(int n) que preenche o vector até à linha n+1. (A primeira linha tem só um elemento: as combinações de zero, zero a zero, que vale 1.) Note que este preenchimento não deve ser feito com as funções anteriormente programadas, mas sim tirando partido de que, ao calcular as combinações de n, k a k, as combinações de n-1, k-1 a k-1 e as combinações de n-1, k a k, já estão calculadas na tabela e podem ser utilizadas. 23. Escreva uma função void writePascalTriangle para escrever a matriz das combinações na consola. Este arranjo de números é conhecido por triângulo de Pascal (http://ptri1.tripod.com/). Para mais informações sobre Blaise Pascal, o grande filósofo e matemático francês do século XVII, veja http://turnbull.mcs.stand.ac.uk/~history/Mathematicians/Pascal.html. 24. A Tarefa B para o Mooshak serve para testar o cálculo das combinações. Deve ler da consola uma sequência de pares de números inteiros não negativos X e Y, 0 <= Y <= X <= 32, dois em cada linha, e, para cada par, escrever numa linha o valor das combinações de X, Y a Y. 25. Como exemplo, considere o seguinte ficheiro de teste: 4 2 6 1 6 2 6 3 6 4 10 3 26. Neste caso, o ficheiro de saída é o seguinte: 6 6 15 20 15 120 POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 4 Terceira parte, crivo de Eratóstenes 27. Eratóstenes foi um notável matemático grego do século III a.C. (http://wwwgroups.dcs.st-and.ac.uk/~history/Mathematicians/Eratosthenes.html). Entre outras coisas, mediu a circunferência da Terra e inventou um engenhoso método para calcular números primos, o chamado crivo de Eratóstenes. 28. A ideia do crivo é muito simples. Suponha que queremos calcular o conjunto de todos os números primos até X. Então começamos com o conjunto [2..X], de todos os números entre 2 e X, e logo retiramos dele todos os números pares, excepto o 2. O primeiro número maior do que 2 que resta é 3, um número primo. Então retiramos todos os múltiplos de 3, excepto o 3. O menor número maior que 3 que resta é 5, um número primo. Fazemos o mesmo com o 5, eliminado os seus múltiplos maiores do que ele, e assim por diante, enquanto houver números para retirar. Quando não houver, todos os números que restam são números primos. 29. Programemos o crivo, que é um outro exercício de programação clássico. Para começar, declare um vector de booleanos na sua classe Classics, de nome sieve. (“Sieve” é a palavra inglesa que quer dizer crivo ou peneira.) A ideia é que no final das operações de construção do crivo se sieve[x] for true, então x é um número primo e se sieve[x] for false, então x não é um número primo. 30. Programe a função void computeSieve (int n) para preencher o crivo tal como Eratóstenes nos ensinou. O valor do argumento representa a dimensão do crivo. (Logo, o último número sobre o qual há informação no crivo será n-1.) 31. Ao construir o crivo, é preciso inicializá-lo com todos os elementos a true. Para retirar um nº do crivo, coloca-se o respectivo elemento a false. Primeiro retira-se zero e um, e depois todos os múltiplos de 2, excepto o próprio 2, com um ciclo for, depois os múltiplos de 3 excepto o próprio 3 e assim por diante. Qual é o nº cujos múltiplos serão retirados na última passagem, após o que o crivo estará pronto? 32. O crivo diz-nos se um número é primo ou não é primo, mas não nos diz directamente qual é o n-ésimo número primo. Podemos calcular isso com um ciclo, ou então preencher um vector com os números primos registados no crivo. Faça isso, acrescentando à classe Classics um vector de números inteiros, primes, e uma função void computePrimes() que o preenche a partir dos dados já calculados no crivo. 33. Não se esqueça de ir testando as suas funções, chamando-as numa função de teste. 34. A Tarefa C é a resolução de um problema que recorre ao crivo de Eratóstenes. Por definição, dois números primos x e y dizem-se primos gémeos, se y = x + 2. O programa lê da consola uma sequência de números inteiros positivos menores do que 100000 e para cada um deles escreve na consola uma linha com o primeiro par de números primos gémeos, separados por um espaço, tal que o primeiro elemento deste par é maior ou igual ao nº lido da consola. 35. Como exemplo, considere o seguinte ficheiro de teste: 10 20 100 101 1000 36. Neste caso, o ficheiro de saída é o seguinte: 11 13 29 31 101 103 101 103 1019 1021 POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 5 Quarta parte, fracções 37. O algoritmo de Euclides serve para calcular o máximo divisor comum de dois números inteiros. Para que serve o máximo divisor comum? Em particular para simplificar fracções. Para isso comece por construir a classe Fraction com construtores, selectores e modificadores apropriados. Uma fracção é um par de números inteiros: o primeiro é o numerador e o segundo é o denominador. 38. Agora vamos construir a classe Euclid. Comece por implementar o algoritmo de Eucides na função int greatestCommonDivisor(int x, int y). 39. Programe a função boolean isSimple (Fraction f) que dá true se a fracção representada no argumento for irredutível. 40. Programe agora a função Fraction simple (Fraction f) que retorna a fracção irredutível equivalente a x. 41. Programe a função Fraction inverse (Fraction f) que inverte a fracção. 42. Programe agora as quatro funções aritméticas sum, difference, product, quotient. Todas têm dois argumentos de tipo Fraction. Todas devem devolver o resultado na forma de uma fracção irredutível. 43. Já agora a potência (função power, é claro): neste caso, o segundo argumento é um número inteiro (positivo, negativo ou zero). 44. E para terminar, la pièce de résistance: no final do século XIX, o matemático alemão Georg Cantor mostrou que há tantos números fraccionários positivos como números naturais. A demonstração é deveras simples: bastou-lhe exibir uma correspondência biunívoca entre os dois conjuntos, o dos números fraccionários positivos e dos números naturais. Para isso ele escreveu os números fraccionários pela seguinte ordem: 1/1, 1/2, 2/1, 1/3, 2/2, 3/1, 1/4, 2/3, 3/2, 4/1, 1/5, 2/4, 3/3, ... 45. De seguida retirou da lista as fracções não simples: por exemplo, 2/2 é uma fracção não simples e é equivalente a 1/1, que já apareceu antes. O mesmo para 2/4 que é equivalente a 1/2, que já apareceu antes. Desta maneira ficam só as fracções irredutíveis e nenhuma escapa. Logo a correspondência está estabelecida. A sequência de fracções assim obtida é a sequência das fracções de Cantor: 1/1, 1/2, 2/1, 1/3, 3/1, 1/4, 2/3, 3/2, 4/1, 1/5, 5/1, 1/6, 2/5, 3/4, ... 46. Pois bem, junte à classe Classics um vector de fracções ArrayList<Fraction> cantor, e preencha-o com as n primeiras fracções de Cantor, por intermédio de uma função void computeCantor (int n); 47. Se quer saber mais sobre Georg Cantor, veja em http://turnbull.mcs.stand.ac.uk/~history/Mathematicians/Cantor.html. 48. Terminemos com a Tarefa D para o Mooshak. O programa lê uma sequência de números da consola, um em cada linha, e para cada um deles, aqui representado por X, 0 <= X <= 100000, escreve na consola dois números: o numerador e o denominador da fracção de Cantor de ordem X. (A primeira fracção, 1/1, é a fracção de ordem zero.) 49. Como exemplo, considere o seguinte ficheiro de teste: 8 9 0 1 38 39 43 45 25 POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 6 50. Neste caso, o ficheiro de saída é o seguinte: 4 1 1 1 8 9 7 1 7 1 5 1 2 3 2 5 12 2 Quinta parte, interface gráfica 51. Esta última parte não é para entregar no Mooshak, mas terá que incluir, no relatório deste guião, evidência que a concluiu, nomeadamente com uns Print Screens. 52. Imagine-se criador de um novo produto a colocar no mercado, com um design inovador. O produto é uma nova calculadora electrónica (a incluir na nova versão do seu sistema operativo preferido) para fracções, com um teclado numérico, zonas de entrada / visualização de operandos (fracções), visor para os resultados e teclas de operações básicas (soma, subtracção, divisão, multiplicação, potência, simplificação, inversão, …). 53. Tire obviamente partido das classes que desenvolveu na parte quatro! 54. E por agora chega de classicismos! POO, 2006-2007 — Guião 3 © Pedro Guerreiro, Fernando Brito e Abreu 7