Problema de Dezembro de 2004: Brick Wall Patterns

solução

Este mês apresento um problema mais fácil que os dois anteriores. Apareceu também numa prova local da Universidade do Porto. A solução é muito simples!

If we want to build a brick wall out of the usual size of brick which has a length twice as long as its height, and if our wall is to be two units tall, we can make our wall in a number of patterns, depending on how long we want it. From the figure one observe that:

How many patterns can you find for a wall of length 4? And, for a wall of length 5?

Problem

Your task is to write a program that given the length of a wall, determines how many patterns there may be for a wall of that length.

Input

Your program receives a sequence of positive integers, one per line, each representing the length of a wall. The maximum value for the wall is length 40. The input terminates with a 0.

Output

For each wall length given in the input, your program must output the corresponding number of different patterns for such a wall in a separate line.

Sample Input

1
2
3
0

Sample Output

1
2
3

Solução

Podemos fazer uma função recursiva muito simples, com um argumento que é o número de bricks que faltam. Vamos chamar a esta função f(n), onde n é o número de bricks que queremos colocar. Os casos base são n=0 e n=1, para os quais há 1 solução apenas. Para os outros n, podemos adicionar um brick vertical a qualquer solução com (n-1) bricks, ou adicionar 2 bricks horizontais a qualquer solução com (n-2) bricks. Por isso, para qualquer n > 1, f(n) = f(n-1) + f(n-2). Implementação (em C) desta solução:

#include <stdio.h>

int f(int n)
{
	if (n == 0 || n == 1)
		return 1;
	else
		return f(n-1) + f(n-2);
}

int main()
{
	int n;
	
	for (;;) {
		scanf("%d", &n);

		if (n == 0)
			break;

		printf("%d\n", f(n));
	}
	return 0;
}

As soluções geradas estão correctas, mas o programa demora um bocado a calcular f(40). Se o input de teste tiver 1000 linhas com 40 (e isso é bem possível, para testar a rapidez do algoritmo) então o programa vai demorar demasiado tempo e não será aceite.

Porque é que demora tanto tempo a calcular f(40)? Vamos ver primeiro como é calculado f(4). f(4) é decomposto em f(3) + f(2); a função f já foi chamada 3 vezes. f(2) é decomposto em f(1) + f(0) e termina; f foi chamada 5 vezes. Mas ainda falta decompor o f(3) chamado ao início... este decompõe-se em f(2) + f(1), e vamos em 7 chamadas. f(2) ainda se decompõe mais duas vezes e temos no total 9 chamadas da função f. Para calcular f(6) vamos calcular f(5) (que se decompõe em f(4) e por isso tem de repetir estas chamadas todas, mais f(3)) e ainda f(4) outra vez! O crescimento do número de chamadas é exponencial. Para f(40) o total de chamadas é 331160281! Impressionante não :)

Agora que já descobrimos porque demora tanto, será que há uma maneira mais rápida de fazer estas contas? Claro que sim não é... a técnica chama-se dynamic programming e consiste em trocar espaço por tempo para reduzir a complexidade de um algoritmo de exponencial para polinomial. O que é que isto quer dizer? Que podemos criar uma solução que vai usar mais memória (para armazenar resultados intermédios e evitar repetição de cálculos para demorar menos tempo. Nem sempre é possível usar esta técnica; isto só é possível quando as soluções do problema podem ser rapidamente calculadas a partir de soluções anteriores.

Neste caso, existem montes de repetições; para calcular f(6) usamos f(5) e f(4), mas f(5) também usa f(4). É aqui que surge a repetição que vamos evitar. Para isso, vamos armazenar a solução de f(4) num array e quando precisarmos já lá está. Podemos fazer ainda melhor; podemos calcular todas as soluções muito rapidamente e guardá-las na memória. Basta lembrar que f(n) = f(n-1) + f(n-2). Vamos ter um array solution[n] com o valor da solução para n bricks. Se já tivermos solution[3] e solution[4], podemos fazer solution[5] = solution[4] + solution[3]. Agora já temos também solution[5], e podemos fazer solution[6] = solution[5] + solution[4]. E sempre assim até 40... simples :)

Implementação (em C):

#include <stdio.h>

int solution[41];

int main()
{
	int n;

	solution[0] = 1;
	solution[1] = 1;
	
	for (n = 2; n <= 40; n++)
		solution[n] = solution[n-1] + solution[n-2];
	
	for (;;) {
		scanf("%d", &n);

		if (n == 0)
			break;

		printf("%d\n", solution[n]);
	}
	return 0;
}

Iniciamos com as soluções base solution[0] = 1 e solution[1] = 1, e usamos um ciclo for para calcular todas as outras até 40. Repare-se que o ciclo principal do programa apenas lê o valor de n da entrada, e escreve na saída um valor do array; isto é muito muito rápido. No caso de ter 1000 vezes o valor 40 na entrada, isto rapidamente produz 1000 vezes a mesma resposta, sem repetir cálculos.

Nestas situações também é possível fazer código "manhoso", ou seja, em vez de criar uma solução à maneira, usamos um programa mais ou menos lento para calcular todas as soluções, e depois fazemos outro programa com as soluções "inline". Qualquer coisa do género:

#include <stdio.h>

int solution[] = {
	1,
	1,
	2,
	3,
	5,
	8,
	...
	...
	165580141
};

int main()
{
	int n;

	for (;;) {
		scanf("%d", &n);

		if (n == 0)
			break;

		printf("%d\n", solution[n]);
	}
	return 0;
}

Isto pode não resultar se o programa demorar demasiado tempo a gerar as soluções, mas serve bem para safar se não conseguirmos descobrir um algoritmo melhor.

Caso não tenham reparado, a sequência gerada foi a sequência de Fibonacci e é um exemplo comum e simples de dynamic programming. A técnica de pré-calcular todas as respostas pode ser usada muitas vezes, por isso convém ler com atenção os enunciados dos problemas e ver quais são todos os inputs possíveis. Se forem só 40, como neste caso, podemos até criar as tabelas inline. Se o input for, por exemplo, um par de coordenadas (x,y) com x e y no intervalo [0, 100] (valores inteiros), então há 100x100 = 10000 inputs diferentes e também podemos armazenar todas as respostas. Outros problemas, como este, têm inputs que não se podem prever e já não podemos pré-calcular todas as respostas. Para praticar, passo a propôr um problema que se resolve com uma técnica semelhante.