Algoritmo sul posto per intercalare un array


62

Ti viene data una matrice di elementi2n

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

Il compito è di intercalare l'array, usando un algoritmo sul posto in modo che assomigli all'array risultante

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

Se il requisito sul posto non fosse presente, potremmo facilmente creare un nuovo array e copiare elementi fornendo un algoritmo di tempo .O(n)

Con il requisito sul posto, un algoritmo di divisione e conquista aumenta l'algoritmo in modo che sia .θ(nlogn)

Quindi la domanda è:

Esiste un algoritmo time, anch'esso sul posto?O(n)

(Nota: è possibile assumere il modello WORD RAM a costo uniforme, quindi sul posto si traduce in una limitazione di spazio ).O(1)


1
Questo è su StackOverflow ma non offrono una soluzione di qualità. La risposta più votata è: "Questo problema non è così banale come le persone si immaginano . Compiti a casa? LOL. C'è una soluzione su arXiv " Ma la soluzione arxiv richiede una teoria dei numeri + prove di riferimento in altri documenti. Sarebbe bello avere una soluzione concisa qui.
Joe


Un altro thread su Stack Overflow: stackoverflow.com/questions/15996288/…
Nayuki

Risposte:


43

Ecco la risposta che elabora l'algoritmo dall'articolo collegato da Joe: http://arxiv.org/abs/0805.1598

Per prima cosa consideriamo un algoritmo che usa divide e conquistare.Θ(nlogn)

1) Dividi e conquista

Ci viene dato

a1,a2,,b1,b2,bn

Ora per usare divide e conquistare, per alcuni , proviamo a ottenere l'arraym=Θ(n)

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

e ricorrere.

Si noti che la porzione è uno spostamento ciclico di

b1,b2,bm,am+1,an

am+1,an,b1,bm

di posti.m

Questo è un classico e può essere eseguito sul posto con tre inversioni e in tempo .O(n)

Quindi la divisione e la conquista ti danno un algoritmo , con una ricorsione simile a .Θ(nlogn)T(n)=2T(n/2)+Θ(n)

2) Cicli di permutazione

Ora, un altro approccio al problema è considerare la permutazione come un insieme di cicli disgiunti.

La permutazione è data da (assumendo a partire da )1

j2jmod2n+1

Se in qualche modo sapessimo esattamente quali fossero i cicli, usando uno spazio aggiuntivo costante, potremmo realizzare la permutazione selezionando un elemento , determinare dove va quell'elemento (usando la formula sopra), mettere l'elemento nella posizione target in uno spazio temporaneo, mettere l'elemento in quella posizione target e continuare lungo il ciclo. Una volta che abbiamo finito con un ciclo, ci spostiamo su un elemento del ciclo successivo e seguiamo quel ciclo e così via.AA

Questo ci darebbe un algoritmo di tempo , ma presuppone che "in qualche modo sapessimo quali fossero i cicli esatti" e provando a fare questa contabilità entro la limitazione di spazio di è ciò che rende difficile questo problema.O(n)O(1)

Qui è dove l'articolo usa la teoria dei numeri.

Si può dimostrare che, nel caso in cui , gli elementi nelle posizioni , sono in cicli diversi e ogni ciclo contiene un elemento nella posizione .2n+1=3k13,32,,3k13m,m0

Questo utilizza il fatto che è un generatore di .2(Z/3k)

Quindi quando , l'approccio del ciclo follow ci dà un algoritmo di tempo , come per ogni ciclo, sappiamo esattamente da dove cominciare: potenze di (incluso ) (quelle può essere calcolato nello spazio ).2n+1=3kO(n)31O(1)

3) Algoritmo finale

Ora uniamo i due precedenti: Divide and Conquer + Permutazione Cycles.

Facciamo una divisione e una conquista, ma selezioniamo modo che sia una potenza di e .m2m+13m=Θ(n)

Quindi, invece, ricorrendo a entrambe le "metà", facciamo ricorso a una sola e facciamo lavoro extra.Θ(n)

Questo ci dà la ricorrenza (per alcuni ) e quindi ci dà un tempo , algoritmo spaziale!T(n)=T(cn)+Θ(n)0<c<1O(n)O(1)


4
Quello è bello.
Raffaello

1
Molto bella. Passando attraverso esempi di permutazione, ora capisco la maggior parte di esso. Due domande: 1. Come si trova effettivamente il valore m? La carta afferma che ci vuole O (log n), perché? 2. È possibile interlacciare in DE un array usando un approccio simile?
num3ric,

