Algorithme en place pour l'entrelacement d'un tableau

62

On vous donne un tableau de éléments2n

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

La tâche consiste à entrelacer le tableau, en utilisant un algorithme sur place tel que le tableau résultant ressemble à

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

Si l'exigence en place n'existait pas, nous pourrions facilement créer un nouveau tableau et copier des éléments en donnant un algorithme time.O(n)

Avec l'exigence en place, un algorithme de division et de conquête oblige l'algorithme à devenir .θ(nlogn)

La question est donc:

Existe-t-il un algorithme time, qui est également en place?O(n)

(Remarque: vous pouvez supposer que le modèle WORD RAM à coût uniforme est utilisé, donc in-situ se traduit par une restriction d'espace ).O(1)

Aryabhata
la source
1
Ceci est sur stackoverflow mais ils ne donnent pas une solution de qualité. La réponse la mieux notée est la suivante: "Ce problème n’est pas aussi trivial que les gens le prétendent . Les devoirs? LOL. Il existe une solution sur arXiv " Mais la solution arxiv nécessite une certaine théorie des nombres + des preuves de référence dans d’autres documents. Ce serait bien d'avoir une solution succincte ici.
Joe
1
Également sur cstheory: cstheory.stackexchange.com/questions/13943/…
Yuval Filmus
Un autre sujet sur Stack Overflow: stackoverflow.com/questions/15996288/…
Nayuki

Réponses:

43

Voici la réponse qui développe l'algorithme à partir du document lié par Joe: http://arxiv.org/abs/0805.1598

Considérons d’abord un algorithme qui utilise diviser pour régner.Θ(nlogn)

1) Diviser et conquérir

On nous donne

a1,a2,,b1,b2,bn

Maintenant, pour utiliser diviser pour régner, pour certains , nous essayons d'obtenir le tableaum=Θ(n)

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

et recurse.

Notez que la partie est un décalage cyclique de

b1,b2,bm,am+1,an

am+1,an,b1,bm

par lieux.m

Ceci est un classique et peut être fait sur place par trois retournements et en time.O(n)

Ainsi, la division et la conquête vous donne un algorithme , avec une récursion similaire à .Θ(nlogn)T(n)=2T(n/2)+Θ(n)

2) Cycles de permutation

Maintenant, une autre approche du problème consiste à considérer la permutation comme un ensemble de cycles disjoints.

La permutation est donnée par (en partant de )1

j2jmod2n+1

Si nous savions exactement quels étaient les cycles, en utilisant un espace supplémentaire constant, nous pourrions réaliser la permutation en choisissant un élément , déterminer où cet élément est placé (à l'aide de la formule ci-dessus), mettre l'élément dans l'emplacement cible dans un espace temporaire, l'élément dans cet emplacement cible et continue le long du cycle. Une fois que nous avons terminé avec un cycle, nous passons à un élément du cycle suivant et suivons ce cycle, etc.AA

Cela nous donnerait un algorithme temporel, mais cela suppose que nous "sachions en quelque sorte quels étaient les cycles exacts" et que nous essayions de faire cette comptabilité dans la limite d'espace est ce qui rend ce problème difficile.O(n)O(1)

C'est ici que le papier utilise la théorie des nombres.

On peut montrer que, dans le cas où , les éléments aux positions , sont dans des cycles différents et chaque cycle contient un élément à la position .2n+1=3k13,32,,3k13m,m0

Ceci utilise le fait que est un générateur de .2(Z/3k)

Ainsi, lorsque , l'approche par cycle nous donne un algorithme temporel , car pour chaque cycle, nous savons exactement par où commencer: puissances de (y compris ) peut être calculé dans l' espace ).2n+1=3kO(n)31O(1)

3) algorithme final

Maintenant, nous combinons les deux précédents: Diviser et conquérir + Cycles de permutation.

Nous faisons une division et conquérons, mais choisissons pour que soit une puissance de et .m2m+13m=Θ(n)

Donc, au lieu de revenir sur les deux "moitiés", nous ne récrivons qu'une seule et faisons un travail supplémentaire.Θ(n)

Ceci nous donne la récurrence (pour quelque ) et nous donne donc un temps , algorithme spatial!T(n)=T(cn)+Θ(n)0<c<1O(n)O(1)

