pi tp 3
1. Multiplicação (Método Russo / Multiplicação por Duplicação)
Background Teórico
O primeiro problema aborda a multiplicação sem usar o operador *. A matemática por trás destas funções baseia-se numa propriedade muito elegante da aritmética binária: podemos multiplicar dois números dividindo um por dois (divisão inteira) e multiplicando o outro por dois.
Seja a operação :
- Se n é par:
- Se n é ímpar:
float mult(int n, float f){
if (n==0) return 0;
if (n%2==0) return (mult(n/2, f + f));
else return (f + mult(n+2, f + f));
}float mult(int n, float r){
float r = 0;
while (n > 0){
if (n%2 ==1)
r += f;
n = n/2; f += f;
}
return r;
}A versão recursiva consome memória extra porque empilha chamadas na memória do computador (a Call Stack). A versão iterativa (com o while) utiliza apenas as variáveis locais, o que a torna mais segura e eficiente em termos de memória espacial (complexidade de espaço .
2. Potenciação (Exponenciação Rápida)
Background Teórico
A potenciação normal exige multiplicar por si mesmo vezes. No entanto, podemos usar uma propriedade matemática para acelerar isto drasticamente: Se quisermos calcular , não precisamos de fazer multiplicações. Podemos fazer . Reduzimos o problema a metade.
float pot(int n, float f){
float r = 1;
int i;
for (i=0; i<n; i ++)
r *=f;
return r;
}float pot(int n, float f){
float r = 1;
while (n>0){
if (n%2 == 1)
r *=f;
r = f * f ; //elevamos a base ao quadrado
n = n/2; // Reduzimos o expoente a metade
}
return r;
}A primeira versão tem uma complexidade de tempo . , o ciclo corre um milhão de vezes. A versão otimizada divide o por em cada passo. Para , o ciclo executa apenas cerca de 20 vezes. A eficiência temporal passa a ser algorítmica ou .
3. Máximo Divisor Comum (MDC)
Background Teórico
Aqui entramos no coração da teoria dos números. O segundo código implementa o famoso Algoritmo de Euclides, que se apoia diretamente no Algoritmo da Divisão. O teorema fundamental dita que, se dividirmos n por m obtendo um resto r, então: Isto permite substituir um número grande pelo resto da sua divisão sucessivamente, até atingir o zero.
O primeiro código (força bruta) procura o MDC testando todos os números menores, começando do menor entre n e m e descendo (c--).
int mdc(int n, int m){
int c;
if (n<m) c = n; // ou c = (n<m)? n:m
else c = m;
while (!(n%c == 0 && m%c == 0))
c--;
return c;
}int mdc(int n, int m){
while(n!=0 && m!=0){
if (n>m) n = n%m;
else m = m%n;
}
if (n==0) return m;
else return n;
}O primeiro código pode exigir milhares de subtrações e testes de divisibilidade. O Algoritmo de Euclides (o segundo código) usa a operação de módulo (%), que calcula o resto instantaneamente. Isto transforma um algoritmo lento e exaustivo numa operação elegante que resolve o problema numa fração mínima de segundos.
4. Sequência de Fibonacci
Background Teórico
A sequência de Fibonacci é uma relação de recorrência clássica onde cada termo é a soma dos dois anteriores: , com e .

int fib(int n){
if (n<2) return n;
else return (fib(n-1) + fib(n-2));
}long fib(int n){
long F[n+1]; int i;
f[0] = 0; F[1] = 1;
for (i = 2; i <= n; i ++)
F[i] = F[i-1] + F[i-2];
return F[n];
}int main(){
int x;
scanf("%d", &x);
printf("fib(%d)=%d", x, fib(n));
return 0;
}A recursividade ingénua (primeiro código) é catastrófica aqui. Para calcular fib(5), o programa calcula fib(3) várias vezes de forma redundante. O tempo de execução cresce exponencialmente . A segunda versão cria um vetor e resolve o problema de baixo para cima (bottom-up), consultando os resultados anteriores. Demora apenas passos (complexidade de tempo , transformando um problema que demoraria anos para um n grande num cálculo instantâneo.