2
@ num3ric: 1) Trovi la massima potenza di che è . Quindi sarà . 2). Sì, è possibile, credo di aver aggiunto una risposta su StackOverflow da qualche parte. Credo che i leader del ciclo in quel caso siano risultati per (per = potenza di ). 3<nO(logn)2a3b2m+13
Aryabhata,

@Aryabhata perché ricerchiamo solo una "metà", anziché due "metà"?
sinoTrinity

1
@Aryabhata Questo algoritmo può essere espanso per intercalare più di due array? Ad esempio, girare in o qualcosa di simile. a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
Doub

18

Sono abbastanza sicuro di aver trovato un algoritmo che non si basa sulla teoria dei numeri o sulla teoria dei cicli. Nota che ci sono alcuni dettagli da elaborare (possibilmente domani), ma sono abbastanza sicuro che funzioneranno. Mi muovo a mano mentre dovrei dormire, non perché sto cercando di nascondere i problemi :)

Sia Ail primo array, Bil secondo, |A| = |B| = Ne supponiamo N=2^kper alcuni k, per semplicità. Lascia che A[i..j]sia il sottoarray di Acon indici ifino a jinclusi. Le matrici sono basate su 0. Lasciate RightmostBitPos(i)restituire la posizione (basata su 0) del bit più a destra che è '1' di i, contando da destra. L'algoritmo funziona come segue.

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);
    }
}

Prendiamo un array di 16 numeri e iniziamo a interfogliarli usando gli swap e vediamo cosa succede:

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

Di particolare interesse è la prima parte del secondo array:

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

Lo schema dovrebbe essere chiaro: aggiungiamo alternativamente un numero alla fine e sostituiamo il numero più basso con un numero alto. Tieni presente che aggiungiamo sempre un numero superiore di uno al numero più alto che abbiamo già. Se in qualche modo fossimo in grado di capire esattamente quale numero è il più basso in un dato momento, possiamo farlo facilmente.

Ora, prendiamo esempi più grandi per vedere se possiamo vedere uno schema. Si noti che non è necessario correggere la dimensione dell'array per costruire l'esempio sopra. Ad un certo punto, otteniamo questa configurazione (la seconda riga sottrae 16 da ogni numero):

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

Ora questo mostra chiaramente uno schema: "1 3 5 7 9 11 13 15" sono tutti 2 separati, "2 6 10 14" sono tutti 4 separati e "4 12" sono 8 separati. Possiamo quindi escogitare un algoritmo che ci dice quale sarà il prossimo numero più piccolo: il meccanismo è praticamente esattamente come funzionano i numeri binari. Ne hai un po 'per l'ultima metà dell'array, un po' per il secondo trimestre e così via.

Se ci viene quindi concesso spazio sufficiente per memorizzare questi bit (abbiamo bisogno di bit, ma il nostro modello computazionale lo consente - un puntatore nell'array necessita anche di bit), possiamo capire quale numero scambiare in tempo ammortizzato.lognlognO(1)

Possiamo quindi portare la prima metà dell'array nel suo stato interlacciato in time e swap. Tuttavia, dobbiamo correggere la seconda metà del nostro array, che sembra tutto incasinato ("8 6 5 7 13 14 15 16").O(n)O(n)

Ora, se possiamo 'ordinare' la prima metà di questa seconda parte, finiamo con "5 6 7 8 13 14 15 16", e l'interleaving ricorsiva di questa metà farà il trucco: interleave l'array in time ( chiamate ricorsive ciascuna delle quali dimezza la dimensione dell'input). Nota che non abbiamo bisogno di uno stack in quanto queste chiamate sono ricorsive di coda, quindi il nostro utilizzo dello spazio rimane .O(n)O(logn)O(1)

Ora, la domanda è: c'è qualche modello nella parte che dobbiamo ordinare? Provare 32 numeri ci dà "16 12 10 14 9 11 13 15" per sistemare. Nota che qui abbiamo lo stesso identico schema! "9 11 13 15", "10 14" e "12" sono raggruppati nello stesso modo che abbiamo visto in precedenza.

Ora, il trucco è di intercalare ricorsivamente questi sottoparti. Interlacciamo "16" e "12" a "12 16". Interlacciamo "12 16" e "10 14" a "10 12 14 16". Interlacciamo "10 12 14 16" e "9 11 13 15" a "9 10 11 12 13 14 15 16". Questo ordina la prima parte.

Proprio come sopra, il costo totale di questa operazione è . Sommando tutto ciò, riusciamo comunque a ottenere un tempo di esecuzione totale di .O(n)O(n)

Un esempio:

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. Saresti disposto a provare a scrivere una prova formale? So che esiste un altro algoritmo (indicato nel documento che Joe ha trovato) che si occupa dei bit. Forse l'hai riscoperto!
Aryabhata,

1

Ecco un algoritmo di tempo lineare non ricorsivo sul posto per intercalare due metà di un array senza memoria aggiuntiva.

L'idea generale è semplice: attraversare la prima metà dell'array da sinistra a destra, scambiando i valori corretti in posizione. Man mano che avanzi, i valori di sinistra ancora da utilizzare vengono scambiati nello spazio lasciato libero dai valori di destra. L'unico trucco è capire come estrarli di nuovo.

Iniziamo con un array di dimensioni N diviso in 2 metà quasi uguali.
[ left_items | right_items ]
Mentre lo elaboriamo, diventa
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]

Lo spazio di scambio aumenta con il seguente modello: A) aumenta lo spazio rimuovendo l'elemento destro adiacente e scambiando un nuovo oggetto da sinistra; B) scambia l'oggetto più vecchio con un nuovo oggetto da sinistra. Se gli elementi a sinistra sono numerati 1..N, questo modello è simile

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

