Chilled Classics

Propaganda
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
Download