Saltar al contenido principal

Solución de tiempo amortizado O(logn)O(\log^* n)

Cuando se realiza una operación Find(v)Find(v), se visitan todos los ancestros de vv hasta llegar a la raíz xx: v=v1v2vr2vr1vr=xv = v_1 \rightarrow v_2 \rightarrow \ldots \rightarrow v_{r-2} \rightarrow v_{r-1} \rightarrow v_r = x. Para agilizar las futuras operaciones de FindFind, no nos cuesta nada colgar a todos los nodos del camino, v1,,vr2v_1, \ldots, v_{r-2}, directamente de vr=xv_r = x (por ejemplo, puede hacerse a la vuelta de la recursión). Así, las futuras operaciones Find(vi)Find(v_i) tomarán tiempo O(1)O(1). Asimismo, se agilizarán los Find(u)Find(u) sobre otros descendientes uu de algún viv_i.

--> imagen

¿Qué impacto tiene esta mejora sobre los tiempos de FindFind? En el peor caso, ninguno, pues si bien una aplicación de FindFind mejora el tiempo de las siguientes operaciones FindFind, el primer FindFind puede costar O(logn)O(\log n) (por ejemplo, si hacemos n1n-1 UnionUnion y luego el primer FindFind). Necesitamos entonces realizar un análisis amortizado.

Considere una secuencia SS de operaciones UnionUnion y FindFind, y llamemos SS' a la secuencia SS sin las operaciones FindFind. Definiremos el rango de un nodo vv, r(v)r(v), como la altura del subárbol luego de realizar las operaciones de SS' (o bien, de aplicar SS pero sin la mejora que acabamos de describir para FindFind). Hablaremos del rango de los nodos mientras analizamos la secuencia verdadera SS, pero debe recordar que r(v)r(v) es fijo e independiente del punto de SS que estemos considerando.

