Do độ phức tạp thời gian của Phân tách chuỗi nặng là \(O(n\log^2 n)\), trong khi Link-Cut Tree (LCT) mà chúng ta thường biết có độ phức tạp \(O(n\log n)\) nhưng hằng số lớn, có thể chậm hơn HLD. Vậy có phương pháp nào vừa là \(O(n\log n)\) mà hằng số lại tương đối nhỏ không? Đây là lúc Cây Nhị Phân Cân Bằng Toàn Cục (Global Balanced Binary Tree - GBBT) xuất hiện.
GBBT thực chất là một rừng (forest) các cây nhị phân, trong đó mỗi cây nhị phân duy trì một chuỗi nặng (heavy path). Tuy nhiên, các cây nhị phân trong rừng này có mối liên hệ với nhau: gốc của mỗi cây nhị phân được nối với nút cha của đỉnh đầu chuỗi nặng tương ứng, giống như trong LCT. Điểm khác biệt là GBBT là cây tĩnh, hình dạng của cây không thay đổi sau khi xây dựng, không giống như LCT.
GBBT là một cấu trúc dữ liệu có thể xử lý các thao tác sửa đổi/truy vấn trên một chuỗi (chain) của cây, đạt được:
Sửa đổi toàn bộ một chuỗi trong \(O(\log n)\).
Truy vấn toàn bộ một chuỗi trong \(O(\log n)\).
Tìm Tổ Tiên Chung Gần Nhất (LCA), sửa đổi/truy vấn trên cây con, đều đạt độ phức tạp tương tự như HLD là \(O(\log n)\).
Tính chất chính
GBBT được tạo thành từ nhiều cây nhị phân được nối với nhau bằng các cạnh nhẹ (light edges). Mỗi cây nhị phân duy trì một chuỗi nặng của cây gốc. Phép duyệt trung thứ tự (in-order traversal) của cây nhị phân này chính là thứ tự các nút trên chuỗi nặng đó theo độ sâu tăng dần. Mỗi nút chỉ thuộc về đúng một cây nhị phân.
Các cạnh được chia thành cạnh nặng (heavy edge) và cạnh nhẹ (light edge). Cạnh nặng là cạnh nằm trong cây nhị phân, được duy trì giống như cách duy trì cây nhị phân thông thường (lưu trữ con trái, con phải và nút cha). Cạnh nhẹ nối từ gốc của một cây nhị phân đến nút cha của đỉnh đầu chuỗi nặng tương ứng. Cạnh nhẹ duy trì theo kiểu "nhận cha không nhận con", tức là chỉ có thể truy cập từ nút con đến nút cha, không thể ngược lại. Lưu ý, các cạnh trong GBBT không có mối quan hệ tương ứng trực tiếp với các cạnh trong cây gốc.
Tổng số cạnh nặng và cạnh nhẹ tạo nên chiều cao của GBBT là \(O(\log n)\). Đây là tính chất đảm bảo độ phức tạp thời gian của GBBT.
Dưới đây là ví dụ về xây dựng GBBT. Hình đầu tiên là cây gốc, lấy nút 1 làm gốc. Các đường nét liền là cạnh nặng.
Hình thứ hai là GBBT được xây dựng, trong đó đường nét đứt là cạnh nhẹ, đường nét liền là cạnh nặng. Mỗi cây nhị phân được bao bởi vòng tròn màu đỏ.
Xây dựng
Đầu tiên, chúng ta thực hiện DFS giống như trong HLD thông thường để tìm con nặng của mỗi nút. Sau đó, bắt đầu từ nút gốc, tìm chuỗi nặng chứa nút gốc, đệ quy xây dựng cây cho các con nhẹ, và nối cạnh nhẹ. Tiếp theo, chúng ta cần xây dựng một cây nhị phân cho các nút trên chuỗi nặng. Ta lưu các nút trên chuỗi nặng vào một mảng, tính toán kích thước (size) đóng góp của mỗi nút (là tổng kích thước cây con của các con nhẹ cộng thêm 1 cho chính nó). Sau đó, ta tìm điểm giữa có trọng số của chuỗi này, dùng nó làm gốc của cây nhị phân, và đệ quy xây dựng hai bên, đồng thời nối cạnh nặng.
std::vector<int>G[N];intn,fa[N],son[N],sz[N];voiddfsS(intu){sz[u]=1;for(intv:G[u]){dfsS(v);sz[u]+=sz[v];if(sz[v]>sz[son[u]])son[u]=v;}}intb[N],bs[N],l[N],r[N],f[N],ss[N];// Xây dựng cây nhị phân cho các nút trong b[bl, br) và trả về gốc của cây nhị phânintcbuild(intbl,intbr){intx=bl,y=br;while(y-x>1){intmid=(x+y)>>1;if(2*(bs[mid]-bs[bl])<=bs[br]-bs[bl])x=mid;elsey=mid;}// Tìm điểm giữa có trọng số bằng tìm kiếm nhị phâny=b[x];ss[y]=br-bl;// ss: kích thước cây con nặng trong cây nhị phânif(bl<x){l[y]=cbuild(bl,x);f[l[y]]=y;}if(x+1<br){r[y]=cbuild(x+1,br);f[r[y]]=y;}returny;}intbuild(intx){inty=x;dofor(intv:G[y])if(v!=son[y])f[build(v)]=y;// Đệ quy xây dựng cây và nối cạnh nhẹ, chú ý nối từ gốc cây nhị phân, không phải từ conwhile(y=son[y]);y=0;do{b[y++]=x;// Lưu trữ các nút trên chuỗi nặngbs[y]=bs[y-1]+sz[x]-sz[son[x]];// bs: prefix sum của (size con nhẹ + 1)}while(x=son[x]);returncbuild(0,y);}
Từ mã nguồn có thể thấy độ phức tạp xây dựng là \(O(n\log n)\). Tiếp theo ta có thể chứng minh chiều cao cây là \(O(\log n)\): xem xét nhảy từ một nút bất kỳ lên nút cha. Nếu nhảy cạnh nhẹ, tương đương với việc nhảy đến một chuỗi nặng khác trong cây gốc, theo tính chất của HLD, tối đa có \(O(\log n)\) lần nhảy cạnh nhẹ; vì khi xây dựng cây nhị phân, nút gốc được chọn là điểm giữa có trọng số tính cả con nhẹ, nên sau mỗi lần nhảy cạnh nặng (trong cây nhị phân), tổng kích thước có trọng số ít nhất tăng gấp đôi, do đó số lần nhảy cạnh nặng cũng tối đa là \(O(\log n)\). Tổng chiều cao cây là \(O(\log n)\).
Truy vấn
Phần trên là về GBBT. Các thao tác sửa đổi và truy vấn trên chuỗi còn lại tương đối đơn giản, chỉ cần bắt đầu từ nút cần thao tác và nhảy liên tục lên nút gốc. Thao tác trên tất cả các nút trên chuỗi nặng có độ sâu nhỏ hơn nút đang xét, về bản chất tương đương với việc thao tác trên tất cả các nút phía bên trái của nút mục tiêu trong cây nhị phân của chuỗi nặng đó. Các thao tác này có thể được phân rã thành một loạt các thao tác trên cây con, tương tự như cách duy trì cây nhị phân thông thường, trong đó có liên quan đến việc duy trì tổng cây con và gán nhãn cây con. Ở đây sử dụng kỹ thuật tính vĩnh viễn của nhãn (lazy propagation/tag permanentization). Cũng có thể sử dụng pushdown để gán nhãn và pushup để duy trì tổng cây con, nhưng cách này có thể phức tạp hơn, vì thông thường thao tác trên cây nhị phân được thực hiện từ trên xuống, nhưng ở đây cần xác định đường nhảy trước, sau đó mới thực hiện pushdown từ trên xuống, có thể dẫn đến hằng số lớn hơn.
// a: nhãn gán cho cây con// s: tổng cây con (không tính giá trị do nhãn gán)inta[N],s[N];voidadd(intx){boolt=true;intz=0;while(x){s[x]+=z;if(t){a[x]++;if(r[x])a[r[x]]--;z+=1+ss[l[x]];s[x]-=ss[r[x]];}t=(x!=l[f[x]]);if(t&&x!=r[f[x]])z=0;// Nhảy qua cạnh nhẹ cần xóa bộ nhớ đệm (clear z)x=f[x];}}intquery(intx){intret=0;boolt=true;intz=0;while(x){if(t){ret+=s[x]-s[r[x]];ret-=1ll*ss[r[x]]*a[r[x]];z+=1+ss[l[x]];}ret+=1ll*z*a[x];t=(x!=l[f[x]]);if(t&&x!=r[f[x]])z=0;// Nhảy qua cạnh nhẹ cần xóa bộ nhớ đệm (clear z)x=f[x];}returnret;}
Ngoài ra, đối với các thao tác trên cây con, cần xem xét các con nhẹ. Cần duy trì tổng cây con và nhãn cây con bao gồm cả các con nhẹ. Có thể tham khảo cách làm trong bài toán "P3384 [Template] Heavy-Light Decomposition".
#include<algorithm>#include<cstdio>#include<cstring>constexprintMAXN=1000000;constexprintMAXM=3000000;constexprintINF=0x3FFFFFFF;usingnamespacestd;structedge{intto;edge*nxt;}edges[MAXN*2+5];edge*ncnt=&edges[0],*Adj[MAXN+5];intn,m;structMatrix{intM[2][2];Matrixoperator*(constMatrix&B)const{staticMatrixret;for(inti=0;i<2;i++)for(intj=0;j<2;j++){ret.M[i][j]=-INF;for(intk=0;k<2;k++)ret.M[i][j]=max(ret.M[i][j],M[i][k]+B.M[k][j]);}returnret;}}matr1[MAXN+5],matr2[MAXN+5];// Mỗi điểm duy trì hai ma trậnintroot;intw[MAXN+5],dep[MAXN+5],son[MAXN+5],siz[MAXN+5],lsiz[MAXN+5];intg[MAXN+5][2],f[MAXN+5][2],trfa[MAXN+5],bstch[MAXN+5][2];intstk[MAXN+5],tp;boolvis[MAXN+5];voidAddEdge(intu,intv){edge*p=++ncnt;p->to=v;p->nxt=Adj[u];Adj[u]=p;edge*q=++ncnt;q->to=u;q->nxt=Adj[v];Adj[v]=q;}voidDFS(intu,intfa){siz[u]=1;for(edge*p=Adj[u];p!=NULL;p=p->nxt){intv=p->to;if(v==fa)continue;dep[v]=dep[u]+1;DFS(v,u);siz[u]+=siz[v];if(!son[u]||siz[son[u]]<siz[v])son[u]=v;}lsiz[u]=siz[u]-siz[son[u]];// size của các con nhẹ + 1}voidDFS2(intu,intfa){f[u][1]=w[u],f[u][0]=0;g[u][1]=w[u],g[u][0]=0;if(son[u]){DFS2(son[u],u);f[u][0]+=max(f[son[u]][0],f[son[u]][1]);f[u][1]+=f[son[u]][0];}for(edge*p=Adj[u];p!=NULL;p=p->nxt){intv=p->to;if(v==fa||v==son[u])continue;DFS2(v,u);f[u][0]+=max(f[v][0],f[v][1]);// f[][] là mảng DP thông thườngf[u][1]+=f[v][0];g[u][0]+=max(f[v][0],f[v][1]);// g[][] chỉ thống kê thông tin của chính nó và con nhẹg[u][1]+=f[v][0];}}voidPushUp(intu){matr2[u]=matr1[u];// matr1 là thông tin của một điểm cộng với con nhẹ, matr2 là thông tin khoảngif(bstch[u][0])matr2[u]=matr2[bstch[u][0]]*matr2[u];// Chú ý hướng chuyển đổi, nhưng nếu định nghĩa phép nhân ma trận khác, hướng có thể khácif(bstch[u][1])matr2[u]=matr2[u]*matr2[bstch[u][1]];}intgetmx2(intu){returnmax(matr2[u].M[0][0],matr2[u].M[0][1]);}intgetmx1(intu){returnmax(getmx2(u),matr2[u].M[1][0]);}intSBuild(intl,intr){if(l>r)return0;inttot=0;for(inti=l;i<=r;i++)tot+=lsiz[stk[i]];for(inti=l,sumn=lsiz[stk[l]];i<=r;i++,sumn+=lsiz[stk[i]])if(sumn*2>=tot)// Là trọng tâm (centroid){intlch=SBuild(l,i-1),rch=SBuild(i+1,r);bstch[stk[i]][0]=lch;bstch[stk[i]][1]=rch;trfa[lch]=trfa[rch]=stk[i];PushUp(stk[i]);// Thống kê thông tin khoảng lênreturnstk[i];}return0;}intBuild(intu){for(intpos=u;pos;pos=son[pos])vis[pos]=true;for(intpos=u;pos;pos=son[pos])for(edge*p=Adj[pos];p!=NULL;p=p->nxt)if(!vis[p->to])// Là con nhẹ{intv=p->to,ret=Build(v);trfa[ret]=pos;// trfa[] của cây con nhẹ nối với gốc chuỗi nặng}tp=0;for(intpos=u;pos;pos=son[pos])stk[++tp]=pos;// Lấy ra các nút trên chuỗi nặngintret=SBuild(1,tp);// Xây dựng cây nhị phân riêng cho chuỗi nặngreturnret;// Trả về gốc cây nhị phân của chuỗi nặng này}voidModify(intu,intval){matr1[u].M[1][0]+=val-w[u];w[u]=val;for(intpos=u;pos;pos=trfa[pos])if(trfa[pos]&&bstch[trfa[pos]][0]!=pos&&bstch[trfa[pos]][1]!=pos){matr1[trfa[pos]].M[0][0]-=getmx1(pos);matr1[trfa[pos]].M[0][1]=matr1[trfa[pos]].M[0][0];matr1[trfa[pos]].M[1][0]-=getmx2(pos);PushUp(pos);matr1[trfa[pos]].M[0][0]+=getmx1(pos);matr1[trfa[pos]].M[0][1]=matr1[trfa[pos]].M[0][0];matr1[trfa[pos]].M[1][0]+=getmx2(pos);}elsePushUp(pos);}intread(){intret=0,f=1;charc=0;while(c<'0'||c>'9'){c=getchar();if(c=='-')f=-f;}ret=10*ret+c-'0';while(true){c=getchar();if(c<'0'||c>'9')break;ret=10*ret+c-'0';}returnret*f;}voidprint(intx){if(x==0)return;print(x/10);putchar(x%10+'0');}intmain(){scanf("%d %d",&n,&m);for(inti=1;i<=n;i++)w[i]=read();intu,v;for(inti=1;i<n;i++){u=read(),v=read();AddEdge(u,v);}DFS(1,-1);// Tìm con nặngDFS2(1,-1);// Tính giá trị DP ban đầu, cũng có thể tính trong Build(), nhưng viết thế này đồng bộ với cách viết HLDfor(inti=1;i<=n;i++){matr1[i].M[0][0]=matr1[i].M[0][1]=g[i][0];matr1[i].M[1][0]=g[i][1],matr1[i].M[1][1]=-INF;// Khởi tạo ma trận}root=Build(1);// root là trọng tâm của chuỗi nặng chứa nút gốcintlastans=0;for(inti=1;i<=m;i++){u=read(),v=read();u^=lastans;// Yêu cầu trực tuyến (online)Modify(u,v);lastans=getmx1(root);// Lấy giá trị trực tiếpif(lastans==0)putchar('0');elseprint(lastans);putchar('\n');}return0;}
Last updated on this page:, Update history Found an error? Want to help improve? Edit this page on GitHub! Contributors to this page:OI-wiki All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply