Capítulo 2
Exercício 9
(a) e (b):
Esse problema é muito legal! Cheguei em uma solução para a letra (b) que me satisfez, mas ainda não consegui provar que está correta. A solução da letra (a) foi bolada pela Cecília. Vou colocar o conteúdo de um chat com o César em que a gente ficou discutindo o problema, tem o meu raciocínio explicadinho:
me: deixa ver se consigo explicar aqui, talvez ajude ate eu entender melhor
me: a grande sacada do k=2 é usar a raiz quadrada. porque? digamos que voce divida o intervalo de N em pedaçinhos menores de tamanho raiz2(n). quantos pedaçinhos teriam? N/raiz2(N), o que é a mesma coisa de raiz2(n).
cesarpalomo: exato
me: logo, eu fico com um número de pedaços em funçao de raiz2(N). no pior caso, eu tenho que testar cada um desses pedaços e o jarro só quebra no último, quando eu tenho que percorrer cada um dos elementos do último pedaçinho. como eu tenho raiz2(N) elementos em cada pedaço, a complexidade do algoritmo fica O( raiz2(N) + raiz2(N) ), o primeiro se refere ao numero de pedacos, o segundo se refere a busca que tenho que fazer
cesarpalomo: exatamente
me: generalizando um pouco, tirando a raiz quadrada e colocando uma raiz qualquer, eu fico com N^(1 - 1/x) para o número de pedaços e N^(1/x) para o numero de passos em cada pedaço
me: fica assim: se eu dividir N em N^(1/x) pedaços, fico com N/N^(1/x) pedaços. fazendo as mágicas da matemática aqui, o total de pedaços fica N^(1-1/x).
me: entao, para o k=2, a melhor escolha é realmente x = 2, em que a complexidade do algoritmo fica O( N^(1 - 1/2) + N^(1/2) )
cesarpalomo: saquei
me: o lance é conseguir encontrar o x para k > 2
se o k = 3, por exemplo, podemos usar x = 1.5
dessa maneira, ficamos com N^(1-1/1.5) pedaços, o que é N^(1/3) pedaços. cada pedaço fica de tamanho N^(1/1.5), ou seja N^(2/3).
em relação ao k=2, o numero de pedacos diminuiu, mas o tamanho de cada pedaco aumentou.
o numero de pedacos era N^(1/2), agora é N^(1/3), diminuiu. o tamanho de cada pedaço era N^(1/2), agora é N^(2/3), aumentou.
cesarpalomo: ta
me: mas o lance é que, agora, como tenho 3 jarras, quando eu encontrar o "pedaço" no qual eu devo buscar, eu não preciso mais percorrer ele um a um.
eu posso pegar esse N^(2/3) e novamente subdividir, como se fosse um subproblema de tamanho N^(2/3) em que eu tenho 2 jarras.
a solução desse subproblema é dividir em N^(2/3 * 1/2) pedaços até encontrar o pedaço e depois fazer uma busca linear neste pedaço, que contem N^(2/3 * 1/2) elementos.
ou seja, a complexidade do subproblema é N^(2/3 * 1/2) = N^(1/3). então a complexidade do problema como um todo foi O( N^(1/3) + complexidade do subproblema ) = O (N^(1/3) + N^(1/3))
acho que ficou um pouco confuso. tá compreensível?
cesarpalomo: eh, preciso dar uma lida com calma pra entender
cesarpalomo: nao ficou confuso
saquei
bom, vc consegue provar por inducao que essa complexidade O( N^(1/x)) se mantém pra outros valores de x?
x na verdade parece que é igual a k, certo?
enfim, vou ver aqui com 4 como ficaria
me: é, acho que a complexidade é O(N^(1/k)) sim. mas nao provei nada ainda nao, so consegui me convencer mesmo.
me: toscamente, seria algo do tipo T(n,k) = O(n^(1/k)) + T( n^(1-1/k), k-1 )
T(n,k) é o custo de testar em uma escada de n degraus com k jarras
cesarpalomo: certo
A dificuldade que estou tendo agora é provar a corretude da complexidade do algoritmo. Mas estou bem convencido que a idéia geral está correta!
A idéia geral do algoritmo é, a cada passo, dividir o total N em "pedaços" de tamanho
e tentarmos resolver o subproblema de tamanho
com
jarras. Poderíamos escrever a recorrência desta maneira:

Onde
representa quanto de N ainda estamos tratando na K-ésima jarra. No começo, com as J jarras iniciais, teriamos a resposta com T(1,J). Teríamos então:

Vamos nos concentrar por um momento no expoente de
na segunda iteração do algoritmo:

Que é exatamente o mesmo expoente da primeira iteração! Com isso, podemos ver que o expoente do N não muda a cada iteração, permanecendo sempre constante e de valor
. Dessa maneira, podemos reescrever a recorrência como:

Essa maneira de escrever a recorrência simplifica bastante o trabalho de prova da questão. Vamos provar que essa recorrência é uma função
. Vamos supor que a recorrência pode ser escrita como
. O caso base funciona trivialmente, temos
. Supondo então que
, temos:

Logo, provamos que o algoritmo tem um tempo computacional limitado assintoticamente por
, que é o que queriamos provar: para um número de jarras maior ou igual a 2 o algoritmo é sub-linear em função de N e, quanto mais jarras, mais rápido ele fica em função de N.
Um detalhe muito interessante deste problema é que todo este trabalho poderia ter sido evitado se o enunciado dissesse que o número de jarras era uma função de N. Se fosse, poderiamos utilizar uma busca binária até a última jarra e então fazer uma varredura linear! Muito legal isso!





