Material de apoio das aulas de tutoria de Algoritmos e Estrutura de dados da Universidade Federal de Ouro Preto, Campus João Monlevade. O conteúdo abordado é sobre relações de recorrência em relação com a análise e complexidade de algoritmos.
3. Seja o problema em que temos um número
natural n >= 0 e queremos descobrir a soma
de todos os números de n até 0. Como
resolver este problema recursivamente?
4. Entender o problema matemático
Precisamos sempre encontrar o caso base e o passo
recursivo. Mas para isso, temos que entender o
problema matemático.
5. Entender o problema matemático
Vejamos possíveis iterações:
0 = 0
1 = 1 + 0 = 1
2 = 2 + 1 + 0 = 3
3 = 3 + 2 + 1 + 0 = 6
...
n = n + n-1 + n-2 + ... + 0
O caso base como podemos
perceber é zero, pois é sempre
onde o problema termina!
6. Entender o problema matemático
Vejamos possíveis iterações:
0 = 0
1 = 1 + 0 = 1
2 = 2 + 1 + 0 = 3
3 = 3 + 2 + 1 + 0 = 6
...
n = n + n-1 + n-2 + ... + 0
O que podemos perceber
analisando as iterações, é que
começamos a somar em n e
vamos diminuindo em 1 para
cada número natural anterior a
n até zero.
n-1
7. Entender o problema matemático
Se assumirmos que diminuímos o problema em um fator de 1, então
teríamos a seguinte situação:
0 = 0
1 = + 0 = 1
2 = + 1 + 0 = 3
3 = + 2 + 1 + 0 = 6
...
n = + n + n-1 + ... + 0
Ao diminuirmos, percebemos que
novamente, nessa iteração, teríamos após o
n, um fator n-1 e então podemos definir
nosso passo recursivo como:
n + soma(n-1)
Pois para resolver o problema, sempre
somamos n com o seu antecessor direto (n-
1).
n-1
8. Soma de n termos
Nossa função então ficaria da seguinte forma:
int soma(int n){
if(n == 0)
return 0;
return n + soma(n-1);
}
9. Mais um exemplo
Seja o problema de elevar qualquer número n á um expoente qualquer k.
Como resolver isso de maneira recursiva. Vamos ver algumas possíveis
interações deste problema matemático, com n = 2 e k >= 0:
20
= 1
21
= 2 ∗ 20
22
= 2 ∗ 2 ∗ 20
= 2 ∗ 21
23
= 2 ∗ 2 ∗ 2 ∗ 20
= 2 ∗ 22
24
= 2 ∗ 21
∗ 21
∗ 21
∗ 20
= 2 ∗ 23
Na multiplicação de mesma base podemos somar os
expoentes e manter a base (1+1+1+0 = 3)
K - 1 Percebemos que o
problema termina sempre
que k = 0. Portanto, desta
vez, a diminuição do nosso
problema, ocorrerá no
expoente e não no número
que estamos somando.
10. Um fato importante a se considerar
sobre recursão
- Nós podemos ter mais de um caso base!
- Sempre precisamos encontrar uma maneira de diminuir o problema. O
fator que vamos diminuir e qual é o nosso parâmetro de diminuição não
será o mesmo para todos os problemas. Podemos, inclusive, ter que
diminuir o problema utilizando mais que um parâmetro para isso.
- É importante notar que o fator de diminuição nem sempre será em 1,
pode por exemplo ser 2 ou até mesmo dividir o problema pela metade
a cada passo recursivo!
11. Mais um exemplo
Seja o problema de elevar qualquer número n á um expoente qualquer k.
Como resolver isso de maneira recursiva. Vamos ver algumas possíveis
interações deste problema matemático, com n = 2 e k >= 0:
20
= 1
21
= 2 ∗ 20
22
= 2 ∗ 2 ∗ 20
= 2 ∗ 21
23
= 2 ∗ 2 ∗ 2 ∗ 20
= 2 ∗ 22
24
= 2 ∗ 21
∗ 21
∗ 21
∗ 20
= 2 ∗ 23
Na multiplicação de mesma base podemos somar os
expoentes e manter a base (1+1+1+0 = 3)
K - 1 O caso base é fácil de
perceber, pois quando k =
0, o problema terminou
para todas as iterações.
Logo k = 0 é o nosso caso
base.
12. Mais um exemplo
Seja o problema de elevar qualquer número n á um expoente qualquer k.
Como resolver isso de maneira recursiva. Vamos ver algumas possíveis
interações deste problema matemático, com n = 2 e k >= 0:
20
= 1
21
= 2 ∗ 20
22
= 2 ∗ 2 ∗ 20
= 2 ∗ 21
23
= 2 ∗ 2 ∗ 2 ∗ 20
= 2 ∗ 22
24
= 2 ∗ 21
∗ 21
∗ 21
∗ 20
= 2 ∗ 23
Na multiplicação de mesma base podemos somar os
expoentes e manter a base (1+1+1+0 = 3)
K - 1 Para o passo recursivo,
percebemos que sempre
podemos multiplicar n por
uma potenciação de 𝑛 𝑘−1.
O k-1 é em relação ao
expoente inicial. Logo o
passo recursivo:
𝑛 * 𝑛 𝑘−1
13. Potenciação recursiva
Nossa função então ficaria da seguinte forma:
int pot(int n, int k){
if(k == 0)
return 1;
return n * pot(n,k-1);
}
14. Análise da recursividade
Qual é a complexidade do algoritmo recursivo que
calcula o fatorial de n? Como conseguimos calcular
esse valor?
15. Análise da recursividade
Analisar a complexidade de algoritmos recursivos
não é tão simples assim e muitas vezes temos que
utilizar recursos matemáticos mais aguçados para
conseguir chegar à um valor.
16. Análise da recursividade
Analisar a complexidade de algoritmos recursivos
não é tão simples assim e muitas vezes temos que
utilizar recursos matemáticos mais aguçados para
conseguir chegar à um valor. Isto envolve saber a
descrição matemática que nos dá o
comportamento do algoritmo, que será definida
como sua relação de recorrência! (recorrência, pois o algoritmo sempre
“recorre” a si mesmo várias vezes para resolver um problema qualquer. Recorrência é o mesmo que repetição!)
17. Relações de recorrência
Uma relação de recorrência é sempre formada por:
o caso base e por um elemento que se parece
muito com o passo recursivo, mas tem a ver com
número de operações completadas. Vamos tentar
entender um pouco deste elemento, utilizando o
fatorial recursivo de n.
18. Relações de recorrência
Seja uma relação de recorrência representada pelo
símbolo 𝑇, podemos definir então a função de 𝑇(n),
que será a função que descreverá nossa relação de
recorrência, de modo que:
𝑇 𝑛 =
𝐶 , 𝑞𝑢𝑎𝑛𝑑𝑜 𝑛 𝑎𝑡𝑖𝑛𝑔𝑒 𝑜 𝑐𝑎𝑠𝑜 𝑏𝑎𝑠𝑒
𝑇 𝑛 𝑝𝑜𝑟 𝑎𝑙𝑔𝑢𝑚 𝑓𝑎𝑡𝑜𝑟 𝑑𝑒 𝑘 + 𝑑 𝑑 ≥< 0
Onde o valor mínimo de n depende do nosso problema matemático!
19. Relações de recorrência
Vamos utilizar como exemplo o fatorial de n.
Sabemos que o caso base é quando n = 1 e neste
caso, o valor retornado é 1. Logo C = 1 quando n = 1,
então:
𝑇 𝑛 =
1 , 𝑠𝑒 𝑛 = 1
20. Relações de recorrência
Agora vamos analisar a função fatorial, para encontrar nossa
relação de recorrência:
int fatorial(int n){
if( n == 1 )
return 1;
return n * fatorial(n-1);
}
Para encontrar a recorrência do algoritmo temos
que nos atentar para o número de operações
que temos a cada passo. Olhando para o passo
recursivo, pode-se perceber que sempre vamos
ter 1 multiplicação mais uma chamada da função
fatorial com o nosso problema diminuído em 1.
Portanto nossa recorrência pode ser definida
por:
1 + 𝑇 𝑛 − 1
Onde T refere-se neste caso a função fatorial!
Uma multiplicação e uma
chamada de função com o
problema diminuído em 1!
21. Relações de recorrência
Logo, k = 1, o fator de diminuição é uma subtração
por k e d = 1, que é o valor de operações
completadas por cada chamada da função.
𝑇 𝑛 =
1 , 𝑠𝑒 𝑛 = 1
𝑇 𝑛 − 1 + 1 ,
22. Relações de recorrência
Para finalizar o nosso T(n), precisamos definir o valor
mínimo que n atinge em que a recorrência ainda
existe. Neste caso, é todo valor que n assume que é
maior que o caso base. Logo
𝑇 𝑛 =
1 , 𝑠𝑒 𝑛 = 1
𝑇 𝑛 − 1 + 1 , 𝑝𝑎𝑟𝑎 𝑛 > 1
Quando n atingir 1, não teremos mais recorrências!
23. Como resolver uma relação de recorrência?
Temos duas maneiras de resolver uma relação de
recorrência:
- Por expansão dos fatores
- Por indução matemática
24. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
1º) Copie a fórmula original
𝑇 𝑛 = 𝑇 𝑛 − 1 + 1
2º) Descubra por quanto que a recorrência é diminuída a cada passo (fator de
diminuição k)
T(n) está escrito em função de T(n-1) então a cada passo o parâmetro n é
subtraído de 1 (k = 1)
25. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
3º) Encontre o valor de T para os próximos valores da diminuição,
utilizando valor da equação original T(n) = T(n – 1) + 1
Como já sabemos que o fator de diminuição é subtraído de 1, então
devemos achar os valores de T(n-1), T(n-2), T(n-3) e assim por diante,
substituindo n pelos respectivos valores na equação inicial.
26. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
3º) Encontre o valor de T para os próximos valores da diminuição,
utilizando valor da equação original T(n) = T(n – 1) + 1
Se ainda está confuso o que devemos fazer, pense nesse problema
matemático, seja:
𝑓 𝑥 = 2𝑥
Se queremos saber o valor de f(2) e f(3) logo x = 2, x = 3, então fazemos:
𝑓 2 = 2 ∗ 2 = 4
𝑓 3 = 2 ∗ 3 = 6
27. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
3º) Encontre o valor de T para os próximos valores da diminuição, utilizando valor
da equação original T(n) = T(n – 1) + 1
Portanto se queremos saber os valores de T(n-1), T(n-2)... Então n = n-1, n = n-2,...
E fazemos como anteriormente substituindo os valores de n. Logo:
𝑇 (𝑛 − 1) = 𝑇 𝑛 − 1 − 1 + 1 = T n − 2 + 1 (3.1)
𝑇 (𝑛 − 2) = 𝑇 𝑛 − 2 − 1 + 1 = T n − 3 + 1(3.2)
𝑇 (𝑛 − 3) = 𝑇 𝑛 − 3 − 1 + 1 = T n − 4 + 1(3.3)
28. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
4º) Substitua os valores encontrados em 3, na equação original (chamamos isto de
expansão da recorrência) T(n) = T(n-1) + 1
Utilizando 3.1, onde encontramos o valor de T(n-1):
𝑇 𝑛 = 𝑇 𝑛 − 2 + 1 + 1 ⇒ 𝑇 𝑛 = 𝑇 𝑛 − 2 + 2 (4.1)
Utilizando 3.2, onde encontramos o valor de T(n-2), substituímos em 4.1
𝑇 𝑛 = 𝑇 𝑛 − 3 + 1 + 2 ⇒ 𝑇 𝑛 = 𝑇 𝑛 − 3 + 3 (4.2)
k
k
29. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
4º) Continuamos a substituir, até conseguirmos enxergar um padrão nas expansões:
Utilizando 3.2, onde encontramos o valor de T(n-3), substituímos em 4.2:
𝑇 𝑛 = 𝑇 𝑛 − 4 + 1 + 3 ⇒ 𝑇 𝑛 = 𝑇 𝑛 − 4 + 4 (4.3)
Podemos notar que o valor de k incrementa a cada expansão, que é o comportamento
experado, já que estamos sempre diminuindo a recorrência pelo fator de 1 (valor inicial
de k). A ideia é perceber o comportamento da recorrência ao se aumentar o valor de k
(diminuir a recorrência)!
k
30. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
4º) Analisando todas as iterações:
𝑇 𝑛 = 𝑇 𝑛 − 2 + 2 (4.1)
𝑇 𝑛 = 𝑇 𝑛 − 3 + 3 4.2
𝑇 𝑛 = 𝑇 𝑛 − 4 + 4 4.3
k
A cada expansão da recorrência, k assume um novo valor. Percebemos também que o valor constante que é
somado a recorrência e indica a quantidade de multiplicações que teremos que realizar para encontrar a
solução do problema (fatorial de n) é o mesmo valor de k em todas as iterações.
31. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
4º) Analisando todas as iterações:
𝑇 𝑛 = 𝑇 𝑛 − 2 + 2 (4.1)
𝑇 𝑛 = 𝑇 𝑛 − 3 + 3 4.2
𝑇 𝑛 = 𝑇 𝑛 − 4 + 4 4.3
k
Logo, depois de k expansões teremos:
𝑇 𝑛 = 𝑇 𝑛 − 𝑘 + 𝑘
32. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
5º) Encontrar quando a expansão para. Precisamos pensar no caso base, que é o
mesmo que pensar em quando T(n-k) nos retornará um valor. Olhando para o caso
base, sabemos que teremos um valor de retorno e não uma nova recorrência quando n
= 1. Para o nosso caso, n = (n-k) que é o valor de argumento da recorrência [T(n-k)]
𝑇 𝑛 = 𝑇 𝑛 − 𝑘 + 𝑘 (5.1)
Logo precisamos saber quando n-k = 1, pois então teremos T(1) e já sabemos que isso é
igual a 1.
𝑛 − 𝑘 = 1 ⇒ 𝑘 = 𝑛 − 1
33. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
5º) Substituindo o valor de k encontrado (n-1) em 5.1 temos
𝑇 𝑛 = 𝑇 𝑛 − (𝑛 − 1) + 𝑛 − 1
= 𝑇 1 + 𝑛 − 1
Como sabemos que T(1) = 1, então
𝑇 𝑛 = 1 + 𝑛 − 1
𝑇 𝑛 = 𝑛
34. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
6º) Definir o limite assintótico de T(n)
Como encontramos que T(n) = n, então neste caso sabemos que o algoritmo fatorial de
n recursivo é O(n), maior grau no polinômio encontrado.
Na prática, depois de várias décadas de estudo, sabe-se que o resultado da relação de
recorrência é suficiente para conhecer a complexidade do algoritmo, porém se
quisermos realmente provar (e às vezes isso será necessário!), precisamos utilizar
indução matemática, que é um método da matemática discreta que nos ensina como
provar problemas matemáticos, entre eles relações de recorrência.
35. Resolvendo recorrências por expansão de
fatores. Receita de bolo!
6º) Definir o limite assintótico de T(n)
Como encontramos que T(n) = n, então neste caso sabemos que o algoritmo fatorial de
n recursivo é O(n), maior grau no polinômio encontrado.
Na prática, depois de várias décadas de estudo, sabe-se que o resultado da relação de
recorrência é suficiente para conhecer a complexidade do algoritmo, porém se
quisermos realmente provar (e às vezes isso será necessário!), precisamos utilizar
indução matemática, que é um método da matemática discreta que nos ensina como
provar problemas matemáticos, entre eles relações de recorrência.
36. Indução matemática
Em uma linguagem bem simplificada. Você pode usar a indução
matemática para provar uma expressão matemática quando:
1 – Sabe que ela é verdadeira para algum valor.
2 – Quer provar que ela é válida então para esse valor em
relação a um fator de um (pode ser aumentar ou diminuir). Ou
seja, quer provar que o próximo valor e os próximos valores
depois deste também são verdadeiros.
38. Indução matemática
Exemplo. Seja um teorema que tenha como parâmetro um número
natural n. Provando que T é válido para todos os valores de n,
provamos que:
1 – T é válido para n = 1 (eu tenho que saber que isto é verdade!);
2 – Para todo n > 1, se T é válido para n – 1, então T é válido para n
(Se conseguirmos provar que n – 1 é válido (e isto implica em provar
que n – 2, n – 3 e assim por diante também é válido), então podemos
provar que n é verdadeiro. Esse é o passo indutivo.
39. O que é uma prova?
Em matemática, uma prova é uma argumentação
precisa que procura convencer o leitor de que uma
certa preposição, previamente enunciada está
correta. Num sentindo mais informa, uma prova é
um texto que ajuda o leitor a entender por que
uma dada afirmação é verdadeira!
40. Provando uma relação de recorrência por indução
matemática
Primeiro temos que provar que o passo base é verdadeiro. Para
nossa equação de recorrência do problema do fatorial:
𝑇 𝑛 =
1 , 𝑠𝑒 𝑛 = 1
𝑇 𝑛 − 1 + 1 , 𝑝𝑎𝑟𝑎 𝑛 > 1
Logo primeiro queremos provar que T(1) = 1. Sabemos que T(1)
é verdadeiro, pois quando n = 1, o algoritmo não realiza
nenhuma multiplicação. Logo, o nosso caso base é verdadeiro!
41. Provando uma relação de recorrência por indução
matemática
Agora supomos que T(k) = k (pois não sabemos quanto é
realmente este valor antes de calcular todas as multiplicações. A
variável n foi substituída por k, pois estamos generalizando para
quaisquer valores n > 1, pois quando n = 1 já provamos ser
verdadeiro). Para provar que T(k) = k é verdadeiro, precisamos
provar que T(k + 1) = k + 1 também é verdadeiro. Se
conseguirmos provar que T(k + 1) é verdadeiro, então T(k)
também será verdadeiro, pois derivamos T(k+1) de T(k) e assim
nosso passo indutivo seria verdadeiro e nossa relação de
recorrência também.
42. Provando uma relação de recorrência por indução
matemática
A ideia principal aqui é a seguinte. Vamos encontrar definir
nossa hipótese de indução. A hipótese de indução é sempre
nosso passo recursivo:
𝑇 𝑛 = 𝑇 𝑛 − 1 + 1
Vamos a partir da nossa hipótese de indução, determinar o valor
de T(k + 1).
43. Provando uma relação de recorrência por indução
matemática
𝑇 𝑘 + 1 = 𝑇 (𝑘 + 1) − 1 + 1
= 𝑇 𝑘 + 1
Sempre, que queremos provar T(k+1), precisamos tentar deixá-lo
em função de T(k), que é o valor que supomos no começo do
exercício que queríamos provar. Supomos que T(k) = k, então
vamos substituí-lo na expressão encontrada acima.
44. Provando uma relação de recorrência por indução
matemática
Como supomos que T(k) = k no começo do exercícios,
podemos então substituí-lo na expressão encontrada no slide
anterior:
𝑇 𝑘 + 1 = 𝑇 𝑘 + 1
T(k + 1) = 𝑘 + 1
Note que é exatamente o que queríamos provar, pois lembre-se
que no começo do exercício, supomos T(k) = k e queríamos
provar que T(k+1) = k +1.
45. Provando uma relação de recorrência por indução
matemática
Escrever uma prova de indução matemática sempre envolve:
- Provar o caso base
- Supor T(k) = k e tentar provar que T(k+1) = k+1
- Encontrar uma hipótese de indução, que é o nosso passo
recursivo
- Utilizar a hipótese de indução para encontrar T(k+1) em
função de T(k)
- Manipular T(k+1) para que ele seja k+1