Vous êtes sur la page 1sur 38

13 Um Programa para

Jogar no Totoloto

13.1 Introdução
Existem duas maneiras por meio das quais os trabalhadores honestos podem, se não en-
riquecer, pelo menos ganhar a sua vida condignamente: vendendo a sua força de trabalho e
vendendo o produto do seu trabalho. No primeiro caso, temos os trabalhadores por conta
de outrem, no segundo, os trabalhadores por conta própria. Com os programadores não é
diferente. Há os que são funcionários de empresas produtoras ou utilizadoras de informáti-
ca, que fazem os programas que os chefes mandam, e os “independentes” (infelizmente
uma minoria), que fazem os programas que gostam de fazer e que, imaginam eles, possam
ter utilidade para outras pessoas, e depois os tentam vender, normalmente através de pe-
quenas empresas que criam eles próprios, em saudável competição com a concorrência e
sob a constante ameaça dos piratas de software.
No entanto, a programação tem uma vantagem muito importante em relação às restan-
tes profissões: é que as suas competências específicas, aliadas à acessibilidade permanente
de computadores, permitem contemplar uma terceira alternativa: a invenção de programas
capazes de aumentar as probabilidades de ganhar nos jogos de azar. Qual o programador
que nunca sonhou em escrever um programa para acertar no totobola ou no totoloto?
Neste capítulo, que por sorte é o número 13, vamos estudar um programa para jogar no
totoloto. Não se trata de um programa mágico que, dado o dia do sorteio, calcule os núme-
ros que vão sair (até porque se eu soubesse fazer esse programa, guardava-o só para mim,
com certeza). O objectivo, muito mais modesto, é gerar automaticamente, por computador,
desdobramentos de apostas múltiplas. Ainda que a utilidade prática disto seja duvidosa, no
que se refere ao aumento da probabilidade de acertar, o programa pode pelo menos facili-
tar o trabalho àqueles que gostam de jogar com múltiplas desdobradas. Seja como for, é
um problema algorítmico interessante, cuja resolução faz intervir alguns dos conceitos
“mais avançados” do Pascal, nomeadamente a recursividade, os registos com variantes, os
vectores conformes, os parâmetros funcionais e ainda algumas construções de utilização

 FCA - EDITORA DE INFORMÁTICA 585


Pascal – Técnicas de Programação

mais especializada e menos frequente, como as instruções goto, as operações sobre conjun-
tos e os tipos enumerados.

13.2 Teoria do totoloto


Em benefício dos leitores menos familiarizados com o totoloto, aqui vão algumas in-
formações acerca deste jogo. Essencialmente, o que se passa é o seguinte: cada semana a
organização extrai aleatoriamente seis números do intervalo de números inteiros entre 1 e
49. Os jogadores tentam adivinhar os números que vão sair e fazem apostas nesse sentido.
Uma aposta é um conjunto de seis números diferentes escolhidos naquele intervalo. Cada
jogador faz quantas apostas quiser. O primeiro prémio é dividido entre os jogadores que
adivinharem exactamente os números saídos no sorteio.
Além do primeiro prémio, existem mais quatro prémios: o terceiro, dividido entre os
apostadores que acertem em cinco dos números saídos; o quarto, para os que acertem em
quatro; e o quinto, para os que acertem em três. Falta ainda o segundo prémio, que é atri-
buído de maneira diferente: depois dos seis números principais, extrai-se um número dito
suplementar. Partilham o segundo prémio os apostadores que acertem em cinco dos núme-
ro principais mais o número suplementar.
O sorteio é mesmo um sorteio e por isso pode sair qualquer conjunto de números, com
igual probabilidade. Assim, a probabilidade de uma qualquer aposta acertar nos seis núme-
ros que vão sair é o inverso do número de extracções possíveis e estas são as combinações
de 49, 6 a 6, ou seja 13983816.
O dizer-se que todas as combinações são igualmente prováveis faz confusão a muita
gente: então a chave “1 2 3 4 5 6” é tão provável como, por exemplo, “4 12 21 26 35 41”?
Claro que sim, dirá um matemático! É por isso que os matemáticos são pobres, temos de
retorquir! Para jogar e ganhar é preciso ter “fé” e está-se mesmo a ver que só por uma
grande coincidência é que sairiam os números todos de seguida! Estamos a misturar duas
coisas diferentes, tentaria esclarecer o matemático: cada chave tem a mesma probabilidade
de sair que qualquer outra e essa probabilidade é precisamente 1/13983816. Por outro
lado, a probabilidade de sair uma chave com os números todos de seguida é 44/13983816,
pois só há 44 chaves dessas (“1 2 3 4 5 6”, “2 3 4 5 6 7”, …, “44 45 46 47 48 49”), en-
quanto a probabilidade de sair uma chave em que os números não estão todos de seguida é
a complementar, ou seja, 13983772/13983816 = 0.99999685!
Esta breve discussão mostra que pela matemática não vamos lá. Esqueçamos a teoria
das probabilidades e confiemos na nossa intuição e na sorte!

13.3 Os computadores no totoloto


Os computadores podem ajudar-nos a jogar no totoloto, de muitas maneiras. Uma será
memorizar para nós todas as chaves saídas, pois “não vale a pena” apostar nelas outra vez.
Outra será controlar os números que estão a dar mais e gerar automaticamente chaves com

586  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

preponderância desses números. Ou, inversamente, ver quais são os que têm primado pela
ausência e insistir neles, porque devem estar mesmo a sair!
No entanto, este tipo de assistência é muito enfadonho de implementar, porque implica
ter a história do Totoloto e mantê-la em dia, semana a semana, e isso exige um espírito
organizado, coisa de que os programadores habitualmente não dispõem (ao contrário do
que às vezes se quer fazer crer…)
Outra hipótese é fazer desdobramentos de chaves múltiplas. Jogar com uma chave múl-
tipla equivale a jogar com mais de seis números de cada vez. Se jogássemos com sete nú-
meros, em vez de seis, por exemplo, as probabilidades de acertar nos seis que vão sair
multiplicavam-se por sete, porque jogar com sete números equivale a fazer sete apostas
simples (isto é, com seis números). Se escolhermos um grau de multiplicidade m, a proba-
bilidade de ganhar torna-se igual ao quociente das combinações de m, 6 a 6, pelas combi-
nações de 49, 6 a 6, e é crescente com m, claro. Para m = 20, por exemplo, este quociente
já dá 0.00277, o que parece querer dizer que ao fim de cerca de sete anos é “praticamente
garantido”! O pior é que tem de se investir muito dinheiro nisto, pois é preciso pagar as
apostas feitas e uma múltipla de 20 equivale a 38760 simples! (De facto, não há milagres e
ao fim de sete anos ter-se-ia gasto o mesmo que numa aposta múltipla de 49, esta sim, a
única que dá garantias de não falhar…)
Ora é aqui que entra o computador. Das apostas todas que se gerarem pelo desdobra-
mento da aposta múltipla, muitas “nunca vão sair”, porque têm um aspecto particular de-
mais. O que nós queremos é apostas regulares, com os números evidentemente bem distri-
buídos e não apostas em que todos os números fossem pares ou inferiores a 20 ou em pro-
gressão aritmética, por exemplo. Portanto, o que temos de fazer é estabelecer os critérios
de aceitação de uma aposta e depois gerá-las todas, uma a uma, descartando as que não
passam nos testes e guardando as que passam. Trata-se de uma operação óptima para
computadores porque podemos imaginar os critérios que quisermos, por muito elaborados
que sejam, deixando para eles a tarefa rotineira, cansativa e exigente de descobrir quais as
apostas que satisfazem esses critérios. Os testes que implementam os critérios de selecção
desta natureza chamam-se filtros e dizemos que uma dada aposta passa ou não passa num
determinado filtro.
Se não formos muito gananciosos e nos contentarmos com o terceiro prémio, que já é
bom, ou mesmo com o quarto, que tipicamente pagará o que se gastou com a aposta, po-
demos imaginar critérios de selecção suplementares de outra natureza. Com efeito, mesmo
com os filtros todos a funcionar, é natural que passem apostas muito parecidas, só diferin-
do num número. Dadas duas apostas destas, se uma delas acertar, a outra terá o terceiro
prémio (se não o segundo, com um bocado mais de sorte…). Isso seria óptimo, é claro,
mas como a ideia é fazer selecções apertadas para permitir jogar com mais números pelo
mesmo dinheiro, podemos achar que não vale a pena ambicionar muitos prémios de cada
vez. Um só já não era mau. Portanto, o que se deverá fazer é impedir que passem duas
apostas que só difiram num número. Chama-se a isto fazer um condicionamento ao cinco
e significa que se acertarmos em seis com a chave múltipla, então ou existe uma chave no
desdobramento com os seis números certos ou existe uma com cinco (o que dá o terceiro

 FCA - EDITORA DE INFORMÁTICA 587


Pascal – Técnicas de Programação

prémio, pelo menos), isto fazendo abstracção de outros mecanismos de selecção, bem en-
tendido. O condicionamento ao quatro é do mesmo género, mas agora satisfazemo-nos com
um quarto prémio, para o que basta que no desdobramento não haja pares de chaves que
difiram em um ou dois números.
Perante isto, as tarefas do programa ficam bem determinadas: aceitar a chave múltipla,
os filtros e os condicionamentos, fazer o desdobramento de acordo com essas indicações e
imprimir os resultados.
No entanto, é conveniente ter uma ideia dos números envolvidos com as apostas múlti-
plas, para não lançar um desdobramento que leve tempo demais a calcular e também para
se poder avaliar com mais rigor quais são as verdadeiras probabilidades de ganhar qual-
quer coisa. Por isso, na secção seguinte, vamos ver um programa para calcular combina-
ções e só depois atacamos o totoloto propriamente dito.

