Algoritmos de divisão e conquista - por que não dividir em mais de duas partes?

33

Nos algoritmos de divisão e conquista, como quicksort e mergesort, a entrada é geralmente (pelo menos nos textos introdutórios) dividida em dois , e os dois conjuntos de dados menores são tratados recursivamente. Faz sentido para mim que isso acelere a solução de um problema se as duas metades demorarem menos da metade do trabalho de lidar com todo o conjunto de dados. Mas por que não dividir o conjunto de dados em três partes? Quatro? n ?

Eu acho que o trabalho de dividir os dados em muitos subconjuntos faz com que não valha a pena, mas me falta a intuição de ver que alguém deve parar em dois subconjuntos.

Também vi muitas referências ao quicksort de três vias. Quando isso é mais rápido? O que é usado na prática?

beta
fonte
Tente criar um algoritmo semelhante ao quicksort que divida uma matriz em três partes.
gnasher729

Respostas:

49

Faz sentido para mim que isso acelere a solução de um problema se as duas metades demorarem menos da metade do trabalho de lidar com todo o conjunto de dados.

Essa não é a essência dos algoritmos de dividir e conquistar. Normalmente, o ponto é que os algoritmos não podem "lidar com todo o conjunto de dados". Em vez disso, é dividido em partes que são triviais para resolver (como classificar dois números), depois são resolvidas trivialmente e os resultados são recombinados de maneira a produzir uma solução para o conjunto de dados completo.

Mas por que não dividir o conjunto de dados em três partes? Quatro? n?

Principalmente porque dividi-lo em mais de duas partes e recombinar mais de dois resultados resulta em uma implementação mais complexa, mas não altera a característica fundamental (Big O) do algoritmo - a diferença é um fator constante e pode resultar em uma desaceleração se a divisão e recombinação de mais de 2 subconjuntos criar sobrecarga adicional.

Por exemplo, se você faz uma classificação de mesclagem de 3 vias, na fase de recombinação, agora você precisa encontrar o maior dos 3 elementos para cada elemento, o que requer 2 comparações em vez de 1, para que você faça o dobro da comparação geral . Em troca, você reduz a profundidade da recursão em um fator de ln (2) / ln (3) == 0,63, para que você tenha 37% menos swaps, mas 2 * 0,63 == 26% mais comparações (e acessos à memória). Se isso é bom ou ruim, depende do que é mais caro no seu hardware.

Também vi muitas referências ao quicksort de três vias. Quando isso é mais rápido?

Aparentemente, pode-se provar que uma variante de pivô duplo do quicksort exige o mesmo número de comparações, mas em média 20% menos swaps, por isso é um ganho líquido.

O que é usado na prática?

Atualmente, quase ninguém mais programa seus próprios algoritmos de classificação; eles usam um fornecido por uma biblioteca. Por exemplo, a API Java 7 realmente usa o quicksort de pivô duplo.

As pessoas que realmente programam seu próprio algoritmo de classificação por algum motivo tendem a se ater à simples variante bidirecional, porque menos potencial de erros supera o desempenho 20% melhor na maioria das vezes. Lembre-se: de longe, a melhoria de desempenho mais importante é quando o código passa de "não está funcionando" para "funcionando".

Michael Borgwardt
fonte
1
Nota pequena: o Java 7 usa a classificação rápida Dual-Pivot apenas ao classificar primitivas. Para classificar objetos, ele usa o Timsort.
Bakuriu 5/05
1
+1 em "Atualmente, quase ninguém mais programa seus próprios algoritmos de classificação" e (mais importante) "Lembre-se: de longe, a melhoria mais importante do desempenho é quando o código passa de" não está funcionando "para" funcionando ". No entanto, eu gostaria de saber se essa sobrecarga ainda é trivial se, por exemplo, dividirmos o conjunto de dados em muitas, muitas partes. Por acaso, assim que tem outras pessoas: bealto.com/gpu-sorting_intro.html stackoverflow.com/questions/1415679/... devgurus.amd.com/thread/157159
AndrewJacksonZA
Eu sou um pouco lento. Alguém poderia explicar por que são necessárias mais 2 * 0,69 comparações? Não sei de onde vieram os 0,69.
21416 jeebface
@jeebface oops, isso foi um erro de digitação (agora corrigido). É 0,63 (a redução na profundidade da recursão), então o resultado de 26% a mais também funciona.
Michael Borgwardt
30

