Comment produire efficacement toutes les séquences binaires avec un nombre égal de 0 et de 1?

10

Une séquence binaire de longueur n est juste une séquence ordonnée sorte que chaque soit ou . Afin de générer toutes ces séquences binaires, on peut utiliser la structure d'arbre binaire évidente de la manière suivante: la racine est "vide", mais chaque enfant gauche correspond à l'ajout de à la chaîne existante et chaque enfant droit à un . Maintenant, chaque séquence binaire est simplement un chemin de longueur commençant à la racine et se terminant à une feuille.x j 0 1 0 1 n + 1x1,,xnxj0101n+1

Voici ma question:

Pouvons-nous faire mieux si nous voulons seulement générer toutes les chaînes binaires de longueur qui ont précisément zéros et uns?n n2nnn

Par "pouvons-nous faire mieux", je veux dire que nous devrions avoir une complexité inférieure à l'algorithme stupide qui construit d'abord l'arbre entier ci-dessus et essaie ensuite de trouver ces chemins avec un nombre égal d'arêtes "gauche" et "droite".

Vidit Nanda
la source
Pouvez-vous trouver un moyen de générer efficacement toutes les séquences strictement croissantes de nombres compris entre 1 et ? 2 nn2n
Cornelius Brand
Je ne peux pas commenter la complexité, mais mon algorithme naïf générerait les marches le long des bords d'une grille carrée d'un coin à un diagonal, en utilisant une sorte de schéma de retour en arrière. Cela signifie que 01 et 10 se retrouvent dans la même position (contrairement à votre arbre) mais avec backtrack nous connaissons cette histoire.
Hendrik Jan
Sur une note peut-être différente, voici une implémentation Java d'un -iterator . (nk)
Pål GD

Réponses:

6

Evidemment, il y a chaînes binaires de longueur . Pour parcourir le binaire, un algorithme doit visiter chaque nœud une fois, c'est-à-dire qu'il doit faire étapes. 2 n 2 n i = 0 2 i = 2 2 n + 1 - 1 = O ( 4 n )4n2n

i=02n2i=22n+11=O(4n)

Prenons un algorithme récursif qui traverse l'arbre que vous avez décrit, mais compte le nombre de uns et de zéros sur son chemin, c'est-à-dire qu'il ne traversera que la bonne partie de l'arbre.
Mais combien de ces chaînes binaires avec 0 et 1 existe-t-il? Nous choisissons 1 pour nos chaînes de longueur et utilisons la formule de Stirling à l'étape 2: n n 2 n ( 2 nnnn2n

(2nn)=(2n)!(n!)2=4nπn(1+O(1/n))

EDIT
Grâce aux commentaires de Peter Shor, nous pouvons également analyser le nombre d'étapes nécessaires au deuxième algorithme, qui compte les 1 et les 0. Je cite son commentaire ci-dessous:

Nous voulons trouver toutes les séquences binaires avec exactement 0 et 1. Nous traversons l'arbre binaire où chaque nœud est une séquence d'au plus 0 et 1. Nous n'avons pas besoin de visiter un nœud avec plus de 0 ou plus de 1. combien de nœuds devons-nous visiter? Il y a chaînes avec 0 et 1. En sommant cela sur tout donne . Maintenant, nous devons visiter chacun de ces nœuds à un coût moyen constant par nœud. Nous pouvons le faire en visitant chaque enfant gauche en premier et chaque enfant droit en second.n 2 n n nnn2nnn iji,jnn i = 0n j = 0(i+ji)iji,jni=0nj=0n(i+ji)=(2n+2n+1)1

En utilisant à nouveau la formule de Stirling, nous obtenons comme durée d'exécution du nouvel algorithme.

(2n+2n+1)1=4n+11n+1(1+O(1/n))1=O(4nn)
tranisstor
la source
Il faut être un peu plus prudent. Vraisemblablement après avoir généré chaque chaîne, nous la traitons en temps . Ainsi, le traitement de toutes les chaînes équilibrées prend du temps . Si l'algorithme de génération "idiot" optimisé est bien , alors il n'y a rien à gagner en passant à un algorithme plus intelligent, à part les opportunités de bugs. Ω(n)O(4n)Ω(4nn)O(4n)
Yuval Filmus
@Yuval Filmus: Que voulez-vous dire exactement par "traitement de chaînes"? Si vous voulez dire le temps passé sur la sortie, qui est certainement , alors vous devez considérer ce facteur également dans le temps d'exécution de l'algorithme "idiot", qui est alors . O ( 4 n n )Θ(n)O(4nn)
tranisstor
2
Mon point était que si vous êtes préoccupé par la différence entre et , vous devez au moins indiquer les temps de fonctionnement corrects; n'est pas suffisant pour révéler une différence potentielle entre les deux algorithmes. De plus, vous devez être prudent en analysant votre nouvel algorithme proposé, pour voir que ces petits facteurs "négligeables" ne le rendent pas plus lent que l'algorithme trivial. 4n˜ O (4n)4n/nO~(4n)
Yuval Filmus
2
Comment construire uniquement la "bonne partie" de l'arbre sans avoir à inclure également les "mauvaises parties"? Vous devez inclure tous les nœuds de l'arbre qui n'ont pas plus de enfants gauches ou enfants droits sur le chemin de la racine à eux. Cela fonctionne, mais vous avez besoin d'un argument supplémentaire pour montrer que cela fonctionne. Plus précisément, vous devez utiliser la formule . n n i = 0n j = 0 ( i + jnni=0nj=0n(i+ji)=(2n+2n+1)1
Peter Shor
2
Nous voulons trouver toutes les séquences binaires avec exactement 0 et 1. Nous traversons l'arbre binaire où chaque nœud est une séquence d'au plus 0 et 1. Nous n'avons pas besoin de visiter un nœud avec plus de 0 ou plus de 1. combien de nœuds devons-nous visiter? Il y a chaînes avec 0 et 1. En sommant cela sur tout donne . Maintenant, nous devons visiter chacun de ces nœuds à un coût moyen constant par nœud. Nous pouvons le faire en visitant chaque enfant gauche en premier et chaque enfant droit en second.n 2 n n n ( i + jnn2nnn iji,jn n i = 0 n j = 0 ( i + j(i+ji)iji,jni=0nj=0n(i+ji)=(2n+2n+1)1
Peter Shor
2

Peut-être que je suis épaisse, mais la question d'origine demandait un moyen de générer toutes les séquences binaires "équilibrées" de longueur 2n qui était plus efficace que de parcourir un arbre de toutes les séquences binaires de longueur 2n et de ne produire que celles qui étaient équilibrées. Alors pourquoi utiliser un arbre?

Voici le pseudocode d'un algorithme récursif qui génère toutes ces séquences (le mot-clé "yield" envoie une séquence à la sortie):

function all-balanced(n) {
  all-specified( "", n, n );
};

function all-specified( currentString, zeroes, ones ) {

  if (zeroes == 0) {
    for i = 0 to ones {
      currentString += "1";
    };
    yield currentString;
    return;
  };

  if (ones == 0) {
    for i = 0 to zeroes {
      currentString += "0";
    };
    yield currentString;
    return;
  };

  all-specified( currentString+"0", zeroes-1, ones );
  all-specified( currentString+"1", zeroes, ones-1 );
  return;
};

Si je me méprends sur quelque chose, dites-le moi, mais il me semble qu'il s'agit de la réponse la plus efficace au problème réellement posé, qui n'a jamais précisé l'utilisation d'un arbre.

afeldspath
la source