
Sa ne imaginam ca parcurgem arborele dat in preordine si numerotam muchiile
incepand cu 1, in ordinea parcurgerii. Ca sa nu ametim, am numerotat nodurile
cu numere si muchiile cu cifre. Pentru arborele din enunt, vom obtine
urmatoarea ordine:
5
/ | \
A/ E| F/
/ | \
2 3 0
/ \
C/ D/
/ \
1 4
|
B|
|
6
Observatia cheie este ca timpul unei muchii este mai mare decat timpii tuturor
muchiilor de sub ea si mai mic decat timpul muchiei de deasupra ei. Sa spunem
acum ca vrem sa aflam LCA(4,6). Ce avem de facut? Sa aflam calea (unica) dintre
4 si 6, sa gasim muchia cu timpul cel mai mare de pe aceasta cale, si sa
tiparim nodul superior al acestei muchii. In cazul nostru, vom gasi calea
6-1-3-4, cu muchiile etichetate cu B, C si D. Muchia cu timpul cel mai mare
este D, iar nodul ei superior este 3. Asadar LCA(4, 6)=3. Pentru a afla
LCA(4,5), gasim calea 4-3-5, cu muchiile D si E. Muchia de cost maxim este E,
iar nodul ei superior este 5, deci LCA(4,5)=5.
Va puteti convinge singuri ca algoritmul functioneaza pentru orice alte noduri.
O exceptie trebuie facuta cand se cauta LCA(X,X), pentru ca nu exista o cale
intre X si X. In acest caz raspunsul este evident X. Pentru a fi si mai
concisi, si pentru a elimina si acest caz particular, putem reformula metoda
astfel: cautam calea intre X si Y, iar LCA(X,Y) este cel mai inalt nod de pe
aceasta cale.
Cateva cuvinte despre detaliile practice, inainte de a vedea care este
"scursura" (the flaw) acestui algoritm. Nu trebuie in nici un caz sa construim
efectiv calea de la X la Y. Ne intereseaza numai sa gasim nodul cel mai inalt
de pe aceasta cale. Ne putem folosi de faptul ca muchiile sunt etichetate
crescator de la frunze spre radacina. Daca pomenim si cuvantul "interclasare",
cred ca totul se lamureste. Algoritmul (in termeni naivi) este:
0. Pune un deget pe X si unul pe Y
1. Cat timp cele doua degete nu indica acelasi nod
a) daca muchia de deasupra lui X are cost mai mic decat muchia de
deasupra lui Y, atunci misca degetul de pe X pe tatal lui X
b) altfel muta degetul de pe Y pe tatal lui Y.
2. LCA(X,Y) este nodul pe care imi tin cele doua degete.
De exemplu, pentru X=4 si Y=6,
0. Punem un deget pe 4 si unul pe 6
1. Comparam muchia (4,3) cu muchia (6,1) si mutam degetul de pe 6 pe 1
1'. Comparam muchia (4,3) cu muchia (1,3) si mutam degetul de pe 1 pe 3
1". Comparam muchia (4,3) cu muchia (3,5) si mutam degetul de pe 4 pe 3
2. Intrucat ambele degete il indica pe 3, LCA(4,6)=3.
Sa analizam complexitatea acestui algoritm. Preprocesarea (adica etichetarea
muchiilor) costa O(N), fiind vorba despre o simpla parcurgere in postordine.
Desigur, nu este suficient sa tinem minte numai tatal unui nod, ci este nevoie
sa ii construim si lista copiilor, dar si aceasta dureaza tot O(N).
Partea mai proasta este ca raspunsul la LCA(X,Y) se poate da in O(L), unde L
este lungimea caii dintre X si Y. Dar L poate fi O(N) pentru un arbore
degenerat (un lant). Deci M intrebari bine (sau rau) alese pot necesita un
timp O(M*N).
Ce ne facem? Scoatem din buzunar inca un cuvant cheie, "structura de multimi
disjuncte". Va trebui sa reorganizam arborele nostru in asa fel incat
inaltimea sa sa nu depaseasca log N nivele. In acest caz, calea cea mai lunga
nu va depasi 2*log N muchii, si vom putea raspunde la M intrebari in
O(M*log N). Desigur, reorganizarea arborelui va da peste cap relatiile de
"stramos" si "stramos comun", deci va trebui sa retinem si informatii despre
forma initiala. Sa privim parcurgerea in postordine ca pe un proces care
porneste cu N noduri separate si la fiecare pas adauga o muchie in arbore,
unind in acest fel arbori din ce in ce mai mari:
5
. . .
. . .
. . .
2 3 0
. . T = 0
. . Toate nodurile sunt deconectate
. . Avem arborii 0, 1, 2, 3, 4, 5, 6
1 4
.
.
.
6
5
/ . .
A/ . .
/ . .
2 3 0
. . T = 1
. . Nodurile 2 si 5 devin conectate
. . Avem arborii 0, 1, (2,5), 3, 4 si 6
1 4
.
.
.
6
5
/ . .
A/ . .
/ . .
2 3 0
. . T = 2
. . Nodurile 6 si 1 devin conectate
. . Avem arborii 0, (1,6), (2,5), 3, 4
1 4
|
B|
|
6
.... si asa mai departe ....
Pentru a afla LCA(X,Y) trebuie sa "pandim" momentul cand o muchie completeaza
o cale intre X si Y. De exemplu, calea intre 6 si 2 se va completa in momentul
adaugarii muchiei 3-5. Fie "e" aceasta muchie importanta. Atunci LCA(X,Y) este
nodul superior al lui "e". In exemplul nostru, LCA(6,2)=5.
Cei care cunosc notiunea de "structura de multimi disjuncte" pot sari
urmatoarele randuri, pana la "******".
Vom numi structura de multimi disjuncte "DS", de la denumirea in engleza
"Disjoint Sets". Ea furnizeaza doua operatii rapide asupra unor multimi de
obiecte, oricare doua multimi fiind disjuncte:
1) Reuniunea a doua multimi (UNION)
2) Aflarea multimii din care face parte un obiect (FIND)
Sa consideram chiar exemplul de mai sus, in care obiectele sunt nodurile
grafului. Adaugam pe rand muchiile grafului, in postordine, adaugarea unei
muchii insemnand reunirea obiectelor de la cele doua capete ale muchiei.
0 1 2 3 4 5 6 Toate obiectele sunt separate
0 1 3 4 5 6 UNION(2,5).
| Acum obiectele 2 si 5 sunt in aceeasi
2 multime.
0 1 3 4 5 UNION(1,6)
| |
6 2
0 1 4 5 UNION(1,3)
/ \ |
6 3 2
Hop! A aparut o diferenta fata de forma arborelui! Dar pe noi nu ne intereseaza
sa pastram forma arborelui, ci ne intereseaza numai sa indicam ca la momentul
T=3, nodurile 6, 1 si 3 fac parte din aceeasi componenta. Deci dorim numai
sa legam cumva un nod din multimea lui 1 cu un nod din multimea lui 3.
0 1 5 UNION(4,3)
/|\ | La momentul T=4 al postordinii,
6 3 4 2 obiectele 1, 3, 4 si 6 au fost unite.
0 5 UNION(3,5)
/ \
1 2
/|\
6 3 4
5 UNION(0,5)
/|\
1 2 0
/|\
6 3 4
Acum sa formalizam ideile, precum si implementarile pentru UNION si FIND,
si sa vedem care e castigul. Pentru fiecare multime alegem un EXPONENT al
ei, un element privilegiat, o "eticheta". Intrucat am reprezentat multimile
ca pe niste arbori, este natural ca exponentul sa fie radacina arborelui.
Operatia FIND(X) nu are decat sa intoarca valoarea radacinii arborelui din
care face parte X. Vom recunoaste ca doua elemente X si Y sunt in aceeasi
multime daca FIND(X)=FIND(Y).
Operatia UNION(X,Y) nu are voie sa traseze o muchie directa intre X si Y,
pentru ca aceasta ar da peste cap structura arborescenta. Dar, daca tot nu
tinem mortis sa unim obiectul X cu obiectul Y, ci multimea lui X cu multimea
lui Y, ce-ar fi sa unim radacina lui X cu radacina lui Y? De fapt, asta am
facut mereu in imaginile de mai sus. De exemplu UNION(3,5) a tras o muchie
intre 1 si 5, pentru ca 1 este radacina arborelui lui 3.
Cum implementam asa ceva? Simplu, cu un vector si doua proceduri. Vectorul
va retine legatura de tip "tata" a fiecarui nod. Am botezat acest vector
DSF de la "Disjoint Set Father". In imaginea finala a lui DS, DSF[3]=1,
DSF[1]=5 s.a.m.d. Initial F[X]=-1 pentru fiecare X, insamnand ca X este singur
in componenta lui si nu este subordonat nimanui.
Atunci:
1) FIND(X) trebuie sa porneasca de la X si sa mearga din tata in tata pana
ajunge la un nod Y cu F[Y]=-1, caz in care Y este exponentul multimii
2) UNION(X,Y) trebuie sa afle:
TataX=FIND(X), TataY=FIND(Y)
si sa seteze F[TataX]=TataY, sau F[TataY]=TataX.
In acest fel radacinile celor doua multimi sunt legate.
Complexitatea acestor operatii. Se vede ca UNION efectueaza doua operatii
FIND si o operatie in timp constant. La randul ei, FIND(X) parcurge calea
de la X la radacina arborelui sau din DS. Avem deci interesul sa legam
nodurile in asa fel incat arborii din DS sa ramana cat mai "turtiti". Printr-o
imbunatatire intuitiva, ne putem asigura ca arborii nu depasesc inaltimea
O(log N). Ideea este ca la o operatie UNION sa legam intotdeauna radacina
arborelui mai scund de radacina arborelui mai inalt. De exemplu, la operatia
UNION(0,5) de mai sus, l-am legat pe 0 de 5 si am obtinut un arbore de inaltime
3. Daca l-am fi legat pe 5 de 0 am fi obtinut un arbore de inaltime 4.
Asadar complexitatea ambelor operatii este O(log N).
***************************************************************************
Pentru a da lovitura de gratie programului nostru si a va lasa in sfarsit sa
va duceti pe la treburile voastre, avem nevoie de urmatoarele structuri de
date:
1) O asemenea structura DS in care muchiile sunt etichetate cu momentul
adaugarii lor:
5
/ | \
E/ A| \F
/ | \
1 2 0
/ | \
B/ C| D/
/ | \
6 3 4
2) Un vector cu postordinea nodurilor in arborele original.
T = A B C D E F G
P = 2 6 1 4 3 0 5
Remarcam din nou ca muchiile in DS sunt etichetate crescator de la frunze
spre radacina. Algoritmul functioneaza astfel:
LCA(X,Y):-
o Cauta in DS muchia cea mai tarzie pe calea dintre X si Y
o Fie Tz momentul la care aceasta muchie a fost adaugata
o Returneaza parintele (in arborele original) al nodului P[Tz].
Exemple:
o X=2, Y=4
Muchiile dintre 2 si 4 in DSF sunt A, D si E. Cea mai tarzie este E
La momentul E a fost parcurs nodul 3, tatal acestuia fiind nodul 5
Raspuns: 5
o X=4, Y=6
Observati in primul rand ca raspunsul trebuie sa fie 3, dar nodul 3 nici nu
apare pe calea dintre 4 si 6 in DS!
Muchiile dintre 4 si 6 sune B si D. Cea mai tarzie este D.
La momentul D a fost vizitat nodul 4, tatal acestuia fiind 3
Raspuns: 3
Complexitate:
1) Preprocesarea inseamna o parcurgere in postordine a arborelui, dar la
fiecare pas se executa o operatie UNION (nodul X care este vizitat este
reunit cu tatal sau). Intrucat UNION costa O(log N), preprocesarea costa
O(N log N)
2) Aflarea lui LCA(X,Y): Aflarea celei mai tarzii muchii dintre X si Y se face
de asemenea prin interclasare, deci raspunsul se da in O(lungimea caii
dintre X si Y), care de data aceasta este O(log N).
3) Complexitatea totala pentru preprocesare + M query-uri:
O(N log N + M log N). Lasam ca tema pentru acasa descoperirea optimizarii
care permite o preprocesare in numai O(N).
Acesta este un exemplu destul de tipic in care e bine sa pierd mult timp
cu preprocesarea ca sa castig pe urma atunci cand mi se pun multe intrebari.
Daca as fi avut de raspuns la o singura intrebare (M=1), primul algoritm ar fi
raspuns in O(N), iar al doilea in O(N log N).
Daca tot am scris tone de explicatii, sper sa fi meritat si sa ma fi facut
clar. OBSERVATIE - sursa e cam de trei ori mai mica decat explicatiile :)
|
|