Assintoticamente falando, isso não importa. Por exemplo, a pesquisa binária faz aproximadamente log 2  n comparações e a pesquisa ternária faz aproximadamente log 3  n comparações. Se você conhece seus logaritmos, sabe que log a  x = log b  x / log b  a, portanto, a pesquisa binária gera apenas 1 / log 3 2 ≈ 1,5 vezes mais comparações que a pesquisa ternária. Essa também é a razão pela qual ninguém nunca especifica a base do logaritmo em grande notação Oh: é sempre um fator constante longe do logaritmo em uma determinada base, independentemente da base real. Portanto, dividir o problema em mais subconjuntos não melhora a complexidade do tempo e praticamente não é suficiente para compensar a lógica mais complexa. De fato, essa complexidade pode afetar negativamente o desempenho prático, aumentando a pressão do cache ou tornando as micro-otimizações menos inviáveis.

Por outro lado, alguma estrutura de dados de árvore usa um alto fator de ramificação (muito maior que 3, geralmente 32 ou mais), embora geralmente por outros motivos. Melhora a utilização da hierarquia de memória: as estruturas de dados armazenadas na RAM fazem melhor uso do cache, as estruturas de dados armazenadas no disco requerem menos leituras HDD-> RAM.

beta
fonte
Sim, procure no octree uma aplicação específica de uma estrutura de árvore mais que binária.
Daaxix 3/08
@daaxix btree é provavelmente mais comum.
Jules
4

Existem algoritmos de busca / classificação que subdividem não por dois, mas por N.

Um exemplo simples é a pesquisa por codificação hash, que leva tempo O (1).

Se a função hash estiver preservando a ordem, ela poderá ser usada para criar um algoritmo de classificação O (N). (Você pode pensar em qualquer algoritmo de classificação apenas fazendo N pesquisas para onde um número deve ir no resultado.)

A questão fundamental é que, quando um programa examina alguns dados e entra em alguns estados seguintes, quantos estados seguintes existem e qual a probabilidade das mesmas?

Quando um computador faz uma comparação de dois números, digamos, e então salta ou não, se os dois caminhos são igualmente prováveis, o contador de programas "conhece" mais uma informação em cada caminho; portanto, em média, ele "aprendeu" um pouco. Se um problema exigir que M bits sejam aprendidos, o uso de decisões binárias não poderá obter a resposta em menos de M decisões. Assim, por exemplo, procurar um número em uma tabela classificada de tamanho 1024 não pode ser feito em menos de dez decisões binárias, nem que seja porque menos não teria resultados suficientes, mas certamente pode ser feito em mais do que isso.

Quando um computador pega um número e o transforma em um índice em uma matriz, ele "aprende" até registrar a base 2 do número de elementos na matriz e faz isso em tempo constante. Por exemplo, se houver uma tabela de salto de 1024 entradas, todas mais ou menos igualmente prováveis, então pular nessa tabela "aprende" 10 bits. Esse é o truque fundamental por trás da codificação de hash. Um exemplo de classificação disso é como você pode classificar um baralho de cartas. Tenha 52 caixas, uma para cada cartão. Jogue cada cartão em sua lixeira e pegue todos eles. Não é necessário subdividir.

Mike Dunlavey
fonte
1

Como se trata de uma divisão geral e conquista, não apenas de classificação, fico surpreso que ninguém tenha trazido o Teorema Mestre.

Em resumo, o tempo de execução dos algoritmos de dividir e conquistar é determinado por duas forças contrárias: o benefício que você obtém ao transformar problemas maiores em pequenos e o preço que você paga para resolver mais problemas. Dependendo dos detalhes do algoritmo, pode ou não ser pago para dividir um problema em mais de duas partes. Se você dividir no mesmo número de subproblemas em cada etapa e souber a complexidade do tempo de combinar os resultados em cada etapa, o Teorema Mestre dirá a complexidade do tempo do algoritmo geral.

O algoritmo de Karatsuba para multiplicação usa uma divisão de três vias e conquista para obter um tempo de execução de O (3 n ^ log_2 3) que supera O (n ^ 2) para o algoritmo de multiplicação comum (n é o número de dígitos no números).

Charles E. Grant
fonte
No teorema mestre, o número de subproblemas que você cria não é o único fator. No Karatsuba e em seu primo Strassen, a melhoria realmente vem da fusão inteligente de soluções de alguns dos subproblemas, para que você reduza o número de chamadas recursivas nos subproblemas. Em resumo, o baumento do teorema do mestre exige que o aaumento seja mais lento para que você tenha uma melhoria em outras divisões.
InformedA
-4

Devido à sua natureza binária, um computador é muito eficiente em dividir as coisas em 2 e não tanto em 3. Você obtém uma divisão em 3 dividindo em 2 primeiro e depois divide uma das partes novamente em 2. Portanto, se você precisar dividir por 2 para obter a sua 3 divisão, você também pode dividir em 2.

Pieter B
fonte