13.4 Combinações de n, k a k
Eis um programa muito ingénuo para calcular as combinações de n, k a k:
program CombinacoesProg(input, output);
var n, k: integer;

function Factorial(n: integer): integer;


var
p, i: integer;
begin
p := 1;
for i := 2 to n do
p := p * i;
Factorial := p
end;
function Combinacoes(n, k: integer): integer;
begin
Combinacoes := Factorial(n) div (Factorial(k) * Factorial(n-k))
end;
begin
write('Valores para n e k? ');
readln(n, k);
if (n >= k) and (k >= 0) then
writeln ('Combinações de ', n: 1, ', ', k: 1, ' a ', k: 1, ' ', Combinacoes(n, k): 1)
else
writeln('Valores inválidos')
end.
Este programa, se bem que formalmente correcto, é “inexecutável”, a não ser para valo-
res de n e k muito pequenos, pois, como é sabido, os factoriais crescem muito depressa e
cedo deixam de ser representáveis por meio dos números inteiros dos computadores. Por

588  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

isso, é preciso ser capaz de calcular as combinações sem usar os factoriais. A solução que
ocorre logo é usar a definição recursiva:
Cn,0=1 (Cn,k denota as combinações de n, k a k)
Cn,n=1
Cn,k=Cn-1,k-1+Cn-1,k

function Combinacoes(n, k: integer): integer;


begin
if (k = 0) or (k = n) then
Combinacoes := 1
else
Combinacoes := Combinacoes(pred(n), pred(k)) + Combinacoes(pred(n), k)
end;
Esta função também não é lá grande coisa, pois qualquer resultado é calculado por
meio de somas e as parcelas elementares são sempre 1. Por outras palavras, o resultado
equivale ao número de chamadas recursivas, o que é um exagero!
A maneira convencional de calcular as combinações de maneira sistemática e eficiente é
o chamado triângulo de Pascal e baseia-se na anterior relação de recorrência, mas usa-a de
maneira menos directa. Eis as primeiras linhas do triângulo de Pascal:
1
1 1
1 2 1
1 13 3
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 5 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
Cada elemento de cada linha, excepto o da esquerda e o da direita, é igual à soma dos
dois elementos “mesmo por cima”, propriedade que de facto traduz a relação de recorrên-
cia. Para todos os efeitos, o triângulo de Pascal é uma matriz triangular, em que os elemen-
tos da primeira coluna e os da diagonal principal valem 1 e os restantes abaixo da diagonal
verificam a relação seguinte:
TP[i, j] := TP[pred(i), pred(j)] + TP[pred(i), j]
Os que estão acima da diagonal não têm interesse e podemos decretar que valem zero, o
que até faz algum sentido e facilita a construção da matriz.
Eis um programa para afixar no terminal as primeiras linhas do triângulo de Pascal,
sem preocupações de formatação:
program TrianguloDePascal(input, output);
const
maxTP = 48;
maxTP1 = 50;

 FCA - EDITORA DE INFORMÁTICA 589


Pascal – Técnicas de Programação

var
TP: array [0..maxTP, 0..maxTP] of integer;
n: integer;
procedure TPCalcular(n: integer);
{pre: n <= maxTP}
var i, j: integer;
begin
for i := 0 to n do
TP[i, 0] := 1;
for j := 1 to n do
TP[0, j] := 0;
for i := 1 to n do
for j := 1 to n do
TP[i, j] := TP[pred(i), pred(j)] + TP[pred(i), j]
end;
procedure TPAfixar(n: integer);
var i, j: integer;
begin
for i := 0 to n do
begin
for j := 0 to i do
write(TP[i, j]);
writeln
end
end;
begin
write('Triângulo de Pascal. Quantas linhas? ');
readln(n);
if n > maxTP1 then
n := maxTP1;
TPCalcular(pred(n));
TPAfixar(pred(n))
end.
Repare-se em como a inicialização da primeira linha com zeros (excepto a primeira po-
sição) facilita a construção das linhas seguintes. No entanto, isso faz-se à custa de muitas
adições com zero, desnecessárias portanto. Não querendo perder tempo a somar “em seco”,
podemos reprogramar assim:
procedure TPCalcular(n: integer);
{pre: n <= maxTP}
var
i, j: integer;
begin

590  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

for i := 0 to n do
begin
TP[i, 0] := 1;
for j := 1 to pred(i) do
TP[i, j] := TP[pred(i), pred(j)] + TP[pred(i), j];
TP[i, i] := 1
end
end;
Se o que queremos é só afixar o triângulo, então a matriz não é indispensável. Com
efeito, cada linha depende apenas da linha anterior e, portanto, resolvia-se o problema com
dois vectores apenas, desde que cada linha fosse impressa logo que estivesse calculada,
assim:
procedure TriPascal(n: integer);
var
a, b: array [0..maxTP] of integer;
i, j: integer;
begin
for i := 0 to n do
begin
a[0] := 1;
for j := 1 to pred(i) do
a[j] := b[pred(j)] + b[j];
a[i] := 1;
for j := 0 to i do
write(a[j]);
writeln;
b := a
end
end;
Mas com um pouco mais de esforço consegue-se ainda melhor, pois, vendo bem, a va-
riável auxiliar b não faz falta nenhuma. Repare-se: no início de cada passo do ciclo for
exterior, os valores dos vectores b e a são idênticos. A afectação a[j] := b[pred(j)] + b[j]
exprime que o novo valor de a[j] é a soma dos valores de a[pred(j)] e a[j] no início deste
passo. Isso também se conseguiria escrevendo a[j] := a[pred(j)] + a[j], desde que se
garantisse que no momento de cada uma destas afectações (isto é, para cada valor de j) os
elementos que constituem as duas parcelas ainda têm os valores antigos, o que não é evi-
dente, pois a afectação tem como efeito mudar o valor de um dos elementos do vector, o
qual, portanto, deixa de poder ser usado no resto do ciclo. Quer dizer, quando se calcula
a[j], é preciso que a[pred(j)] ainda tenha o antigo valor. Se puséssemos simplesmente for j
:= 1 to pred(i) do a[j] := a[pred(j)] + a[j], isso não seria assegurado. O que é importa é
ter um ciclo da direita para a esquerda:
procedure TriPascal(n: integer);
var
a: array [0..maxTP] of integer;
i, j: integer;

 FCA - EDITORA DE INFORMÁTICA 591


Pascal – Técnicas de Programação

begin
for i := 0 to n do
begin
a[i] := 1;
for j := pred(i) downto 1 do
a[j] := a[pred(j)] + a[j];
for j := 0 to i do write(a[j]);
writeln
end
end;
A falta de uma instrução a[0] := 1 não é esquecimento: quando i vale zero, é colocado
o valor 1 na posição 0 do vector. Daí para a frente, esse elemento nunca mais é modifica-
do, nem é preciso!
Para terminar esta secção, eis uma função para calcular as combinações, baseada no
triângulo de Pascal:
function Combinacoes(n, k: integer): integer;
var
a: array [0..maxTP] of integer;
i, j: integer;
begin
for i := 0 to n do
begin
a[i] := 1;
for j := min(k, pred(i)) downto 1 do
a[j] := a[pred(j)] + a[j]
end;
Combinacoes := a[k]
end;

