Resolver problemas por moeda ao ar
Ao longo da vida são comuns os casos em que não conseguimos decidir-nos por duas ou mais opções. Nestas situações costuma-se, em jeito de brincadeira, propor resolver o assunto atirando moeda ao ar. Se sair caras ganha a opção A, se for coroas ganha a B. Contudo, esta técnica tem aplicações mais sérias e interessantes no mundo da computação.
Aniversários coincidentes
Um problema típico consiste em determinar a probabilidade de pelo menos 2 pessoas, dum dado conjunto, fazerem anos no mesmo dia. A forma matemática de o resolver não é muito difícil: se existirem N pessoas têm 365^N aniversários possíveis, mas para não haver nenhum repetido, a primeira terá 365 dias, a segunda 364, e por aí fora. Assim temos 1 -
365!/( (365-N)! * 365^N ).
Mas por "moeda ao ar" é mais fácil resolver este problema: simulamos N aniversários e vemos se existe algum coincidente. Depois de repetidas várias vezes esta simulação, o resultado pretendido é o rácio entre o número de casos coincidentes e o total. Eis uma rápida implementação em C:
$ cat x.c
#define K 100000
#define N 20
int main()
{
int a[365], i, j, collisions=0;
srand(time(0));
for (i=0; i<K; i++) {
/* inicio da cada simulação */
memset(a, 0, sizeof(a));
for (j=0; j<N; j++) {
/* simulação dos aniversários */
if (a[rand()%365]++) {
collisions++;
break;
}
}
}
printf("%f\n", (float)collisions/K);
}
$ gcc x.c
$ ./a.out
0.413520
Se calcularmos o valor matemático exacto:
um valor aproximado até a 2ª casa decimal do estimado aleatoriamente. Assim, em 20 pessoas, existem cerca de 41% de hipóteses de duas fazerem anos no mesmo dia.
Esta técnica pode ser alargada a outros campos da matemática. Por exemplo, se emitirmos N coordenadas (x,y) aleatórias, podemos estimar a área de qualquer função dividindo as que caiem "dentro" dessa função pelas que caiem num quadrado de área pré-definida, que a contenha.
Scheduling
Pretendemos que um dada tarefa corra de x em x tempo, mas não queremos associar-lhe qualquer tipo de contadores, ou de modo geral, não queremos manter informação de estado. Mais uma vez a solução aqui passa pela moeda ao ar. Se tivermos um ciclo periódico de 1 segundo, as nossas tarefas podem ser lançadas com a seguinte construção:
while true:
wait 1s
if u() < 0.3: task1() # 1/0.3s = 3.3s
if u() < 0.1: task2() # 1/0.1s = 10 s
Aqui u() retorna um valor aleatório no intervalo [0,1), logo u()<0.3, por exemplo, tem uma probabilidade de acontecer de 30%, ou seja, cada 1/30% vezes que o ciclo corre task1(). Deste modo, a longo prazo, task1() irá correr em média uma vez a cada 3.3s.
Evitar congestão
Sempre que existem muitos acessos ao mesmo recurso, seja processos a competir por CPU num computador, ou carros pelos acessos nas estradas, uma forma de tentar diminuir a congestão consiste em alguém altruísta decidir esperar algum tempo, aleatório, antes de avançar.
Vamos supor N processos que são lançados num dado tempo que não conseguimos controlar. Evitamos problemas de simultaneidade esperando um pouco no início:
proc1_wrapper:
sleep rand()%60 # espero até 60 segundos
exec proc1 # e depois lanço o processo
Desta forma nenhum processo precisa de comunicar com os outros, nem saber sequer que existem. Este modelo é de resto o que existe na ethernet para resolver conflitos de sinal.
Conclusão
Existem muitos mais casos práticos de aplicação de técnicas aleatórias, que, criteriosamente bem usadas, podem salvar o dia. A regra prática é: se for díficil decidir, escolha-se ao acaso.
Carlos Duarte, 15Jul05
cgdarrobasdf-eu.org