Divisão e Conquista Fernando Lobo Algoritmos e Estrutura de Dados II 1 / 27 Divisão e Conquista É uma técnica para resolver problemas (veremos outras técnicas mais adiante). Consiste em 3 passos: Dividir o problema em um ou mais subproblemas. Conquistar (resolver) cada subproblema recursivamente. Combinar os resultados dos subproblemas para a obter a solução para o problema original. 2 / 27
Divisão e Conquista (cont.) Tempo de execução é dado quase sempre por uma recorrência do tipo: T (n) = D(n) + at (n/b) + C(n). D(n) é o tempo gasto a dividir o problema em subproblemas. a é o número de subproblemas. n/b é o tamanho de cada subproblema. C(n) é o tempo gasto a combinar as soluções dos subproblemas. 3 / 27 Divisão e Conquista (cont.) Normalmente agregamos os tempos D(n) + C(n) correspondentes ao tempo gasto pelo algoritmo em trabalho não recursivo, chamando a esse tempo f (n). Obtemos assim a recorrência T (n) = at (n/b) + f (n), que é a recorrência do Teorema Mestre. 4 / 27
Exemplo 1: MergeSort T (n) = Θ(1) + 2T (n/2) + Θ(n) = 2T (n/2) + Θ(n) = Θ(n lg n) 5 / 27 Exemplo 2: QuickSort Dividir: escolher o elemento pivot e dividir o array em 2 subarrays de tal forma que os elementos de um subarray sejam menores que os elementos do outro subarray. Conquistar: ordenar cada um dos subarrays recursivamente. Combinar: não faz nada. 6 / 27
Exemplo 2: QuickSort (cont.) Ao invés de MergeSort, aqui o tempo de divisão é Θ(n) e o tempo de combinar é zero. No melhor caso, o QuickSort divide sempre o array ao meio, e o tempo de execução é dado por uma recorrência igual à do MergeSort: T (n) = 2T (n/2) + Θ(n) = Θ(n lg n). E no pior caso? O pior caso ocorre se o elemento pivot fosse sempre o menor (ou o maior) elemento. Nesse caso a recorrência seria dada por, T (n) = T (n 1) + T (1) + Θ(n) = T (n 1) + Θ(n) = Θ(n 2 ) 7 / 27 Exemplo 2: QuickSort (cont.) O pior caso é raro acontecer, especialmente se adoptarmos uma boa estratégia para escolher o pivot. Imaginemos que o QuickSort dividia sempre o array em 2 subarrays, um de tamanho n/10 e outro de tamanho 9n/10. O tempo de execução seria dado por T (n) = T (n/10) + T (9n/10) + Θ(n). Não se pode aplicar o Teorema Mestre neste caso. Teríamos de provar usando o método de expansão da árvore de recorrência. Fica como exercício para vocês fazerem. Vão ver que vai dar à mesma Θ(n lg n). 8 / 27
Exemplo 3: Busca Binária Dividir: verificar o elemento que está no meio de array. Conquistar: resolve recursivamente o mesmo problema para um dos subarrays (esq. ou dta.) Combinar: não faz nada. Recorrência é T (n) = T (n/2) + Θ(1). Utilizando o Teorema Mestre, a = 1, b = 2, f (n) = c (em que c é constante). n log b a = n log 2 1 = n 0 = 1 = Θ(f (n)). Estamos no caso 2 do Teorema Mestre. A complexidade é T (n) = Θ(lg n) 9 / 27 Exemplo 4: Potência de um número Problema: calcular a n, sendo n um número inteiro. Algoritmo naive: } a a a{{... a} = Θ(n). n vezes É possível fazer melhor assimptoticamente? 10 / 27
Exemplo 4: Potência de um número (cont.) Sim. Exemplo: 2 8 = 2 2 2 2 2 2 2 2 = 256. Requer 7 multiplicações com o algoritmos naive. Outro método:2 8 = (2 4 ) 2 = 16 2 = 256. Requer apenas 3 + 1 = 4 multiplicações. No caso geral, a n = { a n/2 a n/2, se n par a (n 1)/2 a (n 1)/2 a, se n ímpar Recorrência igual à da busca binária:. T (n) = T (n/2) + Θ(1) = Θ(lg n) 11 / 27 Exemplo 5: Multiplicação de inteiros Faz sentido para inteiros muito grandes. Solução óbvia: algoritmo da escola primária. Exemplo: 23 * 12 ------ 46 + 23 ------ 276 12 / 27
Exemplo 5: Multiplicação de inteiros (cont.) Funciona em qualquer base. Seja x = 1100 e y = 1101, ambos na base 2. xy =? 1100 * 1101 -------- 1100 0000 1100 + 1100 --------- 10011100 13 / 27 Exemplo 5: Multiplicação de inteiros (cont.) Vamos assumir que ambos os números têm o mesmo n o de dígitos. Seja esse número n (n = 4 no exemplo anterior). Tempo de execução do algoritmo em função de n: Θ(n 2 ). É possível fazer melhor? Sim, com divisão e conquista. 14 / 27
Exemplo 5: Divisão e Conquista (versão 1) Ideia base: Dividir x ao meio, ficando com x 1 dígitos na parte esquerda (os mais significativos) e x 0 dígitos na parte direita (os menos significativos). Fazer a mesma coisa para y. x = x 12 n/2 + x 0. Do mesmo modo, y = y 12 n/2 + y 0. xy = (x 12 n/2 + x 0)(y 12 n/2 + y 0) = x 1y 12 n + (x 1y 0 + x 0y 1)2 n/2 + x 0y 0 15 / 27 Exemplo 5: Divisão e Conquista (versão 1) As operações 2 n e 2 n/2 não requerem multiplicações. Implementam-se com shifts. Multiplicação de 2 números de n bits dá origem a 4 multiplicações de 2 números de n/2 bits + um n o constante de adições de números de n bits. Ou seja, T (n) = 4T (n/2) + Θ(n). Aplicando o Teorema Mestre: a = 4, b = 2. n log b a = n log 2 4 = n 2 Estamos no caso 1: T (n) = Θ(n 2 ) 16 / 27
Exemplo 5: Divisão e Conquista (versão 1) Conclusão: algoritmo de divisão e conquista não é melhor que o algoritmo da escola primária. É apenas um algoritmo complicado que tem a mesma complexidade! 17 / 27 Exemplo 5: Divisão e Conquista (versão 2) Será possível usar apenas 3 chamadas recursivas? Se fosse possível obteríamos: T (n) = 3T (n/2) + Θ(n) = Θ(n log 2 3 ) Θ(n 1.59 ) Vamos ver que tal é possível com um truque. 18 / 27
Exemplo 5: Divisão e Conquista (versão 2) Queremos obter: xy = x 1y 12 n + (x 1y 0 + x 0y 1)2 n/2 + x 0y 0 Truque: (x 1 + x 0)(y 1 + y 0) = x 1y 1 + x 1y 0 + x 0y 1 + x 0y 0 Esta multiplicação tem as 4 multiplicações que pretendemos. Se também calcularmos x 1y 1 e x 0y 0 recursivamente, poderemos calcular xy com apenas 3 chamadas recursivas: (x1 + x0)(y1 + y0) // mult1 x1y1 // mult2 x0y0 // mult3 xy = x 1y 12 n + (mult1 x 1y 1 x 0y 0)2 n/2 + x 0y 0 19 / 27 Exemplo 5: Pseudocódigo Recursive-Multiply(x, y) Dividir x ao meio e obter x 1 e x 0 Dividir y ao meio e obter y 1 e y 0 mult1 = Recursive-Multiply(x 1 + x 0, y 1 + y 0) mult2 = Recursive-Multiply(x 1, y 1) mult3 = Recursive-Multiply(x 0, y 0) return mult2 2 n + (mult1 mult2 mult3) 2 n/2 + mult3 20 / 27
Exemplo 6: Multiplicação de matrizes A 1,1 A 1,2 A 1,n B 1,1 B 1,2 B 1,n A 2,1 A 2,2 A 2,n...... B 2,1 B 2,2 B 2,n...... A n,1 A n,2 A n,n B n,1 B n,2 B n,n Input: A i,j, B i,j com i, j = 1, 2,..., n. Output: C i,j = A B = n k=1 A i,k B k,j C i,j é dado pelo produto da linha i pela coluna j. 21 / 27 Exemplo 6: Multiplicação de matrizes (cont.) Algoritmo standard: 3 ciclos a varrer de 1.. n. Matrix-Multiply(A, B, C) for i = 1 to n for j = 1 to n C[i, j] = 0 for k = 1 to n C[i, j] = C[i, j] + A[i, k] B[k, j] Complexidade: Θ(n 3 ). 22 / 27
Exemplo 6: Divisão e Conquista (versão 1) Ideia: dividir a matriz de n x n em 4 matrizes de n 2 x n 2. [ ] [ ] [ ] r s a b e f = t u c d g h }{{}}{{}}{{} C A B r = ae + bg s = af + bh t = ce + dg u = cf + dh Dá origem a 8 multiplicações de matrizes de n 2 x n 2 e a 4 somas de matrizes n 2 x n 2. 23 / 27 Exemplo 6: Divisão e Conquista (versão 1) Tempo de execução: T (n) = 8T (n/2) + Θ(n 2 ). Aplicando o Teorema Mestre: a = 8, b = 2. n log b a = n log 2 8 = n 3 Estamos no caso 1: T (n) = Θ(n 3 ) Conclusão: um algoritmo complicado que não é melhor que o algoritmo standard. 24 / 27
Exemplo 6: versão 2, algoritmo de Strassen Ideia: consegue reduzir de 8 para 7 no número de multiplicações! Dá origem à recorrência: T (n) = 7T (n/2) + Θ(n 2 ). Aplicando o Teorema Mestre: T (n) = Θ(n log 2 7 ) Θ(n 2.81 ) 25 / 27 Exemplo 6: algoritmo de Strassen P 1 = a (f h) P 2 = (a + b) h P 3 = (c + d) e P 4 = d (g e) P 5 = (a + d) (e + h) P 6 = (b d) (g + h) P 7 = (a c) (e + f ) 7 mults, 18 somas r = P 5 + P 4 P 2 + P 6 s = P 1 + P 2 t = P 3 + P 4 u = P 5 + P 1 P 3 P 7 Verifiquem que é verdade. Não me perguntem como é que o Sr. Strassen descobriu isto. Aparentemente ninguém sabe! 26 / 27
Verificação de r r = P 5 + P 4 P 2 + P 6 = (a + d) (e + h) + d (g e) (a + b) h + (b d) (g + h) = ae + ah + de + dh + dg de ah bh + bg + bh dg dh = ae + bg 27 / 27