13.5 Um programa para o totoloto


13.5.1 Tipos
Vamos então ao que interessa. A análise que já fizemos pôs em evidência alguns con-
ceitos importantes, a saber: aposta, aposta múltipla, desdobramento, filtro e condiciona-
mento. Vejamos a que é que cada um deles corresponde, em termos de programação Pas-
cal.
Uma aposta simples é um conjunto de seis elementos escolhidos no intervalo [1..49].
Uma aposta múltipla é um conjunto com mais do que seis elementos escolhido no mesmo
intervalo. Estamos a ver que os subconjuntos de números do intervalo de 1 a 49 têm muita
importância no problema. Merecem um tipo só para eles:
type
Int49 = 1..49;
Numeros = set of Int49;

592  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

Não é impossível que outros totolotos tenham mais do que 49 números (ou menos) e
por isso se calhar devíamos usar um identificador de constante em vez do 49. No entanto,
tal só serviria para atrapalhar o desenvolvimento e, portanto, vamos continuar a trabalhar
com o 49, que é um número que sobressai e que é facilmente localizável no programa.
A cardinalidade de uma aposta é o que permite distinguir as apostas múltiplas das sim-
ples. Eis uma função para a calcular:
function Card(n: Numeros): integer;
var
s: integer;
i: integer;
begin
s := 0;
for i := 1 to 49 do
if i in n then
s := succ(s);
Card := s
end;
(Como curiosidade, refira-se que esta função é predefinida em algumas implementações
do Pascal.)
Mais cedo ou mais tarde, vai ser preciso afixar no terminal ou escrever num ficheiro de
texto os elementos de um conjunto de números, com certeza. Complementarmente, alguns
dos conjuntos irão ser definidos interactivamente a partir do terminal ou simplesmente
lidos a partir de um ficheiro de texto. Eis dois procedimentos para fazer isso:
procedure EscreverNumeros(var f: text; n: Numeros);
var
i: integer;
begin
for i := 1 to 49 do
if i in n then
write(f, ' ', i: 1)
end;
procedure LerNumeros(var f: text; var n: Numeros);
var
i: integer;
begin
n := [];
while not eoln(f) do
begin read(f, i);
if i in [1..49] then
n := n + i;
SaltarBrancos(f)
end;
readln(f)
end;

 FCA - EDITORA DE INFORMÁTICA 593


Pascal – Técnicas de Programação

O procedimento LerNumeros espera todos os números na mesma linha, sendo o fim da


lista de números a incluir no conjunto determinado pelo fim da linha. Repare-se que não é
impossível que existam alguns espaços depois do último número e por isso o fim da linha
poderia não ser alcançado apenas por efeito da instrução read. É necessário saltar explici-
tamente os espaços que se seguem a cada número para contemplar devidamente essa even-
tualidade e é isso que faz o procedimento SaltarBrancos.
Na escolha dos filtros é onde entra a “fé”. Podemos imaginar vários tipos de filtros. Um
primeiro grupo são os filtros de conjuntos: é muito pouco natural que todos os números
saídos sejam pares ou múltiplos de três ou números primos, por exemplo. Mas, por outro
lado, podemos arriscar que pelo menos um múltiplo de cinco há-de sair ou uma potência de
dois. Estes filtros podem ser representados por conjuntos de números, dos quais se preten-
de que saiam pelo menos uns tantos e quanto muito outros tantos. Donde o seguinte tipo:
type
FiltroConj = record
conjunto: Numeros;
minimo, maximo: integer
end;
A função para descobrir se uma aposta passa num filtro destes é a seguinte:
function PassaFiltroConj(a: Numeros; f: FiltroConj): boolean;
begin
with f do
PassaFiltroConj := Card(a * conjunto) in [minimo..maximo]
end;
Um outro grupo de filtros, nos quais é preciso ter mesmo uma grande fé, são os chama-
dos filtros de boletim, que são originados pela disposição dos números no boletim da apos-
ta. Com efeito, os números estão arrumados assim:
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31 32 33 34 35
36 37 38 39 40 41 42
43 44 45 46 47 48 49
Ora com um arranjo destes é “pouco provável” que saiam todos os números na mesma
linha ou na mesma coluna. (No entanto, como a chave tem só seis números, alguma coluna
e alguma linha têm de ficar a branco.) Donde a importância do filtro de linha e do filtro de
coluna, que indicam o máximo de números que devem sair por linha e por coluna, respecti-
vamente. Podemos observar que a arrumação dos números no boletim está muito bem feita

594  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

e as colunas são formadas pelos números congruentes módulo 7 e as linhas pelos números
cujos predecessores são congruentes “quociente” 7.
Parecidos com estes são o filtro das dezenas e o das terminações. O primeiro especifica
o número máximo de números saídos dentro de cada dezena (isto é, de 1 a 10 de 11 a 20,
etc.) e o segundo, o número máximo de números com o mesmo algarismo das unidades.
Generalizando estes dois casos, podemos considerar a classe dos filtros de quociente e a
classe dos filtros de resto. Definimo-las assim:
type
FiltroQuoc = record
divisor: integer;
minimo, maximo: integer
end;
FiltroResto = record
divisor: integer;
minimo, maximo: integer
end;
Os dois tipos têm a mesma estrutura (e os mesmos nomes para os campos, até), mas
são diferentes e não faria sentido afectar um filtro de quociente a um filtro de resto, por
exemplo.
No nosso afã de obter uma aposta tão perfeita quanto possível, é razoável excluir apos-
tas com mais do que uns tantos números em diagonal. Surgem assim os filtros de diagonal,
descendente e ascendente. Mas, muita atenção: é preciso compreender que a diagonal “su-
perior” 5 13 21 é a mesma que a diagonal “inferior” 29 37 45, por exemplo. (Imagine-se o
boletim prolongado infinitamente para a direita, com a sequência 8 9 10 etc. repetida a
seguir ao 7, com 15 16 17 etc. a seguir ao 14, etc., e isso fica evidente). Quer dizer, os
filtros de diagonal descendente e de diagonal ascendente são afinal filtros de resto com
divisor 8 e 6, respectivamente.
As funções para ver se uma aposta passa num filtro de cada um destes tipos são como
se segue. O procedimento inc serve para incrementar de uma unidade o valor do seu argu-
mento (é muito útil quando a variável é o campo de uma estrutura, pois simplifica o ende-
reçamento):
procedure inc(var i: integer);
begin
i := succ(i)
end;
function PassaFiltroQuoc(a: Numeros; f: FiltroQuoc): boolean;
label 88;
var
c: array [0..49] of integer;
i, j: integer;
begin
with f do

 FCA - EDITORA DE INFORMÁTICA 595


Pascal – Técnicas de Programação

begin
for i := 0 to pred(divisor) do
c[i] := 0;
for j := 1 to 49 do
if j in a then
inc(c[pred(j) div divisor]);
PassaFiltroQuoc := false;
for i := 0 to pred(divisor) do
if not (c[i] in [minimo..maximo]) then
goto 88;
PassaFiltroQuoc := true;
end;
88:
end;
function PassaFiltroResto(a: Numeros; f: FiltroResto): boolean;
label 88;
var
c: array [0..49] of integer;
i, j: integer;
begin
with f do
begin
for i := 0 to pred(divisor) do
c[i] := 0;
for j := 1 to 49 do
if j in a then
inc(c[j mod divisor]);
PassaFiltroResto := false;
for i := 0 to pred(divisor) do
if not (c[i] in [minimo..maximo]) then
goto 88;
PassaFiltroResto := true;
end;
88:
end;
Repare-se na utilização das instruções goto, conjugadas com as afectações ao identifi-
cador das funções, para abandonar os cálculos logo que um dos testes falha.
O passo seguinte é reunir todos estes tipos filtro num tipo único, o tipo Filtro. Vamos
usar registos com variante e para isso é conveniente ter um tipo enumerado com as classes
de filtro:

596  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

