Académique Documents
Professionnel Documents
Culture Documents
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
mais especializada e menos frequente, como as instruções goto, as operações sobre conjun-
tos e os tipos enumerados.
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
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;
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
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
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;
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;
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;
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
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:
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;
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:
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
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
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
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;
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:
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;
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.
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
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;
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-
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;
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;
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
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);
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…