Classics

Propaganda
Classics
Primeira parte, Factorial
1. O factorial é um clássico da programação. Todos nós já o programámos, várias
vezes e de várias maneiras, e, de cada vez que aprendermos uma linguagem nova,
uma das primeiras funções a experimentar será sempre o factorial. Já sabemos que
o factorial cresce muito depressa e rapidamente chegamos a números com muitos
algarismos. Pois bem, o nosso primeiro problema de hoje é calcular o menor
número cujo factorial tem pelo menos n algarismos, para um n dado.
2. Como vamos precisar do factorial, comecemos por programá-lo no nosso novo
programa Haskell. Mas, para praticar com listas, usemos não a versão recursiva
“tradicional” mas sim a versão que corresponde à definição directa – o factorial de
x é o produto de todos os números inteiros entre 1 e x – recorrendo à função
product e ao construtor de listas [a..b] que gera a lista [a, a+1, a+2, …, b−1, b].
Mas atenção, que a assinatura deve ser factorial :: Int −> Integer.
3. “Ter n algarismos” é uma maneira indirecta de dizer “ser maior ou igual a 10n−1”,
claro. Logo, vamos precisar da função para calcular a potência de base inteira e
expoente inteiro. Este assunto já foi tratado nas aulas, e ficou até como exercício
programar a função eficientemente, usando um esquema logarítmico. Se ainda não
teve tempo de completar esse exercício, é agora a altura. A função deve chamar-se
y
y/2 2
power, a base é Integer e o expoente é Int. A ideia é que x é (x ) se y for par e é
isso vezes x se y for ímpar.
4. A tarefa A é para submeter a função power. Por exemplo, power 2 6 vale 64, power
55 6 vale 27680640625, power 123 10 vale 792594609605189126649.
5. Regressemos ao nosso problema e generalizemo-lo, para ficar mais simples. Em
vez de calcular o menor número cujo factorial tem mais que n algarismos, calculemos o menor número maior ou igual a x cujo factorial é maior ou igual a y. Será
a função leastFactorialGreaterThanFrom, com dois argumentos, o primeiro Int e o
segundo Integer, e resultado Int. Por exemplo, leastFactorialGreaterThanFrom 3 100
vale 5. De facto, factorial 4 vale 24 e factorial 5 vale 120, o que confirma que o factorial de 5 é o primeiro que vale mais do que 100. Mais exemplos:
leastFactorialGreaterThanFrom 10 5000 vale 10, leastFactorialGreaterThanFrom 1
1000000000000 vale 15.
6. Programemos então esta nova função, usando a técnica da função leastDivisor estudada nas aulas: se o factorial do primeiro argumento for maior ou igual ao segundo, já está; se não, experimenta-se recursivamente o sucessor do primeiro argumento. Como o factorial é crescente, havemos de chegar lá!
7. Com a ajuda das funções power e leastFactorialGreaterThanFrom, podemos resolver o
nosso problema directamente. Trate disso, programando a função
leastFactorialLongerThan, com um argumento de tipo Int, representando o número
de algarismos. Por exemplo, leastFactorialLongerThan 3 vale 5,
leastFactorialLongerThan 10 vale 13, leastFactorialLongerThan 0 vale 0.
8. Para verificar os resultados da nossa função leastFactorialLongerThan com números
grandes, convém ter uma função countDigits para contar os algarismos de um
número, pois não é prático nem seguro contá-los à mão. Assim, depois de calcu2009-01-05
Fundamentos da Programação I, 2007-2008 — Guião 2 © Pedro Guerreiro
1
larmos, por exemplo, leastFactorialLongerThan 200, obtendo 121, podemos calcular
countDigits (factorial 121), que deve dar um número maior ou igual a 200 e
countDigits (factorial 120), que deve dar menos de 200.
9. Depois de estar satisfeito com os seus resultados, submeta as funções
leastFactorialGreaterThanFrom, leastFactorialLongerThan e countDigits na tarefa B.
Submeta também a função power, de novo, porque os testes usam-na.)
10. É verdade que, de posse da função countDigits, podemos resolver o problema inicial por outra via, sem recorrer às potências. Programe isso, numa função
leastFactorialLongerThan', que não é para submissão, mas à qual você se pode referir
no relatório.
Segunda parte, Combinações
11. O factorial é uma função muito interessante na programação, mas também é muito
importante na matemática. Mais concretamente, em estatística e em análise combinatória, o número de permutações de um conjunto com n elementos é dado por
n! 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
12. Mais tarde aprenderemos a gerar esta sequência de permutações. Por enquanto
sabemos apenas calcular quantas há, usando o factorial.
13. Um outro problema interessante em análise combinatória é o das combinações de
n elementos tomados k de cada vez. A fórmula “fechada” para o número de combinações de n, k a k também é conhecida, e mete factoriais:
n
n!
 =
 k  k !(n − k )!