Una propiedad importante es que, como vimos en la subsección anterior, un nodo de rango rr tiene al menos 2r2^r nodos en su subárbol (el que resulta de aplicar SS'). Como, en estos árboles, dos nodos uu y vv de rango rr no pueden descender uno del otro (pues entonces uno sería más alto que el otro), sus subárboles deben ser disjuntos. Por lo tanto, no puede haber más de n2r\frac{n}{2^r} nodos de rango rr.

Otra propiedad importante es que, si en algún momento de SS, uu desciende de vv, entonces r(u)<r(v)r(u) < r(v). Esto ocurre porque sólo la operación UnionUnion crea nuevas descendencias (al colgar xx de yy, todo descendiente de xx pasa a ser también descendiente de yy), mientras que sólo la operación FindFind destruye descendencias (al colgar todos los viv_i directamente de xx, los descendientes de viv_i dejan de ser descendientes de vi+1,,vr1v_{i+1},\ldots,v_{r-1}). Por lo tanto, en SS', donde se han eliminado los FindFind, uu también se hará descendiente de vv y se mantendrá así hasta el final. Como uu desciende de vv al final de SS', debe ser r(u)<r(v)r(u) < r(v).

Para nuestro análisis, definiremos la función F(i)F(i) como F(0)=1F(0)=1 y F(i)=2F(i1)F(i) = 2^{F(i-1)}. Esta función crece muy rápidamente:

i012345F(i)1241665536265536\begin{array}{|c|c|c|c|c|c|c|} \hline i & 0 & 1 & 2 & 3 & 4 & 5 \\ \hline F(i) & 1 & 2 & 4 & 16 & 65536 & 2^{65536} \\ \hline \end{array}

Llamaremos G(n)G(n) a la inversa de FF, G(n)=min{i, F(i)n}G(n) = \min \{ i,~F(i) \ge n \}. La función G(n)G(n) también se llama logn\log^* n, y es la cantidad de veces que debemos tomar logaritmo (base 2 en nuestro caso) a nn para que sea 1\le 1. En la práctica, vale G(n)5G(n) \le 5 para cualquier nn razonable:

n01234516176553665537265536G(n)012345\begin{array}{|c|c|c|c|c|c|c|} \hline n & 0--1 & 2 & 3--4 & 5--16 & 17--65536 & 65537--2^{65536} \\ \hline G(n) & 0 & 1 & 2 & 3 & 4 & 5 \\ \hline \end{array}

Dividiremos a los nn nodos en grupos: el nodo vv pertenecerá al grupo g(v)=G(r(v))g(v) = G(r(v)). Dicho de otro modo, si observamos el bosque que resulta de aplicar SS', los nodos de altura 0 y 1 (hojas y padres de sólo hojas) son del grupo g=0g=0, los nodos de altura 2 son del grupo g=1g=1, los de altura 3 y 4 son del grupo g=2g=2, los de altura 5 a 16 son del grupo g=3g=3, etc.

--> imagen

Con estas definiciones ya podemos presentar el análisis amortizado que haremos. Usaremos contabilidad de costos. La operación UnionUnion cuesta O(1)O(1), por lo que no necesitamos considerarla. Consideraremos que la operación Find(v)Find(v) cuesta 1 por cada nodo que atravesamos en el camino desde vv hasta la raíz xx. Este costo, para el análisis, lo repartiremos entre la operación Find misma y los nodos que atravesamos, de la siguiente forma:

  • Si, al momento de la operación, el nodo es la raíz xx de su árbol, o es hijo de la raíz xx, le cobramos a FindFind.
  • Si, al momento de la operación, el nodo tiene distinto grupo que su padre, le cobramos a FindFind.
  • De otro modo, le cobramos al nodo por el que pasamos.

--> imagen

Note que, cuando recorremos v=v1vr=xv = v_1 \rightarrow \ldots \rightarrow v_r = x, como cada viv_i desciende de vi+1v_{i+1}, vimos que debe valer r(vi)<r(vi+1)r(v_i) < r(v_{i+1}), y por lo tanto g(vi)g(vi+1)g(v_i) \le g(v_{i+1}). Eso significa que cada vez que el grupo de viv_i es distinto del de su padre vi+1v_{i+1}, el valor del grupo debe aumentar. Como el máximo rango posible es r=log2nr = \log_2 n, los grupos posibles van desde 0 hasta G(log2n)=G(n)1G(\log_2 n) = G(n)-1, y entonces en el camino de v1v_1 a vrv_r el valor del grupo puede aumentar sólo G(n)1G(n)-1 veces. Sumando que FindFind paga por la raíz x=vrx=v_r y su hijo vr1v_{r-1}, tenemos que en cada operación nuestra contabilidad de costos le cobra a FindFind a lo más 1+G(n)=O(logn)1+G(n) = O(\log^* n).

Debemos ver ahora cuánto les cobramos a los nodos. Note que, como hemos definido la contabilidad, un nodo que paga adquiere un nuevo padre gracias a la mejora que hace FindFind. Este nuevo padre es un ancestro del padre actual, por lo que su rango es estrictamente mayor. Por lo tanto, cada vez que un nodo paga, adquiere un padre de mayor rango. Una vez que adquiere un padre cuyo rango es de un grupo mayor al del nodo, el nodo no pagará nunca más, pues nunca volverá a tener un padre de su mismo grupo (sólo puede seguir adquiriendo padres de mayor y mayor rango).

¿Cuántas veces puede pagar un nodo hasta adquirir un padre de un grupo superior? Si está en el grupo gg, y su rango sube sólo de a 1 unidad por vez, puede pagar F(g)F(g1)F(g)-F(g-1) veces hasta que su padre pertenezca al grupo g+1g+1. Digamos para simplificar que los nodos de grupo gg pueden pagar F(g)F(g) unidades en total. ¿Cuántos nodos hay de grupo gg? Digamos que son N(g)N(g), con N(g)=r=F(g1)+1F(g)M(r)N(g) = \sum_{r=F(g-1)+1}^{F(g)} M(r), donde hay M(r)M(r) nodos de rango rr. Como vimos que M(r)n2rM(r) \le \frac{n}{2^r}, tenemos que

N(g)r=F(g1)+1F(g)n2r  =  n2F(g1)+1× ⁣ ⁣ ⁣r=0F(g)F(g1)112r  <  2n2F(g1)+1  =  n2F(g1)  =  nF(g)\begin{array}{c c c } N(g) & \le & \sum_{r=F(g-1)+1}^{F(g)} \frac{n}{2^r} \\ & ~~=~~ & \frac{n}{2^{F(g-1)+1}} \times\!\!\! \sum_{r=0}^{F(g)-F(g-1)-1} \frac{1}{2^r} \\ & ~~<~~ & \frac{2n}{2^{F(g-1)+1}} \\ & ~~=~~ & \frac{n}{2^{F(g-1)}} \\ & ~~=~~ & \frac{n}{F(g)} \end{array}

Es decir, tenemos N(g)nF(g)N(g) \le \frac{n}{F(g)} nodos del grupo gg, y cada uno de ellos paga a lo más F(g)F(g) a lo largo de su vida. En total, entre todos los nodos del grupo gg pagan a lo más N(g)F(g)nN(g) \cdot F(g) \le n. Como existen G(n)G(n) grupos distintos, entre todos los nodos pagan nG(n)n \cdot G(n). Por lo tanto, si se realizan Ω(n)\Omega(n) operaciones de FindFind, el costo amortizado de FindFind es O(logn)O(\log^* n), mientras que su costo de peor caso es O(logn)O(\log n). El costo de los UnionUnion es siempre O(1)O(1).