Algoritmo in-loco para intercalar uma matriz


62

Você recebe uma matriz de 2n elementos

a1,a2,,an,b1,b2,bn

A tarefa é intercalar a matriz, usando um algoritmo no local para que a matriz resultante pareça

b1,a1,b2,a2,,bn,an

Se o requisito in-loco não existisse, poderíamos facilmente criar uma nova matriz e copiar elementos, fornecendo um algoritmo de tempo O(n) .

Com o requisito in-loco, um algoritmo de divisão e conquista aumenta o algoritmo para ser θ(nlogn) .

Então a questão é:

Existe um algoritmo de tempo O(n) , 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 O(1) ).


11
Isso ocorre no stackoverflow, mas eles não fornecem uma solução de qualidade. A resposta mais bem avaliada é: "Esse problema não é tão trivial quanto as pessoas imaginam . Lição de casa? LOL. Existe uma solução no arXiv " Mas a solução do arxiv requer alguma teoria dos números + provas de referência em outros trabalhos. Seria bom ter uma solução sucinta aqui.
31412 Joe


Outra discussão sobre o estouro de pilha: stackoverflow.com/questions/15996288/…
Nayuki

Respostas:


43

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

a1,a2,,b1,b2,bn

Agora, para usar dividir e conquistar, por alguns m=Θ(n) , tentamos obter a matriz

[a1,a2,,am,b1,b2,,bm],[am+1,,an,bm+1,bn]

e recursar.

Observe que a porção

b1,b2,bm,am+1,an
é um deslocamento cíclico de

am+1,an,b1,bm

por m lugares.

Este é um clássico e pode ser feito no local por três reversões e em O(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 em 1 )

j2jmod2n+1

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.AA

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=3k13,32,,3k13m,m0

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=3kO(n)31O(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 .m2m+13m=Θ(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<1O(n)O(1)


4
Isso é bonito.
Raphael

11
Muito agradável. Passando por exemplos de permutação, agora entendo a maior parte. Duas perguntas: 1. Como você encontra o valor m? O documento afirma que é preciso O (log n), por quê? 2. É possível intercalar DE uma matriz usando uma abordagem semelhante?
num3ric

2
@ num3ric: 1) Você encontra a maior potência de que é . Então será . 2) Sim, é possível, acredito ter adicionado uma resposta no stackoverflow em algum lugar. Os líderes do ciclo, nesse caso, creio que foram da para (para = potência de ). 3<nO(logn)2a3b2m+13
Aryabhata

@Aryabhata, por que recessamos apenas uma "metade", em vez de duas "metades"?
SinoTrinity

11
@Aryabhata Esse algoritmo pode ser expandido para intercalar mais de duas matrizes? Por exemplo, transformar em ou algo semelhante. a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
Dup

18

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 Aa primeira matriz, Ba segunda, |A| = |B| = Ne assuma N=2^kpara alguns k, por simplicidade. Let A[i..j]Ser o subarray de Acom índices iaté j, inclusive. Matrizes são baseadas em 0. Vamos RightmostBitPos(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.

GetIndex(i) {
    int rightPos = RightmostBitPos(i) + 1;
    return i >> rightPos;
}

Interleave(A, B, N) {
    if (n == 1) {
        swap(a[0], b[0]);
    }
    else {
        for (i = 0; i < N; i++)
            swap(A[i], B[GetIndex(i+1)]);

        for (i = 1; i <= N/2; i*=2)
            Interleave(B[0..i/2-1], B[i/2..i-1], i/2);

        Interleave(B[0..N/2], B[N/2+1..N], n/2);
    }
}

Vamos pegar uma matriz de 16 números e começar a intercalá-los usando swaps e ver o que acontece:

1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16

De particular interesse é a primeira parte da segunda matriz:

|
| 1
| 2
| 2 3
| 4 3
| 4 3 5
| 4 6 5
| 4 6 5 7
| 8 6 5 7

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):

16 24 20 28 18 22 26 30 17 19 21 23 25 27 29 31
0   8  4 12  2  6 10 14  1  3  5  7  9 11 13 15

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 )lognlognO(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:

Interleave the first half:
1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
Sort out the first part of the second array (recursion not explicit):
8 6 5 7 13 14 15 16
6 8 5 7 13 14 15 16
5 8 6 7 13 14 15 16
5 6 8 7 13 14 15 16
5 6 7 8 13 14 15 16
Interleave again:
5 6 7 8   | 13 14 15 16
13 6 7 8  | 5 14 15 16
13 5 7 8  | 6 14 15 16
13 5 14 8 | 6 7 15 16
13 5 14 6 | 8 7 15 16
Sort out the first part of the second array:
8 7 15 16
7 8 15 16
Interleave again:
7 8 | 15 16
15 8 | 7 16
15 7 | 8 16
Interleave again:
8 16
16 8
Merge all the above:
9 1 10 2 11 3 12 4 | 13 5 14 6 | 15 7 | 16 8

Interessante. Você estaria disposto a tentar escrever uma prova formal? Eu sei que existe outro algoritmo (referido no artigo que Joe encontrou) que lida com bits. Talvez você tenha redescoberto isso!
Aryabhata

1

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

step swapspace index changed
1    A: 1         0
2    B: 2         0
3    A: 2 3       1
4    B: 4 3       0     
5    A: 4 3 5     2
6    B: 4 6 5     1
7    A: 4 6 5 7   3
...

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 Nalternando as operações append e swap_oldest conterá N/2itens pelos quais suas idades são indicadas A025480(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]com S[ 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/4trocas. Podemos continuar pelo resto da matriz para um total de N + N/4 + N/8 + ....swaps estritamente menores que 3N/2.

Como calcular A025480:
Isso é definido no OEIS como a(2n) = n, a(2n+1) = a(n).uma formulação alternativa a(n) = isEven(n)? n/2 : a((n-1)/2). Isso leva a um algoritmo simples usando operações bit a bit:

index_t a025480(index_t n){
    while (n&1) n=n>>1;
    return n>>1;  
}

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:

static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }

index_t unshuffle_item(index_t j, index_t sz)
{
  index_t i = j;
  do {
    i = a025480(sz / 2 + i);
  }
  while (i < j);
  return i;
}

void interleave(value_t a[], index_t n_items)
{
  index_t i = 0;
  index_t midpt = larger_half(n_items);
  while (i < n_items - 1) {

    //for out-shuffle, the left item is at an even index
    if (is_even(i)) { i++; }
    index_t base = i;

    //emplace left half.
    for (; i < midpt; i++) {
      index_t j = a025480(i - base);
      SWAP(a + i, a + midpt + j);
    }

    //unscramble swapped items
    index_t swap_ct  = larger_half(i - base);
    for (index_t j = 0; j + 1 < swap_ct ; j++) {
      index_t k = unshuffle_item(j, i - base);
      if (j != k) {
        SWAP(a + midpt + j, a + midpt + k);
      }
    }
    midpt += swap_ct;
  }
}

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_eventeste no início do loop.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.