J'ai fait une lecture rapide du document que vous avez lié. Sur la base des idées données dans cet article, voici une structure de données simple qui obtient un temps lié à chaque opération.O(lognloglogn)
Vous avez mentionné dans votre question que vous pouvez utiliser des arbres équilibrés et augmentés pour accélérer cela. En particulier, si vous avez un arbre binaire et augmentez chaque nœud avec la parité de son sous-arbre gauche, vous pouvez alors faire des mises à jour et des recherches dans le temps chacune. C'est rapide, mais pas assez vite.O(logn)
Maintenant, considérez la généralisation suivante de votre idée. Supposons qu'au lieu d'utiliser un arbre binaire, nous utilisons un arbre à plusieurs voies avec un facteur de ramification . Nous augmentons chaque clé de chaque nœud avec la parité de tous les sous-arbres qui le précèdent (cela généralise l'idée de stocker la parité du sous-arbre gauche). Maintenant, réfléchissons à la façon dont nous effectuerions une recherche ou une mise à jour dans cette arborescence. Pour faire une recherche, nous utilisons une version légèrement modifiée de l'algorithme de recherche d'arbre binaire d'avant: marcher du haut de l'arbre vers le bas, à chaque étape accumuler la parité du sous-arbre uniquement à gauche de chaque nœud. La hauteur de l'arborescence dans ce cas sera et nous effectuons par nœud, donc le coût d'une recherche serakO(logkn)O(1)O(logkn).
Cependant, avec cette configuration, le coût d'une mise à jour augmente. En particulier, si nous modifions la parité d'un élément, nous devons remonter du bas de l'arborescence vers le haut, en changeant la parité stockée de chaque clé dans chaque nœud sur le chemin vers le haut. Il y a clés par nœud et nœuds sur le chemin vers le haut à partir des feuilles, donc le coût d'une telle opération sera , qui est trop lent. Si nous pouvions éliminer en quelque sorte ce supplémentaire terme, alors nous serions dans les affaires.kO(logkn)O(klogkn)=O(klogklogn)k
L'aperçu du document est le suivant. Si vous pensez à notre problème initial, nous avions un tableau de taille et voulions pouvoir calculer les parités de préfixe. Nous avons maintenant un arbre -ary où, à chaque nœud, nous devons être en mesure de résoudre le problème de parité de préfixe sur des tableaux de taille chacun, car chaque nœud met en cache des informations sur les couches en dessous. Dans la structure de données ci-dessus, nous avons résolu le problème de parité de préfixe à chaque nœud en stockant simplement un tableau des parités de préfixe, ce qui signifie que si nous devons effectuer une mise à jour, le coût est . Le point de vue du document est qu'en utilisant une structure de données plus intelligente à chaque nœud, vous pouvez effectuer ces mises à jour de manière beaucoup plus efficace.nkkO(k)
En particulier, le document présente les idées suivantes. Supposons que soit "petit", pour une définition de petit que nous choisirons plus tard. Si vous voulez résoudre le problème de parité des préfixes sur un tableau de taille , il n'y a que tableaux de bits possibles différents de longueur . De plus, il n'y a que requêtes de recherche possibles que vous pouvez effectuer sur un tableau de bits de taille . Par conséquent, le nombre de combinaisons possibles d'un tableau et d'une requête est . Si nous choisissonskk2kkkkk2kkpour être suffisamment petit, nous pouvons rendre cette quantité si petite qu'il devient possible de précalculer le résultat de chaque tableau possible et de chaque requête possible. Si nous le faisons, nous pouvons alors mettre à jour notre structure de données comme suit. Dans chaque nœud de l' arborescence à voies, au lieu que chaque clé stocke la parité de son sous-arbre gauche, nous stockons à la place un tableau de bits, un pour chaque clé du nœud. Lorsque nous voulons trouver la parité de tous les nœuds à gauche du ème enfant, nous faisons juste une recherche dans une table indexée par ces bits (traités comme un entier) et l'index . Pourvu que nous puissions calculer cette table assez rapidement, cela signifie que faire une requête de parité de préfixe prendra toujours du tempskkikiO(logkn), mais maintenant les mises à jour prennent également du temps car le coût d'une requête de parité de préfixe sur un nœud donné sera .O(logkn)O(1)
Les auteurs de l'article ont remarqué que si vous choisissez , alors le nombre de requêtes possibles qui peuvent être faites est . De plus, le coût de l'exécution de toute opération sur l'arborescence résultante sera . Le hic est que vous devez maintenant effectuer un pré-calcul au début de la configuration de la structure de données. Les auteurs donnent un moyen d'amortir ce coût en utilisant une structure de données différente pour les requêtes initiales jusqu'à ce que suffisamment de travail ait été effectué pour justifier l'exécution du travail nécessaire pour configurer la table, bien que vous puissiez dire que vous devez dépenserk=lgn2lgn22lgn2=lgn2n−−√=o(n)O(logkn)=O(lognloglgn2)=O(lognloglogn)o(n)O(n) temps de construire l'arbre en premier lieu et que cela n'affectera pas le temps d'exécution global.
Donc, en résumé, l'idée est la suivante:
- Au lieu d'utiliser un arbre binaire augmenté, utilisez un arbre -ary augmenté .k
- Notez qu'avec un petit , toutes les listes et requêtes possibles à bits peuvent être précalculées.kk
- Utilisez cette structure de données précalculée à chaque nœud de l'arborescence.
- Choisissez pour définir la hauteur de l'arborescence et, par conséquent, le coût par opération, .k=lgn2O(lognloglogn)
- Évitez le coût de précalcul initial en utilisant une structure de données de remplacement temporaire dans chaque nœud jusqu'à ce que le précalcul en vaille la peine.
Dans l'ensemble, c'est une structure de données intelligente. Merci d'avoir posé cette question et de la lier - j'ai beaucoup appris au cours du processus!
En complément, bon nombre des techniques utilisées dans cette structure de données sont des stratégies courantes pour accélérer des solutions apparemment optimales. L'idée de précalculer toutes les requêtes possibles sur des objets de petite taille est souvent appelée la méthode des quatre russes et peut être vue dans d'autres structures de données comme la structure de données Fischer-Heun pour les requêtes de plage minimale ou l'algorithme décrémentiel pour la connectivité des arbres. De même, la technique d'utilisation d'arbres multivoies équilibrés augmentés avec un facteur de ramification logarithmique apparaît dans d'autres contextes, comme la structure de données déterministe d'origine pour la connectivité graphique dynamique, où une telle approche est utilisée pour accélérer les requêtes de connectivité à partir de à .O(logn)O(lognloglogn)