Você recebe uma matriz de elementos
A tarefa é intercalar a matriz, usando um algoritmo no local para que a matriz resultante pareça
Se o requisito in-loco não existisse, poderíamos facilmente criar uma nova matriz e copiar elementos, fornecendo um algoritmo de tempo .
Com o requisito in-loco, um algoritmo de divisão e conquista aumenta o algoritmo para ser .
Então a questão é:
Existe um algoritmo de tempo , que também está em vigor?
(Nota: Você pode assumir o modelo uniforme de WORD RAM de custo, portanto, o local é convertido em restrição de espaço ).
algorithms
arrays
permutations
in-place
Aryabhata
fonte
fonte
Respostas:
Aqui está a resposta que elabora o algoritmo do artigo vinculado por Joe: http://arxiv.org/abs/0805.1598
Primeiro, vamos considerar um algoritmoΘ(nlogn) que usa dividir e conquistar.
1) Dividir e conquistar
Nos é dado
Agora, para usar dividir e conquistar, por algunsm = Θ ( n ) , tentamos obter a matriz
e recursar.
Observe que a porçãob1 1, b2, … Bm, umm + 1, ... umn é um deslocamento cíclico de
porm lugares.
Este é um clássico e pode ser feito no local por três reversões e emO(n) tempo.
Assim, a divisão e conquista fornece um algoritmoΘ(nlogn) , com uma recursão semelhante a T(n)=2T(n/2)+Θ(n) .
2) Ciclos de Permutação
Agora, outra abordagem para o problema é considerar a permutação como um conjunto de ciclos disjuntos.
A permutação é dada por (assumindo que começa em1 )
Se de alguma forma soubéssemos exatamente quais eram os ciclos, usando espaço extra constante, poderíamos realizar a permutação escolhendo um elemento , determinar para onde esse elemento vai (usando a fórmula acima), colocar o elemento no local de destino no espaço temporário, colocar o elemento para esse local de destino e continue ao longo do ciclo. Quando terminamos o ciclo, passamos para um elemento do próximo ciclo e seguimos esse ciclo e assim por diante.A A
Isso nos daria um algoritmo de tempo , mas assume que "de alguma forma sabíamos quais eram os ciclos exatos" e tentamos fazer essa contabilidade dentro da limitação de espaço é o que dificulta esse problema.O(n) O(1)
É aqui que o artigo usa a teoria dos números.
Pode-se mostrar que, no caso em que , os elementos nas posições , estão em ciclos diferentes e cada ciclo contém um elemento na posição .2n+1=3k 1 3,32,…,3k−1 3m,m≥0
Isso usa o fato de que é um gerador de .2 (Z/3k)∗
Assim, quando , a abordagem de seguir o ciclo nos fornece um algoritmo de tempo , pois para cada ciclo, sabemos exatamente por onde começar: potências de (incluindo ) (aquelas pode ser calculado no espaço ).2n+1=3k O(n) 3 1 O(1)
3) Algoritmo final
Agora, combinamos os dois acima: Dividir e Conquistar + Ciclos de Permutação.
Dividimos e conquistamos, mas escolhemos para que seja uma potência de e .m 2m+1 3 m=Θ(n)
Portanto, ao repetir as duas "metades", repetimos apenas uma e fazemos trabalho extra.Θ(n)
Isso nos dá a recorrência (por alguns ) e, portanto, fornece um tempo , algoritmo espacial!T(n)=T(cn)+Θ(n) 0<c<1 O(n) O(1)
fonte
Tenho certeza de que encontrei um algoritmo que não depende da teoria dos números ou da teoria dos ciclos. Observe que existem alguns detalhes a serem trabalhados (possivelmente amanhã), mas tenho certeza de que eles funcionarão. Eu aceno com as mãos como deveria estar dormindo, não porque estou tentando esconder problemas :)
Seja
A
a primeira matriz,B
a segunda,|A| = |B| = N
e assumaN=2^k
para algunsk
, por simplicidade. LetA[i..j]
Ser o subarray deA
com índicesi
atéj
, inclusive. Matrizes são baseadas em 0. VamosRightmostBitPos(i)
retornar a posição (com base em 0) do bit mais à direita que é '1'i
, contando a partir da direita. O algoritmo funciona da seguinte maneira.Vamos pegar uma matriz de 16 números e começar a intercalá-los usando swaps e ver o que acontece:
De particular interesse é a primeira parte da segunda matriz:
O padrão deve estar claro: adicionamos um número alternadamente ao final e substituímos o número mais baixo por um número alto. Observe que sempre adicionamos um número mais alto que o número mais alto que já temos. Se formos capazes de descobrir exatamente qual número é o mais baixo a qualquer momento, podemos fazer isso facilmente.
Agora, vamos a exemplos maiores para ver se conseguimos ver um padrão. Observe que não precisamos corrigir o tamanho da matriz para construir o exemplo acima. Em algum momento, obtemos essa configuração (a segunda linha subtrai 16 de cada número):
Agora, isso mostra claramente um padrão: "1 3 5 7 9 11 13 15" são todos separados por 2, "2 6 10 14" são separados por 4 e "4 12" por 8. Portanto, podemos conceber um algoritmo que nos diga qual será o próximo menor número: o mecanismo é exatamente como os números binários funcionam. Você tem um pouco para a última metade da matriz, um pouco para o segundo trimestre e assim por diante.
Se, portanto, tivermos espaço suficiente para armazenar esses bits (precisamos de bits, mas nosso modelo computacional permite isso - um ponteiro para a matriz também precisa de bits), podemos descobrir qual número trocar em tempo amortizado.log n O ( 1 )logn logn O(1)
Portanto, podemos obter a primeira metade da matriz em seu estado intercalado em tempo e swaps. No entanto, precisamos corrigir a segunda metade de nossa matriz, que parece toda bagunçada ("8 6 5 7 13 14 15 16").O ( n )O(n) O(n)
Agora, se podemos 'classificar' a primeira metade desta segunda parte, terminamos com "5 6 7 8 13 14 15 16" e intercalar recursivamente essa metade fará o truque: intercalamos a matriz em time ( chamadas recursivas, cada uma das quais reduz pela metade o tamanho da entrada). Observe que não precisamos de uma pilha, pois essas chamadas são recursivas finais; portanto, nosso uso de espaço permanece .O ( log n ) O ( 1 )O(n) O(logn) O(1)
Agora, a pergunta é: existe algum padrão na parte que precisamos classificar? Tentar 32 números nos dá "16 12 10 14 9 11 13 15" para corrigir. Observe que temos exatamente o mesmo padrão aqui! "9 11 13 15", "10 14" e "12" são agrupados da mesma maneira que vimos anteriormente.
Agora, o truque é intercalar recursivamente essas subpartes. Entrelaçamos "16" e "12" a "12 16". Entrelaçamos "12 16" e "10 14" para "10 12 14 16". Entrelaçamos "10 12 14 16" e "9 11 13 15" para "9 10 11 12 13 14 15 16". Isso classifica a primeira parte.
Assim como acima, o custo total desta operação é . Somando tudo isso, ainda conseguimos um tempo total de execução de .O ( n )O(n) O(n)
Um exemplo:
fonte
Aqui está um algoritmo de tempo linear não recursivo no local para intercalar duas metades de uma matriz sem armazenamento extra.
A ideia geral é simples: percorra a primeira metade da matriz da esquerda para a direita, trocando os valores corretos no lugar. À medida que você avança, os valores à esquerda a serem usados são trocados no espaço desocupado pelos valores certos. O único truque é descobrir como retirá-los novamente.
Começamos com uma matriz de tamanho N dividida em duas metades quase iguais.
[ left_items | right_items ]
À medida que processamos, torna-se
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]
O espaço de troca cresce com o seguinte padrão: A) aumenta o espaço removendo o item direito adjacente e trocando por um novo item da esquerda; B) troque o item mais antigo por um novo item da esquerda. Se os itens da esquerda estiverem numerados 1..N, esse padrão será semelhante a
A sequência do índice alterado é exatamente o OEIS A025480 , que pode ser calculado com um processo simples. Isso permite que a localização da troca seja encontrada, dado apenas o número de itens adicionados até o momento, que também é o índice do item atual sendo colocado.
Essa é toda a informação que precisamos para preencher a 1ª metade da sequência em tempo linear.
Quando chegarmos ao ponto médio, a matriz terá três partes:
[ placed_items | swapped_left_items | remaining_right_items]
Se pudermos decifrar os itens trocados, reduzimos o problema para metade do tamanho e podemos repetir.Para decifrar o espaço de troca, usamos a seguinte propriedade: Uma sequência criada
N
alternando as operações append e swap_oldest conteráN/2
itens pelos quais suas idades são indicadasA025480(N/2)..A025480(N-1)
. (Divisão inteira, valores menores são mais antigos).Por exemplo, se a metade esquerda originalmente mantivesse os valores 1..19, o espaço de troca conteria
[16, 12, 10, 14, 18, 11, 13, 15, 17, 19]
. A025480 (9..18) é[2, 5, 1, 6, 3, 7, 0, 8, 4, 9]
, que é exatamente a lista de índices dos itens do mais antigo ao mais recente.Assim, podemos decifrar nosso espaço de troca avançando por ele e trocando
S[i]
comS[ A(N/2 + i)]
. Este também é o tempo linear.A complicação restante é que, eventualmente, você alcançará uma posição em que o valor correto deve estar em um índice mais baixo, mas ele já foi trocado. É fácil encontrar o novo local: basta fazer o cálculo do índice novamente para descobrir para onde o item foi trocado. Pode ser necessário seguir a cadeia algumas etapas até encontrar um local não trocado.
Nesse ponto, mesclamos metade da matriz e mantivemos a ordem das partes não imersas na outra metade, com exatamente
N/2 + N/4
trocas. Podemos continuar pelo resto da matriz para um total deN + N/4 + N/8 + ....
swaps estritamente menores que3N/2
.Como calcular A025480:
Isso é definido no OEIS como
a(2n) = n, a(2n+1) = a(n).
uma formulação alternativaa(n) = isEven(n)? n/2 : a((n-1)/2)
. Isso leva a um algoritmo simples usando operações bit a bit:Esta é uma operação O (1) amortizada sobre todos os valores possíveis para N. (1/2 precisa de 1 turno, 1/4 precisa de 2, 1/8 precisa de 3, ...) . Existe um método ainda mais rápido que usa uma pequena tabela de pesquisa para encontrar a posição do bit zero menos significativo.
Dado isso, aqui está uma implementação em C:
Esse deve ser um algoritmo razoavelmente compatível com o cache, pois 2 dos 3 locais de dados são acessados sequencialmente e a quantidade de dados sendo processados está diminuindo estritamente. Esse método pode ser alterado de uma reprodução aleatória para uma reprodução aleatória, negando o
is_even
teste no início do loop.fonte