14. Programe esta fórmula, numa função combinations :: Int −> Int −> Integer, mas não
recorra directamente ao factorial, para não fazer contas desnecessariamente. Por
2009-01-05
Fundamentos da Programação I, 2007-2008 — Guião 2 © Pedro Guerreiro
2
exemplo, pode calcular directamente n! / k! (n−k)! usando a função product para
listas de números inteiros.
15. Existe uma outra fórmula para as combinações, que todos devemos conhecer, e é
uma fórmula recursiva:
 n   n − 1  n − 1 
 =
+

 k   k   k − 1
16. Para k = 0 ou para n = k, o valor é 1.
17. 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, noutras 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 o n-ésimo elemento, obtemos precisamente as combinações dos n−1 primeiros elementos tomados k−1 a k−1. O caso de
base para a recursividade é o caso em que k é zero, pois só há uma maneira de
escolher zero elementos de um conjunto com n elementos, e o caso em que n é
igual a k, pois também só há uma maneira de escolher n elementos de um conjunto
com n elementos.
18. Programe então uma nova função combs, para calcular as combinações usando a
fórmula recursiva. Para valores mediamente grandes, a função demora muito tempo: vendo bem, a razão é simples. O resultado, qualquer que seja, é calculado
somando uma unidade de cada vez, nos casos em que k é zero ou igual a n. Por
exemplo, para calcular as combinações de 4, 2 a 2, que são 6, temos a seguinte
sequência de cálculos: combs 4 2 => combs 3 2 + combs 3 1 => (combs 2 2 + combs
2 1) + comb 3 1 => (1 + combs 2 1) + combs 3 1 => (1 + (combs 1 1 + combs 1 0) +
combs 3 1 => (1 + (1 + 1)) + combs 3 1 => (1 + 2) + combs 3 1 => 3 + combs 3 1 =>
3 + (combs 2 1 + combs 2 0) => 3 + ((combs 1 1 + combs 1 0) + combs 2 0) => 3 +
((1 + combs 1 0) + combs 2 0) => 3 + ((1 + 1) + 1 => 3 + (2 + 1) => 3 + 3 => 6.
Quer dizer, em última análise, o valor 6 é obtido somando 1 seis vezes.
19. Submeta as suas funções combinations e combs na tarefa C. Os resultados são
Integer, mas com a função combs não haverá valores muito grandes nos testes, pois
demorariam demasiado tempo a calcular.
20. 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
numa lista, mais exactamente, numa lista de listas, em que a x-ésima lista contém
x+1 valores, as combinações de x tomadas 0 a 0, 1 a 1, etc. até x a x. A parte interessante disto é que cada lista na lista de listas é calculada a partir da anterior,
usando a fórmula do ponto 15. Com efeito, observe a lista das combinações de 4:
[1,4,6,4,1]. A lista das combinações de 5 é [1,5,10,10,5,1]. Quer dizer, cada elemento, excepto o primeiro e o último, que valem sempre 1, é a soma de dois elementos adjacentes na lista anterior. Isto não é por acaso, claro: é uma consequência directa da fórmula.
21. Pois bem, generalizemos esta questão, programando uma função sumAdjacent ::
[Int] −> [Int] que dada uma lista constrói uma outra em que o primeiro elemento e
o último são iguais aos da primeira lista e os outros são a soma de elementos adjacentes desta lista, pela mesma ordem. Por exemplo: sumAdjacent [4,5,6,7] vale
2009-01-05
Fundamentos da Programação I, 2007-2008 — Guião 2 © Pedro Guerreiro
3
22.
23.
[4,9,11,13,7]; sumAdjacent [6] vale [6,6]. A lista vazia dá a lista vazia: sumAdjacent
[] vale [].
Submeta a sua função sumAdjacent na tarefa D.
Se, partindo de uma lista inicial, não vazia, aplicarmos a função sumAdjacent
sucessivamente, construímos um trapézio de números. Chamamos informalmente
“trapézio de números” a uma lista de listas de números, em que o comprimento de
cada lista é superior em uma unidade ao da lista anterior. Programemos então uma
função trapezium que dada uma lista de inteiros Int e um inteiro Int calcula uma lista de listas de inteiros Int, com tantas listas quantas as indicadas pelo segundo
argumento e em que cada uma é obtida da anterior pela função sumAdjacent. Por
exemplo, trapezium [2,3,5] 4 vale [[2,3,5],[2,5,8,5],[2,7,13,13,5],[2,9,20,26,18,5]],
trapezium [] 6 vale [[],[],[],[],[],[]], trapezium [1,2,3] 0 vale [].
24. Fornecendo a lista [1] à função trapezium obtemos o famosíssimo triângulo de Pascal, que tabela eficientemente as combinações todas, até ao nível que quisermos.
Terminemos em grande esta segunda parte do guião, programando a função pascal,
com um argumento de tipo Int, e que constrói o triângulo de Pascal com o número
de linhas indicado pelo argumento. Por exemplo: pascal 5 vale
[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]].
25. Submeta as suas funções trapezium e pascal na tarefa E.
Terceira parte, crivo de Eratóstenes
26. Eratóstenes foi um notável matemático grego do século III a.C. Entre outras coisas fantásticas, mediu a circunferência da Terra e inventou um engenhoso método
para calcular números primos, o chamado crivo de Eratóstenes.
27. 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.
28. Programemos o crivo, que é um outro exercício de programação clássico, pelo
qual passam todos os que ambicionam tornar-se verdadeiros programadores. Para
começar, e generalizando para simplificar, programemos uma função
removeMultiplesOf que retira de uma lista dada todos os múltiplos de um número
dado. Por exemplo removeMultiplesOf 3 [1..10] vale [1,2,4,5,7,8,10].
29. Continuemos, programando a função eratosthenes que aplica o esquema inventado
por Eratóstenes a uma lista qualquer: guarda o primeiro elemento e remove todos
os seus múltiplos; do que obtiver, guarda o segundo, e remove todos os seus múltiplos, e assim por diante, até não haver mais para remover. Por exemplo,
eratosthenes [5,8..64] vale [5,8,11,14,17,23,26,29,38,41,47,53,59,62], eratosthenes
[2,4..40] vale [2].
30. Com isto, construir uma tabela de números primos, guardando o resultado numa
lista, fica simples, não é? Programe então a função primes, tal que primes x dá a lista de todos os números primos até x. Por exemplo, primes 10 vale [2,3,5,7], primes
53 vale [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53], primes 1 vale [].
31. Submeta as suas funções eratosthenes e primes na tarefa F.
32. Para mais informações sobre Eratóstenes, veja http://www-groups.dcs.stand.ac.uk/~history/Mathematicians/Eratosthenes.html.
2009-01-05
Fundamentos da Programação I, 2007-2008 — Guião 2 © Pedro Guerreiro
4
Quarta parte, fracções
33. Nas aulas teóricas estudámos o algoritmo de Euclides para calcular o máximo
divisor comum de dois números inteiros. Para que serve o máximo divisor
comum? Para simplificar fracções, é claro! Em programação, uma fracção é um
par de números inteiros: o primeiro é o numerador e o segundo é o denominador.
O numerador é qualquer, mas o denominador tem de ser positivo, para evitar confusões com o sinal da fracção e com divisões por zero.
34. Nesta parte do guião vamos programar fracções. Quer dizer, vamos programar
aquelas operações com fracções que aprendemos no ensino básico e que tantas
dores de cabeça nos deram na altura: somar, subtrair, multiplicar, dividir e simplificar. Para simplificar, usaremos o máximo divisor comum, claro. Podemos ir buscar a versão de combate que estudámos nas aulas, ou então recorrer à função gcd
do prelúdio.
35. Já observámos que uma fracção é um par de números inteiros. Em Haskell, em
vez de trabalharmos directamente com pares de Int, o que nos obrigaria, por
exemplo, a escrever a assinatura da soma de fracções na forma addFraction :: (Int,
Int) −> (Int, Int) −> (Int, Int), começamos por declarar um tipo Fraction, assim:
type Fraction = (Int, Int)
36. Agora, a assinatura da soma fica mais simples: addFraction :: Fraction −> Fraction
−> Fraction.
37. Para programar, recorremos aos pares. Por exemplo, a soma começa assim:
addFraction :: Fraction −> Fraction −> Fraction
addFraction (x1, y1) (x2, y2) = …
38. Programe então as funções addFraction, subtractFraction, multiplyFraction,
divideFraction e simple. As quatro primeiras representam os operadores aritméticos
habituais e têm dois argumentos de tipo Fraction e resultado de tipo Fraction, e a
última é a função de simplificação, que têm só um argumento. Eis alguns exemplos: addFraction (1,6)(3,4) vale (11,12), subtractFraction (1,6)(3,8) vale (−5,24),
multiplyFraction (4,9)(3,8) vale (1,6), divideFraction (-8,11)(-1,2) vale (16,11).
39. Submeta as suas funções addFraction, subtractFraction, multiplyFraction, divideFraction
na tarefa G.
40. 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 as fracções 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, ...
41. Repare: por construção, todos as fracções estão presentes nesta sequência infinita.
42. De seguida retirou da lista as fracções não simples: por exemplo, 2/2 é uma fracção não simples, que é equivalente a 1/1, a qual 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.
43. O nosso objectivo é construir a lista de Cantor, isto é, a sequência das fracções
irredutíveis, por aquela ordem. Talvez não consigamos a lista infinita, mas conseguiremos uma lista tão grande quanto quisermos e o nosso computador deixar.
44. Primeira questão: dado uma fracção qual é a outra fracção que vem a seguir a ela
na sequência do ponto 40? Programe, com uma função nextFraction.
2009-01-05
Fundamentos da Programação I, 2007-2008 — Guião 2 © Pedro Guerreiro
5
45. Na tarefa H, submeta a sua função nextFraction. Por exemplo, nextFraction (12, 27)
vale (13,26).
46. A seguir, programe uma função fractionsFromTo para construir a lista de todos as
fracções de uma fracção data até outra, admitindo que a segunda vem depois da
primeira, na lista do ponto 40. Por exemplo, fractionsFromTo (4,5) (2,8) vale
[(4,5),(5,4),(6,3),(7,2),(8,1),(1,9),(2,8)].
47. Usando a função fractionsFromTo, programe a função fractionsTo, para construir a
lista de todas as fracções até a fracção dada, começando em (1,1). Por exemplo,
fractionsTo (3,2) vale [(1,1),(1,2),(2,1),(1,3),(2,2),(3,1),(1,4),(2,3),(3,2)].
48. Finalmente, programe a função cantor, para construir a lista de Cantor até uma
fracção dada, removendo as fracções não simples da lista gerada pela função do
ponto fractionsTo. Por exemplo, cantor (5,1) vale
[(1,1),(1,2),(2,1),(1,3),(3,1),(1,4),(2,3),(3,2),(4,1),(1,5),(5,1)].
49. Na tarefa I, submeta as funções fractionsFromTo, fractionsTo e cantor.
50. Para saber mais sobre Georg Cantor, veja http://turnbull.mcs.stand.ac.uk/~history/Mathematicians/Cantor.html.
51. Factoriais, combinações, números primos, fracções: nunca mais se esqueça disto!
2009-01-05
Fundamentos da Programação I, 2007-2008 — Guião 2 © Pedro Guerreiro
6
Download