La sequenza di modifica dell'indice è esattamente OEIS A025480 , che può essere calcolata con un semplice processo. Ciò consente di trovare la posizione di scambio, dato solo il numero di elementi aggiunti finora, che è anche l'indice dell'articolo corrente da posizionare.

Queste sono tutte le informazioni di cui abbiamo bisogno per popolare la prima metà della sequenza in tempo lineare.

Quando arriviamo al punto medio, l'array avrà tre parti: [ placed_items | swapped_left_items | remaining_right_items] se riusciamo a riordinare gli elementi scambiati, abbiamo ridotto il problema a metà della dimensione e possiamo ripetere.

Per riordinare lo spazio di swap, utilizziamo la seguente proprietà: Una sequenza creata Nalternando le operazioni append e swap_oldest conterrà gli N/2elementi in base ai loro valori di età A025480(N/2)..A025480(N-1). (Divisione intera, i valori più piccoli sono più vecchi).

Ad esempio, se la metà sinistra originariamente contenesse i valori 1..19, lo spazio di swap conterrebbe [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]. A025480 (9..18) è [2, 5, 1, 6, 3, 7, 0, 8, 4, 9], che è esattamente l'elenco degli indici degli articoli dal più vecchio al più recente.

Quindi possiamo riordinare il nostro spazio di swap avanzando attraverso di esso e scambiando S[i]con S[ A(N/2 + i)]. Questo è anche tempo lineare.

La restante complicazione è che alla fine raggiungerai una posizione in cui il valore corretto dovrebbe essere in un indice inferiore, ma è già stato scambiato. È facile trovare la nuova posizione: esegui nuovamente il calcolo dell'indice per scoprire dove è stato scambiato l'articolo. Potrebbe essere necessario seguire la catena alcuni passaggi fino a trovare una posizione non scambiata.

A questo punto, abbiamo unito metà dell'array e mantenuto l'ordine delle parti non unite nell'altra metà, con N/2 + N/4scambi esattamente . Possiamo continuare attraverso il resto dell'array per un totale di N + N/4 + N/8 + ....swap che è strettamente inferiore a 3N/2.

Come calcolare A025480:
questo è definito in OEIS come a(2n) = n, a(2n+1) = a(n).una formulazione alternativa è a(n) = isEven(n)? n/2 : a((n-1)/2). Ciò porta a un semplice algoritmo che utilizza operazioni bit a bit:

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

Si tratta di un'operazione O (1) ammortizzata su tutti i valori possibili per N. (1/2 necessita di 1 turno, 1/4 necessita 2, 1/8 necessita 3, ...) . Esiste un metodo ancora più veloce che utilizza una piccola tabella di ricerca per trovare la posizione del bit zero meno significativo.

Detto questo, ecco un'implementazione in 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;
  }
}

Questo dovrebbe essere un algoritmo abbastanza compatibile con la cache, poiché si accede in sequenza a 2 delle 3 posizioni dei dati e la quantità di dati in elaborazione sta diminuendo rigorosamente. Questo metodo può essere trasformato da out-shuffle a in-shuffle annullando il is_eventest all'inizio del loop.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.