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