Soit un entier, et que désigne l'ensemble de tous les entiers. Supposons l'intervalle des entiers .
Je recherche une structure de données pour représenter une carte . Je souhaite que la structure de données prenne en charge les opérations suivantes:
doit retourner .
doit mettre à jour pour que , c'est-à-dire mettre à jour vers une nouvelle carte tel que pour et pour .
devrait retourner l'intervalle le plus grand tel que et soit constant sur (c'est-à-dire, ).
devrait mettre à jour vers une nouvelle carte telle que pour et pour .f ′ f ′ ( i ) = f ( i ) + δ i ∈ [ a , b ] f ′ ( i ) = f ( i ) i ∉ [ a , b ]
Je veux que chacune de ces opérations soit efficace. Je compterais le temps ou comme efficace, mais le temps est trop lent. Ce n'est pas grave si les temps de fonctionnement sont amortis. Existe-t-il une structure de données qui rend simultanément toutes ces opérations efficaces?O ( lg n ) O ( n )
(J'ai remarqué un modèle similaire apparu dans plusieurs défis de programmation. C'est une généralisation qui suffirait pour tous ces problèmes de défi.)
add
serait cependant linéaire dans le nombre de sous-intervalles de ; avez-vous pensé à un arbre splay avec des nœuds unaires " " supplémentaires , compactés paresseusement? + δRéponses:
Je crois que le temps logarithmique pour toutes les requêtes est réalisable. L'idée principale est d'utiliser un arbre d'intervalle, où chaque nœud de l'arbre correspond à un intervalle d'indices. Je vais développer les idées clés en commençant par une version plus simple de la structure de données (qui peut prendre en charge get et set mais pas les autres opérations), puis ajouter des fonctionnalités pour prendre également en charge les autres fonctionnalités.
Un schéma simple (prend en charge get et set, mais pas add ni stab)
Disons qu'un intervalle est plat si la fonction est constante sur , c'est-à-dire si .f [ a , b ] f ( a ) = f ( a + 1 ) = ⋯ = f ( b )[a,b] f [a,b] f(a)=f(a+1)=⋯=f(b)
Notre structure de données simple sera un arbre d'intervalles. En d'autres termes, nous avons un arbre binaire, où chaque nœud correspond à un intervalle (d'indices). Nous allons stocker l'intervalle correspondant dans chaque nœud de l'arbre. Chaque feuille correspondra à un intervalle plat, et elles seront disposées de telle sorte que la lecture des feuilles de gauche à droite nous donne une séquence d'intervalles plats consécutifs qui sont disjoints et dont l'union est tout de . L'intervalle pour un nœud interne sera l'union des intervalles de ses deux enfants. De plus, dans chaque nœud feuille nous stockerons la valeur de la fonction sur l'intervallev [ 1 , n ] ℓ V ( ℓ ) f I ( ℓ ) f fI(v) v [1,n] ℓ V(ℓ) f I(ℓ) correspondant à ce nœud (notez que cet intervalle est plat, donc est constant sur l'intervalle, donc nous stockons juste une seule valeur de dans chaque nœud feuille).f f
De manière équivalente, vous pouvez imaginer que nous partitionnons en intervalles plats, puis la structure de données est un arbre de recherche binaire où les clés sont les extrémités gauche de ces intervalles. Les feuilles contiennent la valeur de à une certaine gamme d'indices où est constant.f f[1,n] f f
Utilisez des méthodes standard pour vous assurer que l'arbre binaire reste équilibré, c'est-à-dire que sa profondeur est (où compte le nombre actuel de feuilles dans l'arbre). Bien sûr, , donc la profondeur est toujours au plus . Cela vous sera utile ci-dessous.m m ≤ n O ( lg n )O(lgm) m m≤n O(lgn)
Nous pouvons maintenant prendre en charge les opérations get et set comme suit:
i O ( lg n ) O ( lg n )get(i) est simple: on parcourt l'arbre pour trouver la feuille dont l'intervalle contient . Il s'agit simplement de traverser un arbre de recherche binaire. Comme la profondeur est , le temps d'exécution est .i O(lgn) O(lgn)
Premièrement, nous trouvons l'intervalle de feuille contenant ; si , alors nous divisons cet intervalle feuille en deux intervalles et (transformant ainsi ce nœud feuille en nœud interne et introduisant deux enfants).a a 0 < a [ a 0 , a - 1 ] [ a , b 0 ][a0,b0] a a0<a [a0,a−1] [a,b0]
Ensuite, nous trouvons l'intervalle de feuille contenant ; si , nous divisons cet intervalle feuille en deux intervalles et (transformant ainsi ce nœud feuille en nœud interne et introduisant deux enfants).[a1,b1] b b<b1 [a1,b] [b+1,b1]
À ce stade, je prétends que l'intervalle peut être exprimé comme l'union disjointe des intervalles correspondant à un sous-ensemble de nœuds dans l'arbre. Supprimez donc tous les descendants de ces nœuds (en les transformant en feuilles) et définissez la valeur stockée dans ces nœuds sur .[a,b] O(lgn) O(lgn) y
Enfin, puisque nous avons modifié la forme de l'arbre, nous effectuerons toutes les rotations nécessaires pour rééquilibrer l'arbre (en utilisant n'importe quelle technique standard pour garder un arbre équilibré).
Étant donné que cette opération implique quelques opérations simples sur les nœuds (et que cet ensemble de nœuds peut être facilement trouvé en temps ), le temps total pour cette opération est .O(lgn) O(lgn) O(lgn)
Cela montre que nous pouvons prendre en charge les opérations get et set en temps par opération. En fait, le temps d'exécution peut être montré comme étant , où est le nombre d'opérations définies effectuées jusqu'à présent.O(lgn) O(lgmin(n,s)) s
Ajout de la prise en charge de l'ajout
Nous pouvons modifier la structure de données ci-dessus afin qu'elle puisse également prendre en charge l'opération d'ajout. En particulier, au lieu de stocker la valeur de la fonction dans les feuilles, elle sera représentée comme la somme des nombres stockés dans un ensemble de nœuds.
Plus précisément, la valeur de la fonction à l'entrée sera récupérable comme la somme des valeurs stockées dans les nœuds sur le chemin de la racine de l'arbre jusqu'à la feuille dont l'intervalle contient . Dans chaque nœud nous stockons une valeur ; si représentent les ancêtres d'une feuille (y compris la feuille elle-même), alors la valeur de la fonction à sera .f(i) i i v V(v) v0,v1,…,vk vk I(vk) V(v0)+⋯+V(vk)
Il est facile de prendre en charge les opérations get et set en utilisant une variante des techniques décrites ci-dessus. Fondamentalement, lorsque nous traversons l'arbre vers le bas, nous suivons la somme des valeurs en cours d'exécution, de sorte que pour chaque nœud que la traversée visite, nous connaîtrons la somme des valeurs des nœuds sur le chemin de la racine à . Une fois que nous aurons fait cela, de simples ajustements à l'implémentation de get et set décrits ci-dessus suffiront.x x
Et maintenant, nous pouvons prendre en charge efficacement. Tout d'abord, nous exprimons l'intervalle comme l'union des intervalles correspondant à un ensemble de nœuds dans l'arborescence (en divisant un nœud au point d'extrémité gauche et au point d'extrémité droit si nécessaire ), exactement comme dans les étapes 1 à 3 de l'opération définie. Maintenant, nous ajoutons simplement à la valeur stockée dans chacun de ces nœuds . (Nous ne supprimons pas leurs descendants.)[ a , b ] O ( lg n ) O ( lg n )add([a,b],δ) [a,b] O(lgn) O(lgn) δ O(lgn)
Cela fournit un moyen de prendre en charge get, set et add, en temps par opération. En fait, le temps d'exécution par opération est où compte le nombre d'opérations définies plus le nombre d'opérations d'ajout.O(lgn) O(lgmin(n,s)) s
Soutenir l'opération de stab
La requête lancinante est la plus difficile à prendre en charge. L'idée de base sera de modifier la structure de données ci-dessus pour conserver l'invariant supplémentaire suivant:
Ici, je dis qu'un intervalle est un intervalle plat maximal si (i) est plat et (ii) aucun intervalle contenant n'est plat (en d'autres termes, pour tout satisfaisant , soit ou n'est pas plat).[a,b] [a,b] [a,b] a′,b′ 1≤a′≤a≤b≤b′≤n [a′,b′]=[a,b] [a′,b′]
Cela rend l'opération de stabilisation facile à mettre en œuvre:
Cependant, nous devons maintenant modifier l'ensemble et ajouter des opérations pour maintenir l'invariant (*). Chaque fois que nous divisons une feuille en deux, nous pourrions violer l'invariant si une paire adjacente d'intervalles de feuille a la même valeur de la fonction . Heureusement, chaque opération set / add ajoute au plus 4 nouveaux intervalles de feuilles. De plus, pour chaque nouvel intervalle, il est facile de trouver l'intervalle foliaire immédiatement à gauche et à droite de celui-ci. Par conséquent, nous pouvons dire si l'invariant a été violé; si c'était le cas, alors nous fusionnons les intervalles adjacents où a la même valeur. Heureusement, la fusion de deux intervalles adjacents ne déclenche pas de changements en cascade (nous n'avons donc pas besoin de vérifier si la fusion peut avoir introduit des violations supplémentaires de l'invariant). En tout, cela implique d'examinerf 12 = O ( 1 ) O ( lg n )f f 12=O(1) paires d'intervalles et éventuellement les fusionner. Enfin, étant donné qu'une fusion modifie la forme de l'arbre, si cela viole les invariants d'équilibre, effectuez les rotations nécessaires pour maintenir l'arbre en équilibre (en suivant les techniques standard pour maintenir les arbres binaires en équilibre). Au total, cela ajoute au plus travail supplémentaire aux opérations set / add.O(lgn)
Ainsi, cette structure de données finale prend en charge les quatre opérations et le temps d'exécution de chaque opération est . Une estimation plus précise est temps par opération, où compte le nombre d'opérations définies et ajoutées.O ( lg min ( n , s ) ) sO(lgn) O(lgmin(n,s)) s
Pensées de séparation
Ouf, c'était un schéma assez complexe. J'espère que je n'ai commis aucune erreur. Veuillez vérifier attentivement mon travail avant de vous fier à cette solution.
la source