type
FiltroClasse = (conjunto, quociente, resto);
Filtro = record
minimo, maximo: integer;
case queClasse: FiltroClasse of
conjunto: (oConjunto: Numeros);
quociente, resto: (oDivisor: integer);
end;
Este registo com variante exibe uma estrutura mais rica do que os que têm aparecido
até agora: é formado por uma parte fixa com dois campos e por uma parte variante com
campo-rótulo e duas alternativas, a segunda das quais com duas constantes discriminantes.
Para criar objectos destes, vamos precisar de três procedimentos, um para cada varian-
te:
procedure CriarFiltroConjunto(var f: Filtro; n: Numeros; m1, m2: integer);
begin
with f do
begin
minimo := m1;
maximo := m2;
queClasse := conjunto;
oConjunto := n
end
end;
procedure CriarFiltroQuociente(var f: Filtro; d, m1, m2: integer);
begin
with f do
begin
minimo := m1;
maximo := m2;
queClasse := quociente;
oDivisor := d
end
end;
procedure CriarFiltroResto(var f: Filtro; d, m1, m2: integer);
begin
with f do
begin
minimo := m1;
maximo := m2;
queClasse := resto;
oDivisor := d
end
end;

 FCA - EDITORA DE INFORMÁTICA 597


Pascal – Técnicas de Programação

Se há um tipo único para os filtros, também haverá uma única função de filtração.
Aproveitamos para agrupar nela o código das funções PassaFiltroQuoc e PassaFiltroRes-
to, que era muito semelhante. Repare-se na relação entre duas funções FDiv e FMod e os
operadores aritméticos div e mod:
function FDiv(x, m: integer): integer;
begin
FDiv := pred(x) div m
end;
function FMod(x, m: integer): integer;
begin
FMod := x mod m
end;
function PassaFiltro(a: Numeros; f: Filtro): boolean;
function PFQR (m1, m2, d, lim: integer;
function f(x, m: integer): integer): boolean;
label 88;
var
c: array [0..49] of integer;
i, j: integer;
begin
for i := 0 to lim do
c[i] := 0;
for j := 1 to 49 do
if j in a then
inc(c[f(j, d)]);
PFQR := false;
for i := 0 to lim do
if not (c[i] in [m1..m2]) then
goto 88;
PFQR := true;
88:
end;
begin
with f do
case queClasse of
conjunto : PassaFiltro := Card(a * oConjunto) in [minimo..maximo];
quociente: PassaFiltro := PFQR(minimo, maximo, oDivisor, 49 div oDivisor, FDiv);
resto: PassaFiltro := PFQR(minimo, maximo, oDivisor, pred(oDivisor), FMod);
end
end;
Esta função determina se uma aposta passa num filtro. No programa haverá normal-
mente vários filtros em vigor, provavelmente dispostos em vector. Quer dizer: faz falta
uma função para ver se uma aposta passa nos filtros todos:

598  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

function PassaTodosFiltros(a: Numeros;


var f: array [i1..i2: integer] of Filtro;
nf: integer): boolean;
label 88;
var
i: integer;
begin
PassaTodosFiltros := false;
for i := i1 to nf do
if not PassaFiltro(a, f[i]) then
goto 88;
PassaTodosFiltros := true;
88:
end;
Termina assim a definição dos filtros. Vejamos agora o condicionamento. Na base desta
operação está o cálculo do número de números em que duas apostas diferem. Ora isso
implementa-se com uma função:
function Distancia(a, b: Numeros): integer;
begin
Distancia := 6 - Card(a * b)
end;
O condicionamento é uma função booleana com três argumentos: um inteiro, represen-
tando o grau do condicionamento, um vector de apostas (acompanhado pelo número de
apostas presentes no vector) e uma aposta. Dará verdade se nenhum dos elementos do vec-
tor se distanciar da aposta mais do que a diferença do grau para 6 ou, o que é o mesmo, se
nenhum tiver com a aposta uma intersecção com cardinalidade superior ou igual ao grau:
function Condicionada(g: integer;
var v: array [i1..i2: integer] of Numeros;
nv: integer;
a: Numeros): boolean;
label 88;
var
i: integer;
begin
if g >= 6 then
Condicionada := true
else
begin
Condicionada := false;
for i := i1 to nv do
if Card(v[i] * a) >= g then
goto 88;
Condicionada := true;

 FCA - EDITORA DE INFORMÁTICA 599


Pascal – Técnicas de Programação

88:
end
end;
Dos conceitos identificados no início da análise, falta discutir o de desdobramento. Tra-
ta-se de gerar todas as combinações dos números presentes na aposta múltipla, seis a seis.
Veremos mais à frente algumas maneiras de o fazer. Para já, é claro que o resultado do
desdobramento é o jogo, isto é, a lista das apostas simples que se procurava. A melhor
maneira de implementar esta lista é com um vector de apostas, acompanhado do número de
apostas desdobradas:
const
maxApostas = 2048; {por exemplo}
type
Jogo = record
apostas: array [1..maxApostas] of Numeros;
nApostas: 0..maxApostas;
end;
Quais são as operações abstractas sobre jogos. Facilmente reconhecemos o seguinte
grupo: uma para começar um novo jogo, uma para acrescentar mais uma aposta, uma para
ver se a capacidade do jogo já se esgotou (o vector está cheio) e uma para escrever o jogo
num ficheiro de texto. Ei-las:
procedure NovoJogo(var j: Jogo);
begin
j.nApostas := 0
end;
procedure AcrescentarAposta(var j: Jogo; a: Numeros);
begin
with j do
begin
nApostas := succ(nApostas);
apostas[nApostas] := a
end
end;
function JogoEsgotado(var j: Jogo): boolean;
begin
JogoEsgotado := j.nApostas = maxApostas
end;
procedure EscreverJogo(var f: text; var j: Jogo);
var
i: integer;
begin
with j do

600  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

begin
for i := 1 to nApostas do
begin
EscreverNumeros(f, apostas[i]);
writeln(f)
end
end
end;
A anterior função Condicionada foi programada genericamente, em termos de um vec-
tor de Numeros. Aproveitemos o balanço e especializamo-la para o caso do tipo Jogo:
function CondicionadaJogo
(g: integer; var j: Jogo; a: Numeros): boolean;
begin
CondicionadaJogo := Condicionada(g, j.apostas, j.nApostas, a)
end;

13.5.2 Variáveis
Vistos os tipos de dados e as operações, é altura de fazer a declaração das variáveis. Os
dados são a aposta múltipla que se quer desdobrar e os resultados são o vector das apostas
simples seleccionadas:
var
multipla: Numeros;
esteJogo: Jogo;
Cada jogador vai poder usar os filtros que quiser (se não tiver cuidado e for exigente
demais, pode ser que nenhuma aposta passe, mas isso é lá com ele). O mais simples será
ter um vector de filtros:
const
maxFiltros = 16;
var
osFiltros: array [1..maxFiltros] of Filtro;
nFiltros: 0..maxFiltros;
O condicionamento pode ser representado por um número inteiro. Os únicos valores ra-
zoáveis são 4 e 5, como explicámos na secção 2, ainda que outros valores inferiores pos-
sam ser usados, por conta e risco do jogador. Um condicionamento superior ou igual a seis
equivale à ausência de condicionamento:
var
cond: integer;
Vamos ter um programa interactivo, que começa por pedir todas as informações de que
necessita, nomeadamente a aposta múltipla e os filtros, faz os cálculos e depois escreve
num ficheiro de texto a lista das apostas que passaram no teste. Por isso, precisamos de

 FCA - EDITORA DE INFORMÁTICA 601


Pascal – Técnicas de Programação

três variáveis ficheiro: as pré-declaradas input e output e uma para o ficheiro dos resulta-
dos:
var fRes: text;

13.5.3 Interface
Numa fase preliminar do programa é preciso aceitar a aposta múltipla, definir os fil-
tros e estabelecer o condicionamento. A primeira operação é simples e faz-se usando o
procedimento LerNumeros. A terceira também, pois envolve apenas a leitura de um núme-
ro inteiro. A segunda é mais complicada porque é indispensável dar ao jogador a
possibilidade de usar os filtros standard oferecidos pelo programa e também de definir os
seus próprios filtros (dentro das três classes previstas) e isso exige um diálogo elaborado.
Os filtros standard devem servir para cobrir as necessidades mais usuais. Por isso va-
mos oferecer filtros de pares, de múltiplos de 3, de 4 e de 5, de potências de 2, de números
primos, de números de Fibonacci, estes na classe dos filtros de conjunto, e ainda os filtros
de linha, de coluna, de dezena e de terminação, de diagonal descendente e de diagonal as-
cendente. Que mais não seja para efeitos de documentação, não fica mal ter no programa a
lista destes filtros, por meio de um tipo enumerado:
type
FiltroStandardTipo = (pares, mult3, mult4, mult5, pot2, primos, fibonacci,
linhas, colunas, dezenas, terminacoes,
diagDesc, diagAsc);
Associados a cada um dos primeiros sete elementos deste tipo, existe um conjunto fixo
que vai constituir o conjunto do filtro respectivo. O melhor é definirmos os valores desses
conjuntos de uma vez por todas no início do programa:

type
FiltroConjuntoTipo = pares .. fibonacci;
var
tConjuntos: array [FiltroConjuntoTipo] of Numeros;
procedure InicTConjuntos;
begin
tConjuntos[pares] := [2,4,6,8,10,12,14,16,18,20,22,24,26,
28,30,32,34,36,38,40,42,44,46,48];
tConjuntos[mult3] := [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48];
tConjuntos[mult4] := [4,8,12,16,20,24,28,32,36,40,44,48];
tConjuntos[mult5] := [5,10,15,20,25,30,35,40,45];
tConjuntos[pot2] := [1,2,4,8,16,32];
tConjuntos[primos] := [2,3,5,7,11,13,17,19,23,29, 31,37,41,43,47];
tConjuntos[fibonacci] := [1,2,3,5,8,13,21,34]
end;
A maneira como o utilizador vai escolher os filtros é um dos aspectos mais importantes
da interface do programa. O ideal seria ter um sistema de menus flexível, com os quais se

602  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

poderia seleccionar os filtros que interessassem do elenco dos filtros standard, ou então a
opção filtro à medida. Esta parte do programa consome muito código e não tem nada de
especial, do ponto de vista do totoloto. Apenas para não deixar este exercício incompleto,
vejamos um procedimento de selecção dos filtros, programado para um terminal alfanumé-
rico convencional. A interacção desenrola-se a dois níveis: escolha da classe do filtro e
definição de valores para o filtro. Em ambos os níveis se faz uma validação dos dados e
qualquer anomalia provoca o cancelamento da operação e o regresso ao menu principal.
procedure SeleccionarFiltros;
var
f: Filtro;
resp: char;
ok: boolean;
i: integer;
begin
i := 0;
repeat
writeln('1 - Filtros de conjunto "standard"');
writeln('2 - Filtros de boletim');
writeln('3 - Filtros de conjunto à-medida');
writeln('4 - Filtros de quociente');
writeln('5 - Filtros de resto');
writeln;
writeln('0 - (Fim da selecção)');
write ('Escolha, por favor: ');
readln(resp);
if resp in ['1'..'5'] then
begin
case resp of
'1': DefinirFiltroConjunto (f, ok);
'2': DefinirFiltroBoletim (f, ok);
'3': DefinirFiltroAMedida (f, ok);
'4': DefinirFiltroQuociente(f, ok);
'5': DefinirFiltroResto (f, ok);
end;
if ok then
begin
i := succ(i);
osFiltros[i] := f
end
else
writeln('CANCELADO')
end
until (resp = '0') or (i = maxFiltros);
nFiltros := i;
end;

 FCA - EDITORA DE INFORMÁTICA 603


Pascal – Técnicas de Programação

Os cinco procedimentos DefinirFiltroConjunto, DefinirFiltroBoletim, DefinirFiltro-


AMedida, DefinirFiltroQuociente e DefinirFiltroResto teriam de ser declarados antes. To-
dos eles contêm uma secção de código que se vai encarregar de determinar os mínimos e os
máximos do filtro. Podemos pô-la em evidência:
procedure LimitesFiltro (var m1, m2: integer; var ok: boolean);
begin
write('Limites mínimo e máximo: ');
readln(m1, m2);
ok := (0 <= m1) and (m1 <= m2)
end;
Os procedimentos de filtro de conjunto à medida têm de aceitar um conjunto, mas para
isso já existe o procedimento LerNumeros. Os dos filtros de quociente e de resto precisam
de saber qual é o divisor, conjuntamente com o mínimo e o máximo. Eis um procedimento
para tratar disso:
procedure DivisorLimitesFiltro (var d, m1, m2: integer; var ok: boolean);
begin
write('Divisor: ');
readln(d);
ok := d in [1..49];
if ok then
LimitesFiltro(m1, m2, ok)
end;
Seguem-se os cinco procedimentos para definição de filtros:
procedure DefinirFiltroConjunto(var f: Filtro; var ok: boolean);
var
resp: char;
m1, m2: integer;
begin
writeln('1 - Primos');
writeln('2 - Pares');
writeln('3 - Múltiplos de 3');
writeln('4 - Múltiplos de 4');
writeln('5 - Múltiplos de 5');
writeln('6 - Potências de 2');
writeln('7 - Números de Fibonacci');
writeln;
write ('Escolha, por favor ');
readln(resp);
ok := c in ['1'..'7'];
if ok then
LimitesFiltro(m1, m2, ok);
if ok then
CriarFiltroConjunto(f, tConjuntos[escolha[resp]], m1, m2)
end;

604  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

O vector escolhas, usado na última instrução do procedimento anterior, proporciona


uma maneira conveniente para fazer a conversão entre os caracteres de selecção e índices
da tabela dos conjuntos standard. Ele é declarado e inicializado assim:
var
escolhas: array ['1'..'7'] of FiltroConjuntoTipo;
procedure InicEscolhas;
begin
escolhas['1'] := primos;
escolhas['2'] := pares;
escolhas['3'] := mult3;
escolhas['4'] := mult4;
escolhas['5'] := mult5;
escolhas['6'] := pot2;
escolhas['7'] := fibonacci
end;
Retomemos a programação dos procedimentos dos filtros:
procedure DefinirFiltroAMedida(var f: Filtro; var ok: boolean);
var
resp: char;
m1, m2: integer;
n: Numeros;
begin
write('Indique os elementos do conjunto: ');
LerNumeros(input, n);
write('Os números são: ')
EscreverNumeros(output, n);
writeln;
write('Está certo? ');
readln(resp);
ok := resp in ['S', 's'];
if ok then
LimitesFiltro(m1, m2, ok);
if ok then
CriarFiltroConjunto(f, n, m1, m2)
end;
procedure DefinirFiltroBoletim(var f: Filtros; var ok: boolean);
var
resp: char;
m1, m2: integer;
begin
writeln('1 - Filtro de linhas');
writeln('2 - Filtro de colunas');
writeln('3 - Filtro de dezenas');
writeln('4 - Filtro de terminações');
writeln('5 - Filtro de diagonal descendente');
writeln('6 - Filtro de diagonal ascendente');

 FCA - EDITORA DE INFORMÁTICA 605


Pascal – Técnicas de Programação

writeln;
write ('Escolha, por favor: ');
readln(resp);
ok := resp in ['1'..'6'];
if ok then
LimitesFiltro(m1, m2, ok);
if ok then
case resp of
'1': CriarFiltroResto(f, 7, m1, m2);
'2': CriarFiltroQuociente(f, 7, m1, m2);
'3': CriarFiltroResto(f, 10, m1, m2);
'4': CriarFiltroQuociente(f, 10, m1, m2);
'5': CriarFiltroResto(f, 8, m1, m2);
'6': CriarFiltroResto(f, 6, m1, m2);
end
end;
procedure DefinirFiltroQuociente(var f: Filtro; var ok: boolean);
var
d: integer;
m1, m2: integer;
begin
DivisorLimitesFiltro(d, m1, m2, ok);
if ok then
CriarFiltroQuociente(f, d, m1, m2)
end;
procedure DefinirFiltroResto(var f: Filtro; var ok: boolean);
var
d: integer;
m1, m2: integer;
begin
DivisorLimitesFiltro(d, m1, m2, ok);
if ok then
CriarFiltroResto(f, d, m1, m2)
end;

13.6 Desdobramentos
Na Secção 4 vimos como calcular as combinações de n, k a k. Precisamos agora de as
gerar, pois é essa a essência do desdobramento. Isolemos o problema, procurando um pro-
grama para escrever num ficheiro de texto todas as combinações de n, k a k, para valores
de n e k dados pelo terminal.
Para representar o conjunto cujas combinações queremos, podemos escolher o intervalo
de inteiros entre 1 e n. Deste modo, se n fosse 6 e k fosse 3, por exemplo, as primeiras
linhas do ficheiro resultado seriam assim:

606  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

3 2 1
4 2 1
4 3 1
4 3 2
5 2 1
...
e as últimas assim:

