Exercícios do livro "Algorithm Design" (Kleinberg, Tardos)
Table of Contents

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 $N^{1/k}$ e tentarmos resolver o subproblema de tamanho $N^{(k-1)/k}$ com $k-1$ jarras. Poderíamos escrever a recorrência desta maneira:

(1)
\begin{align} T(x, n) = N^{\frac{x}{k}} + T(1 - \frac{x}{k}, k-1) \end{align}

Onde $x$ 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:

(2)
\begin{eqnarray} T(1,J) &=& N^{\frac{1}{J}} + T(1-\frac{1}{J}, J-1) \\ T(1 - \frac{1}{J}, J-1) &=& N^{ (1-\frac{1}{J}) / (J-1) } + T(1 - \frac{1-\frac{1}{J}}{J-1}, J-2) \\ \cdots \end{eqnarray}

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

(3)
\begin{eqnarray} (1 - \frac{1}{J}) / (J-1) \\ \frac{J-1}{J} / (J-1) \\ \frac{1}{J} \end{eqnarray}

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 $N^{\frac{1}{J}}$. Dessa maneira, podemos reescrever a recorrência como:

(4)
\begin{align} T(k) = N^{\frac{1}{J}} + T(k-1) \end{align}

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 $O(N^{\frac{1}{J}})$. Vamos supor que a recorrência pode ser escrita como $T(k) = k \cdot N^{\frac{1}{J}}$. O caso base funciona trivialmente, temos $T(2) = 2 \cdot N^{\frac{1}{2}}$. Supondo então que $T(k-1) = (k-1)*N^{\frac{1}{J}}$, temos:

(5)
\begin{eqnarray} T(k) &=& k \cdot N^{\frac{1}{J}} \\ T(k) &=& N^{\frac{1}{J}} + (k-1) \cdot N^{\frac{1}{J}} \\ T(k) &=& N^{\frac{1}{J}} + T(k-1) \end{eqnarray}

Logo, provamos que o algoritmo tem um tempo computacional limitado assintoticamente por $O(N^{\frac{1}{J}})$, 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!

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.