Teoria dos Números – Aplicações Práticas na
Computação
Aqui apresentamos algumas aplicações da Teoria dos Números na Computação. Para saber mais detalhes, recomendamos a leitura das páginas 205 a 208 e de 241 a 244 do livro de K. Rosen e as páginas 424 a 440 do livro de E. R. Scheinerman (2a edição).
1. Criptografia
• Esta é a aplicação mais importante, provavelmente. A criptografia métodos para “esconder” (encriptar ou cifrar) o conteúdo de mensagens transmitidas pela internet. A aritmética modular e os números primos estão envolvidos em vários destes
métodos. Veremos dois.
• Em todos os métodos, assumiremos que cada símbolo (letras, pontuação, etc.) da mensagem tem um número natural que o representa. Por exemplo, se as mensagens usarem apenas as 11 letras de A a K, podemos adotar esta representação:
A B C D E F G H I J K
0 1 2 3 4 5 6 7 8 9 10
De fato, o computador representa textos simples (planos) usando uma representação numérica chamada de ASCII ou a representação UNICODE. (Clique nos links para mais detalhes).
1.1 Código de César
• A figura abaixo ilustra o processo para o nosso alfabeto completo (de A a Z):
• Por exemplo, podemos codificar mensagens do alfabeto de 11 letras (de A a K) definido antes usando um deslocamento de k=3 letras. Assim, cada letra x será codificada com a seguinte função:
C(x) = (x + 3) mod 11
Dessa forma, a letra A (representação: 0) será encriptada como um D (representação: 3) e a letra K (representação: 10) será encriptada como um C (representação: 2).
• Para decriptar um caractere y é muito simples, basta usar a função:
D(y) = (y - 3) mod 11
Assim, a letra D (representação: 3) torna-se novamente um A (representação: 0) e a letra C (representação: 2) volta a ser um K (representação: 10, pois (2-3) mod 11 = -1 mod 11 = 10).
• Os problemas com o código de César são:
o O código é fácil de quebrar (exemplo: para um hacker), pois é fácil descobrir o k que foi usado. É possível criar um programa que testa todas as possibilidades, por exemplo.
• Diferente do método de César, nos métodos ditos de criptografia de chave pública:
o Todo mundo pode saber a função de encriptação. Tal função é chamada de chave pública.
o Mas a função de decriptação é uma chave privada que só um dos lados deve conhecer. Idealmente, ela deve ser difícil de ser descoberta, mesmo conhecendo-se a chave pública.
• Um método de chave pública serve, por exemplo, para uma aplicação de banco pela internet ou qualquer outra que exija comunicação segura.
o O banco divulga a chave pública para o seu navegador, que ele usa para encriptar a sua senha (ou outra mensagem que você tenha enviado).
o A sua senha é enviada criptografada pela internet e, assim, se um hacker “escutar” os dados que você transmitiu, não vai descobrir sua senha.
o No servidor do banco, a senha (ou outra mensagem que você envie) é decriptada pela chave secreta que só o banco conhece.
• Veremos a seguir um método de criptografia de chave pública.
1.2 Cripto-Sistema de Rabin
• Criado por Michael Rabin, um ganhador do prêmio Turing (o “Nobel” da
• Vamos dar um exemplo adequado ao alfabeto de 11 letras definido antes. Não podemos fazer o resto da divisão por 11, porque 11 não é o produto de dois primos. Usando o valor 21 (que é 3 × 7), a codificação é feita assim:
C(x) = x2 mod 21
Assim, teremos:
o a letra A (representação: 0) é encriptada como o código 0 o a letra C (representação: 2) é encriptada como o código 4 o a letra K (representação: 10) é encriptada como o código 16
• Os dois números primos em questão (3 e 7, no exemplo) formam a chave privada. Conhecendo os dois, é possível decriptar rapidamente a mensagem.
o Não vamos ver como é feita a decriptação, porque usa várias técnicas de aritmética modular. Veja o livro de Scheinerman.
• Porém, se você não conhece os dois números primos usados e se os dois números primos forem “gigantes”, com centenas de algarismos cada, então o processo de decriptação será imensamente difícil.
o Será preciso fatorar o número (21, no exemplo) para obter os dois números primos que o compõem.
o Porém, fatoração é um problema para o qual ainda não existem algoritmos muito eficientes.
o Se o número for grande (o que não é o caso do exemplo acima), um hacker poderia levar milhões de anos tentando fatorá-lo!
• Um método mais preciso e que é bastante usado na prática é o método RSA. Se tiver
interesse, leia sobre ele no livro do Rosen ou do Scheinerman.
2. Geradores de Números Aleatórios
• Em algumas aplicações, é desejável que o programa se comporte de maneira aparentemente aleatória (ao acaso), apresentando alguma variação limitada nas suas saídas. Alguns exemplos:
o Em jogos (games) de futebol: o passe ou o chute nem sempre saem
perfeitos. A direção da bola pode variar (pouco ou muito, dependendo do jogador).
o Em simulações de trânsito: o percurso, a velocidade ou a velocidade de
reação dos carros devem variar entre si e devem variar um pouco a cada simulação.
o Em simulações em geral: tipicamente, deseja-se introduzir variações que
tornem a simulação mais realista.
o Em testes de software: para criar entradas diversificadas para testar se um
programa está funcionando corretamente.
• Esse tipo de variação pode ser obtido usando os chamados geradores de números pseudo-aleatórios. Eles geram uma sequência de números (um por vez, geralmente)
usando um método exato, mas é difícil prever os valores futuros da sequência (exceto para quem sabe o método).
• Um tipo de gerador de números aleatórios simples e muito usado é o gerador congruente linear, que pode ser criado assim:
o Primeiro escolha um valor semente (seed), para dar início à seqüência.
Exemplo:
o Para gerar o próximo valor (rn), multiplique o valor anterior (rn-1) por um valor inteiro a, some um valor c e tire o resto da divisão por m. Usando a=7, c=3, m=31, teríamos:
rn = (7.rn-1 + 3) mod 31
o A seqüência produzida seria:
2, 17, 29, 20, 19, 12, 25, 23, 9, 4, 0, 3, 24, 16, 22, 2, 17, 29, ...
• A sequência gerada é infinita, mas é cíclica. Os valores estão todos no intervalo de 0 a m-1. No máximo, este gerador gera exatamente os m valores distintos do intervalo citado, mas pode acontecer de nem todos os valores deste intervalo serem gerados.
o Para aumentar a quantidade de valores distintos gerados, o m deve ter um valor grande (232, por exemplo). Mas para gerar todos os m valores possíveis, é preciso obedecer mais alguns critérios na escolha dos valores de a, c e m. Vejam o Wikipedia para mais detalhes. (Desafio: crie, em Python,
um gerador que gere todos os m valores possíveis, para algum m de sua escolha entre 100 e 1000).
o No exemplo dado antes, os critérios não foram satisfeitos. Por isso, ele só gera 15 valores distintos, de forma cíclica. Veja que os três valores finais da seqüência listada já são a repetição dos três valores do início.
• A vantagem do gerador congruente linear é que ele é simples de implementar e usa um mínimo de memória. Por isso, são bastante usados e já vêm implementados por padrão em várias linguagens de programação.
• Porém, existem outros geradores melhores, que não veremos em detalhes.
3. Funções Hash
o Em alguns métodos de criptografia o Na compactação de arquivos
o Para implementar conjuntos (como os sets de Python e o HashSet de Java) o Para implementar dicionários (como os de Python e o HashMap de Java) o Etc.
• Vamos falar de funções hash usadas numa aplicação específica:
o Suponha que em uma aplicação, você deseja guardar as fichas de cadastro dos clientes de uma loja. Além disso, as fichas devem ser indexadas por CPF, ou seja, dado um CPF, você deve identificar rapidamente a ficha.
Apenas para comparar: se você criar uma lista de “fichas de cadastro” de forma ineficiente, você vai ser obrigado a percorrer toda a lista, o que seria ineficiente.
o Suponha que você só quer manter N cadastros de pessoas na memória. O restante ficará guardado em um banco de dados (pode ser em arquivo).
Por que fazer isso? Porque manter na memória torna o acesso mais rápido. Isso é um tipo de memória cache. Porém, nela, não cabem todas as fichas.
Assim, quando for dado um CPF que não está na memória, você consulta o banco e põe a ficha na memória. Para isso, talvez você precise descartar (desalocar) outra ficha que estava na memória.
o O problema é: que estrutura usar para manter N pessoas indexadas pelos seus CPFs na memória de forma eficiente?
o Nós vamos usar um simples array de tamanho fixo N, indexado de 0 a N-1.
Hash(CPF) = CPF mod N
o Assim, você tem, para cada CPF, o índice do array onde a ficha do cliente pode ser guardada!
o Porém, pode haver colisão de hash: quando dois CPFs têm o mesmo valor
hash e, portanto, são associados ao mesmo índice do array.
o Se isso acontecer, uma estratégia que pode ser adotada é: você apaga o cadastro que estava antes naquele índice e guarda somente o novo cadastro. (Lembrando que o cadastro é apagado da memória, mas permanece armazenado no banco de dados).
• Uma idéia bem próxima a essa é usada para implementar tabelas hash, tais como os
dicionários de Python e o HashMap e Hashtable de Java. A diferença é que não há desalocação nestas estruturas.
o Neste caso, e se houver conflito: como resolver? Deixo para vocês pesquisarem!
“Longe de vós, toda amargura, e cólera, e ira,
e gritaria, e blasfêmias, e bem assim toda malícia.
Antes, sede uns para com os outros benignos,
compassivos, perdoando-vos uns aos outros,