...
6 4 3
6 5 1
6 5 2
6 5 3
6 5 4
Neste caso concreto, com o valor de k fixo e conhecido à partida, isto consegue-se com
três ciclos for encadeados:
procedure GerarCombs(var f: text; n: integer);
var
i1, i2, i3: integer;
begin
for i1 := 3 to n do
for i2 := 2 to pred(i1) do
for i3 := 1 to pred(i2) do
writeln(f, i1: 1, ' ', i2: 1, ' ', i3)
end;
Claro que é impossível generalizar directamente este procedimento se k for variável. No
entanto, a forma dos ciclos for pode inspirar-nos o seguinte esquema recursivo, em que
fazemos abstracção da necessidade de escrever a lista dos elementos de cada combinação:
procedure Combs(n, k: integer);
var i: integer;
begin
if k = 0 then
{nada}
else
for i := k to n do
Combs(pred(i), pred(k))
end;
Por sinal, este esquema proporciona uma maneira alternativa de contar as combinações
de n, k a k, assim:
function Combinacoes(n, k: integer): integer;
var
s: integer;

 FCA - EDITORA DE INFORMÁTICA 607


Pascal – Técnicas de Programação

procedure Combs(n, k: integer);


begin
if k = 0 then
s := succ(s)
else
for i := k to n do
Combs(pred(i), pred(k))
end;
begin
s := 0;
Combs(n, k);
Combinacoes := s
end;
Esta variante da função para o cálculo das combinações fornece-nos a pista para a
questão da escrita dos elementos, pois basta arranjar uma variável global em relação ao
procedimento Combs onde se vão memorizando os elementos a escrever: a memorização
faz-se no ramo else, a escrita no ramo then:
procedure GerarCombs(var f: text; n, k: integer);
var
a: array [Int49] of integer;
procedure Combs(ni, ki: integer);
var
i: integer;
begin
if ki = 0 then
begin
EscreverVector(f, a, k);
writeln(f)
end
else
for i := ki to ni do
begin
a[ki] := i;
Combs(pred(i), pred(ki))
end
end;
begin
Combs(n, k)
end;
Repare-se na necessidade de distinguir os nomes dos parâmetros do procedimento
Combs dos do procedimento GerarCombs, para manter internamente a visibilidade do
parâmetro k.
O procedimento EscreverVector poderia ter sido declarado à parte, pois corresponde a
uma operação abstracta sobre vectores de inteiros e não tem nada de particular a ver com
este problema:

608  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

procedure EscreverVector (var f: text;


var v: array [i1..i2: integer] of integer;
nv: integer);
var
i: integer;
begin
for i := nv downto 1 do
write(f, ' ', v[i]: 1)
end;
Se nos apareceu um procedimento para escrever um vector, o mais certo é daqui a pou-
co surgir a necessidade de um para ler um vector. O melhor é arrumarmos já com ele, para
não ter de voltar atrás mais tarde. Como o procedimento anterior escreve os valores dos
elementos por ordem decrescente dos índices, devemos fazer com que o novo os leia pela
mesma ordem e isso dá origem a uma pequena dificuldade, pois à partida não se sabe
quantos números estão na linha:
procedure LerVector(var f: text;
var v: array [i1..i2: integer] of integer;
var nv: integer);
var
i, n, d: integer;
begin
n := 0;
while not eoln(f) and (n < i2 - pred(i1)) do
begin
read(f, v[i2 - n]);
n := succ(n);
SaltarBrancos(f)
end;
readln(f);
d := i2 - pred(i1) - n;
if d > 0 then
for i := 1 to n do
s[i] := s[i + d];
nv := n
end;
Em relação ao problema que nos interessa, ainda há uns retoques a dar. Já temos um
mecanismo que gera as combinações de n, k a k, sobre o conjunto dos inteiros entre 1 e n.
Não é disso exactamente que andamos atrás: o que nós queremos são desdobramentos de
uma aposta múltipla e nessa os números escolhidos estão salteados. Se representarmos a
aposta por meio de um vector, em substituição do conjunto, então os números gerados
podem servir de índices para obter as apostas desdobradas. Aliás, esta ideia de representar
as apostas por meio de vectores de inteiros é perfeitamente razoável e pode ser considerada
de maneira abstracta, independentemente de vir a calhar neste momento. Com duas repre-
sentações alternativas, justificam-se os procedimentos de conversão de uma na outra:

 FCA - EDITORA DE INFORMÁTICA 609


Pascal – Técnicas de Programação

procedure NumerosVector(n: Numeros;


var v: array [i1..i2: integer] of integer;
var nv: integer);
var
i, j: integer;
begin
j := 0;
for i := 1 to 49 do
if i in n then
begin
j := succ(j);
v[j] := i
end;
nv := j
end;
procedure VectorNumeros(var v: array [i1..i2: integer] of integer;
nv: integer;
var n: Numeros);
var
i: integer;
begin
n := [];
for i := 1 to nv do
n := n + [v[i]]
end;
O procedimento principal do programa é o que se encarrega de fazer o desdobramento,
partindo de uma aposta múltipla e gerando o vector das apostas simples (desdobradas) que
passaram todos os testes e satisfazem o condicionamento em vigor. Estes tratamentos fa-
zem-se no seio de um procedimento recursivo do género do procedimento Combs visto há
pouco. É preciso ter cuidado para não deixar transbordar o jogo. Baseamos esse controlo
no procedimento Confirmar, já usado anteriormente.
procedure Desdobrar(n: Numeros; var j: Jogo);
var
a, m: array [Int49] of integer;
na: integer;
x: Numeros;
procedure Combs(ni, ki: integer);
var
i: integer;
begin
if ki = 0 then
begin
VectorNumeros(a, 6, x);

610  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

if PassaTodosFiltros(x, OsFiltros, nFiltros) then


if CondicionadaJogo(cond, j, x) then
begin
Confirmar(not JogoEsgotado(j), 'Apostas demais');
AcrescentarAposta(j, x)
end
end
else
for i := ki to ni do
begin
a[ki] := m[i];
Combs(pred(i), pred(ki))
end
end;
begin
NumerosVector(n, m, na);
Combs(na, 6)
end;

13.7 Programa principal


Posto isto, resta o programa principal que tem quatro fases distintas: inicialização das
tabelas internas, obtenção dos dados, cálculo e escrita dos resultados. Se tivéssemos segui-
do uma abordagem puramente descendente, se calhar era por estes subproblemas que tí-
nhamos começado. Para ter um código equilibrado, que reflicta bem essa decomposição
estrutural, faltam ainda algumas operações muito simples:
procedure AceitarAposta;
begin
write('Aposta múltipla: ');
LerNumeros(input, multipla)
end;
procedure ObterCondicionamento;
begin
write('Condicionamento');
readln(cond)
end;
procedure CalcularDesdobramento;
begin
NovoJogo(esteJogo);
Desdobrar(multipla, esteJogo)
end;
procedure EscreverResultados;
begin
writeln('Número de apostas: ', esteJogo.nApostas);
EscreverJogo(fRes, esteJogo)
end;

 FCA - EDITORA DE INFORMÁTICA 611


Pascal – Técnicas de Programação

Eis o programa principal:


begin
InicTConjuntos;
InicEscolhas;
AceitarAposta;
SeleccionarFiltros;
ObterCondicionamento;
CalcularDesdobramento;
EscreverResultados
end.
Não vale a pena apresentar o programa completo nesta altura pois ele é uma mera cola-
gem de alguns dos pedaços de programa que fomos escrevendo. Todavia, é interessante
registar a sua estrutura global, pondo em evidência a arrumação das definições e declara-
ções. Aqui está ela:
program TotoLoto(input, output, fRes);
const
maxApostas = ...;
maxFiltros = ...;
type
Int49 = ...;
Numeros = ...;
FiltroClasse = ...;
Filtro = ...;
Jogo = ...;
FiltroStandardTipo = ...;
FiltroConjuntoTipo = ...;
var
multipla: ...;
esteJogo: ...;
osFiltros: ...;
nFiltros: ...;
cond: ...;
tConjuntos: ...;
escolhas: ...;
fRes: text;
{Geral}
procedure inc
procedure SaltarBrancos
function FDiv
function FMod
procedure EscreverVector
procedure Confirmar
{Tipo Numeros}
function Card

612  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