Aryabhata
la source
4
C'est beau.
Raphaël
1
Très agréable. En passant par des exemples de permutation, je comprends maintenant la plupart des choses. Deux questions: 1. Comment trouvez-vous réellement la valeur m? Le papier prétend qu'il faut O (log n), pourquoi? 2. Est-il possible d’entrelacer un tableau en utilisant une approche similaire?
num3ric
2
@ num3ric: 1) Vous trouvez la plus grande puissance de qui est . Donc ce sera . 2) Oui, c'est possible, je pense avoir ajouté une réponse quelque part sur stackoverflow. Les responsables de cycle dans ce cas, je crois, sont sortis pour (pour = puissance de ). 3<nO(logn)2a3b2m+13
Aryabhata
@Aryabhata, pourquoi ne récidivons-nous que sur un "demi", au lieu de deux "moitiés"?
sinoTrinity
1
@Aryabhata Cet algorithme peut-il être étendu pour entrelacer plus de deux tableaux? Par exemple, tournez en ou quelque chose de similaire. a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
Doub
18

Je suis presque sûr d'avoir trouvé un algorithme qui ne repose pas sur la théorie des nombres ou la théorie des cycles. Notez qu'il y a quelques détails à régler (peut-être demain), mais je suis assez confiant. Je fais la main comme je suis supposé dormir, pas parce que j'essaie de cacher des problèmes :)

Laissez - Aêtre le premier tableau, Ble second, |A| = |B| = Net d' assumer N=2^kpour certains k, pour simplifier. Soit A[i..j]le sous-tableau de Aavec indices ijusqu'à j, inclus. Les tableaux sont basés sur 0. Retournons RightmostBitPos(i)la position (basée sur 0) du bit le plus à droite qui est '1' i, en comptant à partir de la droite. L'algorithme fonctionne comme suit.

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

Prenons un tableau de 16 nombres, commençons simplement à les entrelacer à l'aide de swaps et voyons ce qui se passe:

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

La première partie du second tableau est particulièrement intéressante:

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

Le modèle doit être clair: nous ajoutons un nombre à la fin et remplaçons le nombre le plus bas par un nombre élevé. Notez que nous ajoutons toujours un nombre supérieur au nombre le plus élevé que nous avons déjà. Si nous étions en mesure de déterminer exactement quel nombre est le plus bas à un moment donné, nous pouvons le faire facilement.

Maintenant, nous allons pour des exemples plus grands pour voir si nous pouvons voir un motif. Notez qu'il n'est pas nécessaire de fixer la taille du tableau pour construire l'exemple ci-dessus. À un moment donné, nous obtenons cette configuration (la deuxième ligne soustrait 16 de chaque nombre):

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

Maintenant, cela montre clairement un motif: "1 3 5 7 9 11 13 15" sont tous 2 séparés, "2 6 10 14" sont tous 4 séparés et "4 12" sont 8 séparés. Nous pouvons donc concevoir un algorithme qui nous dit quel sera le prochain nombre le plus petit: le mécanisme est à peu près exactement le fonctionnement des nombres binaires. Vous avez un peu pour la dernière moitié du tableau, un peu pour le deuxième trimestre et ainsi de suite.

Si nous disposons donc de suffisamment d’espace pour stocker ces bits (nous avons besoin de bits, mais notre modèle de calcul le permet - un pointeur dans le tableau a également besoin de bits), nous pouvons déterminer le nombre à échanger dans temps amorti.lognlognO(1)

On peut donc obtenir la première moitié du tableau dans son état entrelacée dans temps et swaps. Cependant, nous devons réparer la seconde moitié de notre tableau, qui semble tout foiré ("8 6 5 7 13 14 15 16").O(n)O(n)

Maintenant, si nous pouvons "trier" la première moitié de cette seconde partie, nous aboutissons à "5 6 7 8 13 14 15 16", et entrelacer récursivement cette moitié fera l'affaire: nous entrelacez le tableau en time ( appels récursifs dont chacun divise par deux la taille de l’entrée). Notez que nous n'avons pas besoin d'une pile car ces appels sont récursifs, notre utilisation de l'espace reste donc .O(n)O(logn)O(1)

Maintenant, la question est la suivante: existe-t-il un motif dans la partie à trier? Essayer 32 numéros nous donne "16 12 10 14 9 11 13 15" à corriger. Notez que nous avons exactement le même modèle ici! "9 11 13 15", "10 14" et "12" sont regroupés de la même manière que nous avons vue précédemment.

Maintenant, l'astuce consiste à entrelacer récursivement ces sous-parties. Nous entrelacez "16" et "12" en "12 16". Nous entrelacez "12 16" et "10 14" avec "10 12 14 16". Nous entrelacez "10 12 14 16" et "9 11 13 15" pour "9 10 11 12 13 14 15 16". Cela trie la première partie.