function Condicionada
procedure EscreverNumeros
procedure LerNumeros
procedure NumerosVector
procedure VectorNumeros
{Tipo Filtros}
procedure CriarFiltroConjunto
procedure CriarFiltroQuociente
procedure CriarFiltroResto
function PassaFiltro
function PassaTodosFiltros
{Tipo Jogo}
procedure NovoJogo
procedure AcrescentarAposta
function JogoEsgotado
procedure EscreverJogo
function CondicionadaJogo
{Interface}
procedure InicTConjuntos
procedure InicEscolhas
procedure LimitesFiltro
procedure DivisorLimitesFiltro
procedure DefinirFiltroConjunto
procedure DefinirFiltroAMedida
procedure DefinirFiltroBoletim
procedure DefinirFiltroQuociente
procedure DefinirFiltroResto
{Desdobramento}
procedure Desdobrar
{Estruturais}
procedure SeleccionarFiltros
procedure AceitarAposta
procedure ObterCondicionamento
procedure CalcularDesdobramento
procedure EscreverResultados
begin
...
end.

13.8 Reflexões a posteriori


Agora que o trabalho está feito, reflictamos um pouco sobre o programa a que chegá-
mos e contemplemos algumas alternativas. É inegável que a estrutura do procedimento
Desdobrar é muito subtil. Se em vez do esquema recursivo tivéssemos optado por seis
ciclos for imbricados, as coisas seriam mais elementares, mas o programa ficava com mui-

 FCA - EDITORA DE INFORMÁTICA 613


Pascal – Técnicas de Programação

to menos graça. E repare-se no aspecto aterrador do procedimento com os seis ciclos den-
tro uns dos outros:
procedure Desdobrar(n: Numeros; var j: Jogo);
var
i1, i2, i3, i4, i5, i6: integer;
m: array [Int49] of integer;
na: integer;
x: Numeros;
begin NumerosVector(n, m, na);
for i1 := 6 to na do
for i2 := 5 to pred(i1) do
for i3 := 4 to pred(i2) do
for i4 := 3 to pred(i3) do
for i5 := 2 to pred(i4) do
for i6 := 1 to pred(i5) do
begin
x := [m[i1], m[i2], m[i3], m[14], m[i5], m[i6]];
if PassaTodosFiltros(x) then
...
end
end;
É óbvio que a geração das combinações (a partir das quais se vão obter as apostas des-
dobradas) é um processo de natureza sequencial. Há a primeira combinação, a segunda,
etc., a última. Se fôssemos adeptos dos chamados pseudo-códigos, provavelmente a pági-
nas tantas teríamos escrito qualquer coisa assim:
enquanto houver mais combinações
fazer
obter a próxima combinação;
gerar a aposta;
etc.
fimfazer
Isto é muito parecido com o processamento de um ficheiro sequencial. Podia até sugerir
uma outra decomposição do problema: numa primeira fase geravam-se de seguida todas as
combinações e escreviam-se num ficheiro. Curiosamente, até já temos um procedimento
que faz isso: o procedimento GerarCombs. Depois, o ficheiro seria processado em leitura
pelo procedimento Desdobrar, num ciclo análogo ao do pseudo-código:
procedure EscolherAposta(var indices: array [i1..i2: integer] of integer;
var aposta: array [j1..j2: integer] of integer;
var x: Numeros);
var
i: integer;
begin
x := [];
for i := 1 to 6 do

614  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

x := x + [aposta[indices[i]]]
end;
procedure Desdobrar(var f: text; n: Numeros; var j: Jogo);
var
m: array [Int49] of integer;
comb: array [1..6] of integer;
na: integer;
x: Numeros;
begin
NumerosVector(n, m, na);
while not eof(f) do
begin
LerVector(f, comb);
readln(f);
EscolherAposta(comb, m, x);
if PassaTodosFiltros(x, osFiltros, nFiltros) then
...
end
end;
A decomposição que esta solução consubstancia é um bocado forçada. O ideal seria
sermos capazes de “consumir” as combinações à medida que elas vão sendo “produzidas”,
em vez de termos de as guardar todas no ficheiro. Aliás, é isto que faz a nossa solução
original. No entanto, fá-lo mediante um esquema recursivo assaz elaborado, que se baseia
no jogo dos valores da variável de controlo do ciclo for e dos parâmetros da chamada re-
cursiva. Alternativamente, poderíamos ter encarado a possibilidade de gerar cada combi-
nação a partir da anterior, pois a sequência das combinações exibe um padrão deveras
regular. Precisaríamos ainda de saber qual é a primeira combinação (o que é fácil), de
determinar o fim da sequência e de dispor de uma variável para conter o valor da combina-
ção corrente. Com esta infra-estrutura, não seria difícil gerar a sequência das combinações
de n, k a k, à base de um ciclo while.
Vendo bem, o conjunto de operações que acabámos de identificar é “isomorfo” do con-
junto das operações de leitura de um ficheiro sequencial em Pascal: há um “reset” para
começar, um “eof” para ver se já acabou, um “get” para se passar ao seguinte e uma vari-
ável janela, para conter o elemento corrente. Com as combinações, as coisas podem apre-
sentar-se assim:
var comb: array [Int49] of integer;

procedure ResetCombs(n, k: Int49); ...

function EndOfCombs: boolean; ...

procedure GetComb; ...


O procedimento ResetCombs não é complicado, pois a primeira combinação é formada
pelos números mais pequenos, todos de seguida. Já as outras duas são mais delicadas. Para

 FCA - EDITORA DE INFORMÁTICA 615


Pascal – Técnicas de Programação

a função EndOfCombs poderíamos pensar em arranjar uma variável inteira suplementar, a


inicializar no procedimento ResetCombs com o número de combinações que se vão gerar,
o que é fácil de calcular pelo triângulo de Pascal. Essa variável seria decrementada de uma
unidade por cada chamada de GetComb. Com isto, a função booleana daria verdade se e
só se o valor da variável fosse zero. Ou então, poderíamos implementar uma marca de fim
de combinações, a qual deveria ser gerada pelo procedimento GetComb depois da última
combinação. Estudemos primeiro este procedimento GetComb, para ganhar sensibilidade
sobre qual das soluções será mais conveniente.
O procedimento GetComb não tem parâmetros: o seu efeito faz-se sentir sobre a com-
binação corrente, que muda de valor passando a tomar o valor da combinação seguinte
(isto no caso de não ser a última, claro). A passagem de uma combinação à seguinte, já
ilustrada no início da secção 6, tem semelhanças com o problema da escrita do sucessor de
um número num sistema de numeração convencional. No sistema de base 10, por exemplo,
como é que se passa de um numeral para o seguinte? É uma operação que todos sabemos
fazer perfeitamente, mas que exige um momento de reflexão para descrever. Em termos de
programação Pascal, o que se quer é um procedimento que, dada uma cadeia de caracteres
formada exclusivamente por algarismos, representando um número inteiro n, a transforme
na cadeia que representa o número n+1. Informalmente, o que se tem de fazer é percorrer a
cadeia da direita para a esquerda, à procura do primeiro algarismo diferente de '9'. Esse
algarismo é substituído pelo seu sucessor e todos os que estão para a direita passam a zero:
procedure SuccCad(var s: packed array [i1..i2: integer] of char);
var
i: integer;
begin
i := i2;
while s[i] = '9' do
begin
s[i] := '0';
i := pred(i)
end;
s[i] := succ(s[i])
end;
Este simpático procedimento só funciona se a cadeia não for formada só por noves, cla-
ro. Se uma cadeia assim puder ocorrer, terá de ser tratada de maneira diferente, pois a sua
“sucessora” não é representável. Para detectar uma tal ocorrência, sem testar à partida se
todos os caracteres são '9' e sem mudar muito o procedimento, a melhor solução é acres-
centar um carácter “sentinela” à estrutura de dados. Este carácter suplementar, colocado
na posição de maior peso, deve estar sempre a zero, para não influir no valor representado.
O facto dele passar a '1' é sinal de transbordo da capacidade, um erro do qual porventura
não se pode recuperar. Se assim for, a única saída é parar o programa com uma mensagem
explicativa e para isso podemos usar o procedimento Confirmar:
procedure SuccCad(var s: packed array [i1..i2: integer] of char);
var
i: integer;

616  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

begin
i := i2;
while s[i] = '9' do
begin
s[i] := '0';
i := pred(i)
end;
s[i] := succ(s[i]);
Confirmar(s[i1] = 0, 'Transbordo de cadeia numérica')
end;
Regressemos às combinações, armados com a experiência ganha nesta questão das ca-
deias numéricas. Vendo bem, para passar à combinação seguinte, o que se faz é incremen-
tar o elemento de menor índice que pode ser incrementado, isto é, cujo valor é inferior em
pelo menos duas unidades ao do elemento imediatamente à sua esquerda; os elementos à
sua direita tomam os menores valores que podem tomar, os quais coincidem com os
respectivos índices. Portanto, numa primeira aproximação:
{Atenção: isto é só uma tentativa}
procedure GetComb;
var
i: integer;
begin
i := 1;
while comb[succ(i)] - comb[i] = 1 do
begin
comb[i] := i;
i := succ(i)
end;
comb[i] := succ(comb[i])
end;
É claro que isto não está bem, pois a expressão comb[succ(i)] na condição do ciclo
while não tem sentido quando succ(i) vale mais do que o número de elementos na combi-
nação. Isso acontece quando os números são todos de seguida e, em particular, quando a
combinação é a última, caso que tem de ser devidamente detectado, não esqueçamos. A
maneira mais airosa de evitar este problema é fazer como nas cadeias numéricas, acrescen-
tando um elemento nas combinações, de modo a que com ele a condição do while dê falso
de certeza. Mas isso obriga-nos a rever a declaração da variável comb:
var
comb: array [1..50] of integer;
Por hipótese, não queremos gerar combinações de mais de 49 elementos.
Note-se que a posição que vai conter o elemento que funciona como sentinela é a de or-
dem k+1, quando se geram as combinações de n, k a k. A inicialização desse elemento deve
ser feita no âmbito do procedimento ResetCombs. O fim das combinações pode ser detec-
tado quando o elemento de ordem k se tornar superior a n, em virtude da execução do pro-

 FCA - EDITORA DE INFORMÁTICA 617


Pascal – Técnicas de Programação

cedimento GetComb. Nessa altura, a combinação corrente não tem significado, o que não
faz mal nenhum, pois ninguém irá usá-la. (É uma situação semelhante à dos ficheiros em
leitura, em que a variável-janela fica indefinida quando se atinge o fim do ficheiro.) Portan-
to, é fundamental reter os valores de n e de k, ao longo do processamento. O melhor é as-
sociá-los ao vector das combinações, por meio de um registo.
Feitas estas observações, eis a programação da variável comb e das três operações
ResetCombs, EndOfCombs e GetComb.
var
comb: record
elems: array [1..50] of integer;
nn, kk: integer;
end;
procedure ResetCombs(n, k: Int49);
var
i: integer;
begin
with comb do
begin
for i := 1 to k do
elems[i] := i;
elems[succ(k)] := maxint;
nn := n;
kk := k
end
end;
function EndOfCombs: boolean;
begin
EndOfCombs := comb.elems[kk] > comb.nn
end;
procedure GetComb;
var i: integer;
begin
with comb do
begin
i := 1;
while elems[succ(i)] - elems[i] = 1 do
begin
elems[i] := i;
i := succ(i)
end;
elems[i] := succ(elems[i])
end
end;

618  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

Note-se que este conjunto de operações não suporta o caso de k valer zero, o que é uma
pena. Podemos considerar que é um caso marginal, com o qual não vale a pena perder
tempo, até porque há uma dificuldade intrínseca em processá-lo: as combinações de n, 0 a
0 são apenas uma, a combinação vazia! No entanto, a consideração dos casos extremos é
muitas vezes esclarecedora e revela deficiências na solução encontrada. Uma boa solução é
uma solução geral, sem casos particulares! Ora este conjunto de operações não serviria
sequer para calcular de maneira iterativa as combinações de n, k a k, para n e k quaisquer,
entre 0 e 49!
Em vez de tentar remediar com uma análise por casos, à base de instruções if-then-else,
tentemos atacar os problemas pela raiz. De facto, há dificuldades tanto na função
EndOfCombs como no procedimento GetComb e elas são consequência da decisão de de-
tectar o fim das combinações por meio da análise da configuração dos elementos da com-
binação corrente num momento em que eles já não são significativos. Era muito mais sau-
dável deixar ao cuidado do procedimento GetComb a indicação da impossibilidade de ge-
rar mais uma combinação, por meio de um parâmetro de saída booleano ou por meio de
uma mudança de valor de um campo booleano a juntar na variável comb. Optando por
esta segunda alternativa, podemos dispensar a função EndOfCombs, passando a aceder
directamente ao campo do registo.
Quanto ao elemento suplementar no vector das combinações, esqueçamo-lo por um
momento, pois a simplificação que ele introduzia perdeu-se, por se tornar impossível man-
ter a diferença entre dois elementos na condição do ciclo, uma vez que nem sempre haverá
dois elementos! Nestas condições, a procura do primeiro par de elementos consecutivos
que difiram de mais de uma unidade faz-se com a ajuda de uma variável booleana, como
de costume.
var
comb: record
elems: array [Int49] of integer;
nn, kk: integer;
endOfCombs: boolean;
end;
procedure ResetCombs(n, k: integer);
var i: integer;
begin
with comb do
begin
for i := 1 to k do
elems[i] := i;
nn := n;
kk := k;
endOfCombs := false
end
end;
procedure GetComb;
var i: integer; encontrou: boolean;

 FCA - EDITORA DE INFORMÁTICA 619


Pascal – Técnicas de Programação

begin
with comb do
begin
i := 1; encontrou := false;
while (i < kk) and not encontrou do
if elems[succ(i)] - elems[i] > 1 then
encontrou := true
else
begin
elems[i] := i;
i := succ(i)
end;
if i = kk then
if elems[kk] < nn then
encontrou := true;
if encontrou then
elems[i] := succ(elems[i])
else
endOfCombs := true
end
end;
É um bocado frustrante abandonar aquele bonito ciclo da primeira versão do procedi-
mento GetComb (“while comb[succ(i)] - comb[i] = 1 do ...”) e trocá-lo por esta confu-
são com a variável booleana encontrou e o campo booleano endOfCombs no registo comb.
Recordemos que o ciclo caiu por nem sempre se poder garantir que há pelo menos dois
elementos no vector. Mas será que, com um pouco de jeito, não se consegue dar a volta a
mais esta dificuldade? Claro que sim: basta usar não uma, mas duas sentinelas, a primeira
na posição de ordem k+1 e a segunda na posição de ordem k+2. Quanto a valores iniciais
para essas sentinelas, os melhores candidatos são n+1 e maxint, respectivamente, pois
com eles o fim das sequências pode ser detectado quando o valor da primeira sentinela
passar a n+2, o que é muito prático. Tenhamos paciência e reescrevamos tudo de novo:
var
comb: record
elems: array [1..51] of integer;
nn, kk: integer;
end;
procedure ResetCombs(n, k: integer);
var
i: integer;
begin
with comb do

620  FCA - EDITORA DE INFORMÁTICA


Um Programa para Jogar no Totoloto

begin
for i := 1 to k do
elems[i] := i;
elems[k+1] := succ(n);
elems[k+2] := maxint;
nn := n;
kk := k
end
end;
function EndOfCombs: boolean;
begin
EndOfCombs := comb.elems[succ(comb.kk)] > succ(comb.nn)
end;
procedure GetComb;
var
i: integer;
begin
with comb do
begin
i := 1;
while elems[succ(i)] - elems[i] = 1 do
begin
elems[i] := i;
i := succ(i)
end;
elems[i] := succ(elems[i])
end
end;
Repare-se que, depois da chamada do procedimento ResetCombs, a função EndOf-
Combs dá falso, sempre, o que é uma consequência de haver pelo menos uma combinação
para quaisquer valores de n e k. Aliás, esta observação indica-nos que o ciclo apropriado
para o processamento é o ciclo repeat e não o ciclo while, como temos vindo a considerar.
Nestes termos, o procedimento Desdobrar ficaria assim:
procedure Desdobrar(n: Numeros; var j: Jogo);
var
m: array [Int49] of integer;
na: integer;
x: Numeros;
begin
NumerosVector(n, m, na);
ResetCombs(na, 6);

 FCA - EDITORA DE INFORMÁTICA 621


Pascal – Técnicas de Programação

repeat
EscolherAposta(comb.elems, m, x);
if PassaTodosFiltros(x) then
...
GetComb
until EndOfCombs
end;
Resta fazer um comentário em relação à possibilidade de usar um campo do registo
comb para manter o número de combinações que falta gerar. Não haveria grande vanta-
gem nisso, porque as sentinelas continuariam a fazer falta e elas já fornecem “de borla” o
mecanismo para detectar o fim das combinações. Para mais, ao calcular o número de com-
binações logo à partida, correríamos o risco de ser acusados de estar a fazer “batota”, visto
que uma das utilizações deste conjunto de operações poderia ser precisamente contar o
número das combinações…

622  FCA - EDITORA DE INFORMÁTICA

Vous aimerez peut-être aussi