Comme ci-dessus, le coût total de cette opération est . En additionnant tout cela, nous parvenons tout de même à obtenir un temps d'exécution total de .O(n)O(n)

Un exemple:

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
Alex ten Brink
la source
Intéressant. Seriez-vous prêt à essayer de rédiger une preuve formelle? Je sais qu'il existe un autre algorithme (mentionné dans le document que Joe a trouvé) qui traite les bits. Peut-être l'avez-vous redécouvert!
Aryabhata
1

Voici un algorithme linéaire non temporel in-récursif pour entrelacer deux moitiés d'un tableau sans stockage supplémentaire.

L'idée générale est simple: parcourez la première moitié du tableau de gauche à droite en échangeant les valeurs correctes. Au fur et à mesure que vous avancez, les valeurs restantes non encore utilisées sont échangées dans l'espace laissé vacant par les valeurs correctes. La seule astuce consiste à déterminer comment les retirer à nouveau.

Nous commençons avec un tableau de taille N divisé en 2 moitiés presque égales.
[ left_items | right_items ]
En le traitant, il devient
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]

L'espace d'échange croît avec le motif suivant: A) agrandit cet espace en supprimant l'élément de droite adjacent et en échangeant un nouvel élément à gauche; B) permutez l’objet le plus ancien avec un nouvel élément à gauche. Si les éléments de gauche sont numérotés 1..N, ce modèle ressemble à

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 séquence d'indexation modifiée correspond exactement à OEIS A025480 , qui peut être calculée à l'aide d'un processus simple. Cela permet de trouver l'emplacement de permutation compte tenu du nombre d'éléments ajoutés jusqu'à présent, qui correspond également à l'index de l'élément en cours de placement.

C'est toute l'information dont nous avons besoin pour renseigner la première moitié de la séquence en temps linéaire.

Lorsque nous arriverons au point médian, le tableau comportera trois parties: [ placed_items | swapped_left_items | remaining_right_items] si nous pouvons déchiffrer les éléments échangés, nous avons réduit le problème à la moitié de la taille et nous pouvons répéter.

Pour déchiffrer l’espace de permutation, nous utilisons la propriété suivante: Une séquence construite en Nalternant les opérations append et swap_oldest contiendra des N/2éléments pour lesquels leur âge est donné A025480(N/2)..A025480(N-1). (Division entière, les plus petites valeurs sont plus anciennes).

Par exemple, si la moitié gauche contenait à l’origine les valeurs 1..19, l’espace de permutation contiendrait [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]. A025480 (9..18) est [2, 5, 1, 6, 3, 7, 0, 8, 4, 9], qui est exactement la liste des index des éléments du plus ancien au plus récent.

Ainsi, nous pouvons déchiffrer notre espace de swap en le parcourant et en le permutant S[i]avec S[ A(N/2 + i)]. C'est aussi le temps linéaire.

La complication restante est que vous finirez par atteindre une position où la valeur correcte devrait être à un indice inférieur, mais elle a déjà été remplacée. Il est facile de trouver le nouvel emplacement: il suffit de refaire le calcul de l'index pour savoir où l'élément a été échangé. Il peut être nécessaire de suivre la chaîne quelques étapes jusqu'à ce que vous trouviez un emplacement non échangé.

À ce stade, nous avons fusionné la moitié du tableau et conservé l'ordre des parties non fusionnées dans l'autre moitié, avec exactement des N/2 + N/4échanges. Nous pouvons continuer à travers le reste du tableau pour un total de N + N/4 + N/8 + ....swaps strictement inférieur à 3N/2.

Comment calculer A025480:
Ceci est défini dans OEIS comme étant a(2n) = n, a(2n+1) = a(n).une formulation alternative a(n) = isEven(n)? n/2 : a((n-1)/2). Cela conduit à un algorithme simple utilisant des opérations au niveau du bit:

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

Il s’agit d’une opération O (1) amortie sur toutes les valeurs possibles pour N. (1/2 besoin 1, équipe 1/4 besoin 2, 1/8 besoin 3, ...) . Il existe une méthode encore plus rapide qui utilise une petite table de correspondance pour trouver la position du bit zéro le moins significatif.

Dans ce cas, voici une implémentation en 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;
  }
}

Cela devrait être un algorithme assez convivial pour le cache, puisque 2 des 3 emplacements de données sont accédés de manière séquentielle et que la quantité de données en cours de traitement diminue strictement. Cette méthode peut être transformée en mélange aléatoire en annulant le is_eventest au début de la boucle.

AShelly
la source