Xét bài toán: cho mảng A[0..N-1], có Q truy vấn:
- Truy vấn 1: Cập nhật A[i] = val
- Truy vấn 2: Tính tổng đoạn [l, r]
Cách trực tiếp: mỗi truy vấn tổng mất O(N), tổng O(NQ) → TLE nếu N, Q lớn.
Ý tưởng chia căn: Chia mảng thành các khối kích thước √N. Mỗi khối lưu tổng của khối đó. Khi truy vấn, chỉ cần cộng tổng các khối đầy đủ + các phần tử lẻ ở hai đầu.
#include<bits/stdc++.h>usingnamespacestd;structSqrtDecomp{intn,blockSize;vector<int>arr,blocks;SqrtDecomp(constvector<int>&a){n=a.size();blockSize=(int)ceil(sqrt(n));arr=a;blocks.assign(blockSize,0);for(inti=0;i<n;i++)blocks[i/blockSize]+=arr[i];}voidupdate(intidx,intval){blocks[idx/blockSize]+=val-arr[idx];arr[idx]=val;}intquery(intl,intr){intsum=0;intbl=l/blockSize,br=r/blockSize;if(bl==br){for(inti=l;i<=r;i++)sum+=arr[i];}else{// Phần lẻ bên tráifor(inti=l;i<(bl+1)*blockSize;i++)sum+=arr[i];// Các khối đầy đủ ở giữafor(intb=bl+1;b<br;b++)sum+=blocks[b];// Phần lẻ bên phảifor(inti=br*blockSize;i<=r;i++)sum+=arr[i];}returnsum;}};intmain(){ios::sync_with_stdio(false);cin.tie(nullptr);intn,q;cin>>n>>q;vector<int>a(n);for(inti=0;i<n;i++)cin>>a[i];SqrtDecompsd(a);while(q--){inttype;cin>>type;if(type==1){// Cập nhậtintidx,val;cin>>idx>>val;sd.update(idx,val);}else{// Truy vấn tổng [l, r]intl,r;cin>>l>>r;cout<<sd.query(l,r)<<"\n";}}return0;}
importmathclassSqrtDecomp:def__init__(self,arr):self.n=len(arr)self.block_size=math.ceil(math.sqrt(self.n))self.arr=arr[:]self.blocks=[0]*self.block_sizeforiinrange(self.n):self.blocks[i//self.block_size]+=self.arr[i]defupdate(self,idx,val):block_id=idx//self.block_sizeself.blocks[block_id]+=val-self.arr[idx]self.arr[idx]=valdefquery(self,l,r):total=0bl=l//self.block_sizebr=r//self.block_sizeifbl==br:foriinrange(l,r+1):total+=self.arr[i]else:# Phần lẻ bên tráiforiinrange(l,(bl+1)*self.block_size):total+=self.arr[i]# Các khối đầy đủ ở giữaforbinrange(bl+1,br):total+=self.blocks[b]# Phần lẻ bên phảiforiinrange(br*self.block_size,r+1):total+=self.arr[i]returntotaln,q=map(int,input().split())a=list(map(int,input().split()))sd=SqrtDecomp(a)for_inrange(q):parts=list(map(int,input().split()))ifparts[0]==1:sd.update(parts[1],parts[2])else:print(sd.query(parts[1],parts[2]))
Mỗi khối lưu một bảng tần suất (map hoặc mảng nếu giá trị nhỏ). Khi truy vấn:
- Các khối đầy đủ: tra bảng tần suất → O(1) mỗi khối
- Phần lẻ: đếm trực tiếp → O(√N)
#include<bits/stdc++.h>usingnamespacestd;structRangeFreq{intn,blockSize;vector<int>arr;vector<unordered_map<int,int>>blockFreq;RangeFreq(constvector<int>&a){n=a.size();blockSize=max(1,(int)ceil(sqrt(n)));arr=a;intnumBlocks=(n+blockSize-1)/blockSize;blockFreq.resize(numBlocks);for(inti=0;i<n;i++)blockFreq[i/blockSize][arr[i]]++;}// Đếm số lần x xuất hiện trong [l, r]intquery(intl,intr,intx){intcnt=0;intbl=l/blockSize,br=r/blockSize;if(bl==br){for(inti=l;i<=r;i++)if(arr[i]==x)cnt++;}else{for(inti=l;i<(bl+1)*blockSize;i++)if(arr[i]==x)cnt++;for(intb=bl+1;b<br;b++){autoit=blockFreq[b].find(x);if(it!=blockFreq[b].end())cnt+=it->second;}for(inti=br*blockSize;i<=r;i++)if(arr[i]==x)cnt++;}returncnt;}};
Sqrt Decomposition xử lý tốt mỗi truy vấn riêng lẻ, nhưng Mo's Algorithm xử lý nhiều truy vấn cùng lúc bằng cách sắp xếp chúng để tối thiểu số thay đổi giữa các truy vấn liên tiếp.
Bài toán mẫu: Cho mảng A[0..N-1], có Q truy vấn: với mỗi [l, r], tính một giá trị nào đó (tổng, số phần tử khác nhau, ...). Không có cập nhật.
Sắp xếp truy vấn (l, r) theo:
1. block_id = l / blockSize (tăng dần)
2. Nếu cùng block: r tăng dần (block chẵn) hoặc r giảm dần (block lẻ)
→ đây là "odd-even optimization" để tăng cache hit
Visualization - Các truy vấn được sắp xếp:
Block size B = 3, Mảng N = 10
Block 0 (idx 0-2): Q1(0,4) Q2(1,3) Q5(2,7)
Block 1 (idx 3-5): Q3(3,8) Q6(4,6)
Block 2 (idx 6-8): Q4(7,9) Q7(6,9)
Block 3 (idx 9-9): Q8(9,9)
R di chuyển trong mỗi block:
Block 0: R → → → (tăng)
Block 1: R ← ← ← (giảm, vì block lẻ)
Block 2: R → → → (tăng)
Block 3: R → (tăng)
Tổng di chuyển của R: O(N × √N) vì R quét qua lại tối đa √N lần
Tổng di chuyển của L: O(Q × √N) vì mỗi truy vấn L di chuyển tối đa √N
→ Tổng: O((N + Q) × √N)
#include<bits/stdc++.h>usingnamespacestd;intblockSize;structQuery{intl,r,idx;booloperator<(constQuery&other)const{intblockA=l/blockSize,blockB=other.l/blockSize;if(blockA!=blockB)returnblockA<blockB;// Odd-even optimizationreturn(blockA&1)?(r>other.r):(r<other.r);}};intmain(){ios::sync_with_stdio(false);cin.tie(nullptr);intn,q;cin>>n>>q;vector<int>a(n);for(inti=0;i<n;i++)cin>>a[i];blockSize=max(1,(int)sqrt(n));vector<Query>queries(q);for(inti=0;i<q;i++){cin>>queries[i].l>>queries[i].r;queries[i].idx=i;}sort(queries.begin(),queries.end());vector<longlong>ans(q);longlongcurAns=0;intcurL=0,curR=-1;autoadd=[&](intpos){// Thêm phần tử a[pos] vào đoạn hiện tại// Cập nhật curAns tùy bài toán};autoremove=[&](intpos){// Xóa phần tử a[pos] khỏi đoạn hiện tại// Cập nhật curAns tùy bài toán};for(auto&qr:queries){while(curL>qr.l)add(--curL);while(curR<qr.r)add(++curR);while(curL<qr.l)remove(curL++);while(curR>qr.r)remove(curR--);ans[qr.idx]=curAns;}for(inti=0;i<q;i++)cout<<ans[i]<<"\n";return0;}
XOR Trick: Dùng mảng visited[] để đánh dấu. Khi thêm/xóa đỉnh, XOR bit visited:
- Nếu đỉnh xuất hiện 2 lần trong đoạn → hủy nhau (tương đương không nằm trên đường đi)
- Nếu xuất hiện 1 lần → nằm trên đường đi
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;constintLOG=17;vector<int>adj[MAXN];intval[MAXN],in[MAXN],out[MAXN],euler[2*MAXN];intpar[MAXN][LOG],depth[MAXN];inttimer=0;voiddfs(intu,intp){in[u]=timer;euler[timer++]=u;par[u][0]=p;depth[u]=(p==-1)?0:depth[p]+1;for(inti=1;i<LOG;i++)par[u][i]=(par[u][i-1]==-1)?-1:par[par[u][i-1]][i-1];for(intv:adj[u])if(v!=p)dfs(v,u);out[u]=timer;euler[timer++]=u;}intlca(intu,intv){if(depth[u]<depth[v])swap(u,v);for(inti=LOG-1;i>=0;i--)if(par[u][i]!=-1&&depth[par[u][i]]>=depth[v])u=par[u][i];if(u==v)returnu;for(inti=LOG-1;i>=0;i--)if(par[u][i]!=par[v][i])u=par[u][i],v=par[v][i];returnpar[u][0];}intblockSize;boolvisited[MAXN];intfreq[MAXN];// Tần suất giá trị trên đường đi hiện tạiintdistinct=0;structQuery{intl,r,lca_node,idx;booloperator<(constQuery&other)const{intbl=l/blockSize,br=other.l/blockSize;if(bl!=br)returnbl<br;return(bl&1)?(r>other.r):(r<other.r);}};voidtoggle(intnode){if(visited[node]){freq[val[node]]--;if(freq[val[node]]==0)distinct--;}else{if(freq[val[node]]==0)distinct++;freq[val[node]]++;}visited[node]=!visited[node];}intmain(){intn,q;cin>>n>>q;for(inti=0;i<n;i++)cin>>val[i];// Đọc cây, xây dựng Euler Tourfor(inti=0;i<n-1;i++){intu,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}memset(par,-1,sizeof(par));dfs(0,-1);blockSize=max(1,(int)sqrt(2*n));vector<Query>queries(q);for(inti=0;i<q;i++){intu,v;cin>>u>>v;if(in[u]>in[v])swap(u,v);intw=lca(u,v);if(w==u){// u là tổ tiên của vqueries[i]={in[u],in[v],-1,i};}else{queries[i]={out[u],in[v],w,i};}}sort(queries.begin(),queries.end());vector<int>ans(q);intcurL=0,curR=-1;for(auto&qr:queries){while(curL>qr.l)toggle(euler[--curL]);while(curR<qr.r)toggle(euler[++curR]);while(curL<qr.l)toggle(euler[curL++]);while(curR>qr.r)toggle(euler[curR--]);// Nếu LCA không nằm trong đoạn, thêm vàoif(qr.lca_node!=-1)toggle(qr.lca_node);ans[qr.idx]=distinct;// Hoàn tác nếu đã thêm LCAif(qr.lca_node!=-1)toggle(qr.lca_node);}for(inti=0;i<q;i++)cout<<ans[i]<<"\n";return0;}
#include<bits/stdc++.h>usingnamespacestd;intblockSize;intcurTime=0;// Số cập nhật đã áp dụngstructQuery{intl,r,t,idx;booloperator<(constQuery&o)const{if(l/blockSize!=o.l/blockSize)returnl/blockSize<o.l/blockSize;if(r/blockSize!=o.r/blockSize)returnr/blockSize<o.r/blockSize;returnt<o.t;}};structUpdate{intpos,oldVal,newVal;};intmain(){intn,q;cin>>n>>q;vector<int>a(n);for(inti=0;i<n;i++)cin>>a[i];vector<Query>queries;vector<Update>updates;intupdateCount=0;blockSize=max(1,(int)pow(n,2.0/3.0));for(inti=0;i<q;i++){chartype;cin>>type;if(type=='Q'){intl,r;cin>>l>>r;queries.push_back({l,r,updateCount,(int)queries.size()});}else{intpos,val;cin>>pos>>val;updates.push_back({pos,-1,val});// oldVal sẽ cập nhật sauupdateCount++;}}// Xử lý Mo's với chiều thời gian// add(pos), remove(pos), applyUpdate(t), undoUpdate(t)// Tương tự Mo's thường nhưng thêm apply/undo updatereturn0;}
voidapplyUpdate(Update&u,intcurL,intcurR){// Cập nhật giá trị tại u.pos// Nếu u.pos nằm trong [curL, curR]: remove old, add newa[u.pos]=u.newVal;}voidundoUpdate(Update&u,intcurL,intcurR){// Hoàn tác cập nhậta[u.pos]=u.oldVal;}
Hilbert Curve (level 2):
┌───┬───┐
│ 1 │ 2 │
│ │ │
├───┼───┤
│ 0 │ 3 │
│ │ │
└───┴───┘
Thứ tự duyệt: (0,0)→(0,1)→(1,1)→(1,0)
→ Các điểm gần nhau trên đường cong gần nhau trên mặt phẳng
// Tính Hilbert order cho điểm (x, y) trong lưới 2^n × 2^ninlinelonglonghilbertOrder(intx,inty,intpow=21,introtate=0){if(pow==0)return0;inthpow=1<<(pow-1);intseg=(x<hpow)?((y<hpow)?0:3):((y<hpow)?1:2);seg=(seg+rotate)&3;intnx=x&(x^hpow),ny=y&(y^hpow);intnrot=(rotate+[3,0,0,1][seg])&3;longlongsubSquareSize=1LL<<(2*pow-2);longlongans=seg*subSquareSize;longlongadd=hilbertOrder(nx,ny,pow-1,nrot);ans+=(seg==1||seg==2)?add:(subSquareSize-add-1);returnans;}structQuery{intl,r,idx;longlongord;voidcomputeOrder(){ord=hilbertOrder(l,r);}booloperator<(constQuery&o)const{returnord<o.ord;}};
Mo's thường: blockSize = √N
Mo's 3D (updates): blockSize = N^(2/3)
Mo's 2D: blockSize = N^(2/3) cho cả hai chiều
Thực tế: thử nghiệm với N^(2/3) hoặc N/sqrt(Q) thường cho kết quả tốt
// SAI: curL và curR xử lý không đối xứngwhile(curL>qr.l)add(--curL);// Thêm bên trái: giảm trướcwhile(curR<qr.r)add(++curR);// Thêm bên phải: tăng trướcwhile(curL<qr.l)remove(curL++);// Xóa bên trái: tăng sauwhile(curR>qr.r)remove(curR--);// Xóa bên phải: giảm sau// Đây là pattern đúng! Đảm bảo curL và curR luôn nằm trong đoạn [curL, curR]
// SAI: chỉ sắp xếp theo l/blockSizesort(queries.begin(),queries.end(),[](Querya,Queryb){returna.l/B<b.l/B;});// ĐÚNG: sắp xếp theo (l/blockSize, r) với odd-even optimization
1. Quên 0-indexed: Đề bài cho 1-indexed → phải trừ 1
2. Quên hoàn tác LCA trong Mo's on Trees
3. Block size = 0 khi N = 1 → chia cho 0 → RE
4. Giá trị âm trong freq array → cần offset hoặc dùng map
5. Không xử lý trùng LCA trong Mo's on Trees
Khi nào dùng Sqrt Decomposition?
- Cần cập nhật điểm + truy vấn đoạn → Sqrt Decomposition
- Nhiều truy vấn offline, không cập nhật → Mo's Algorithm
- Nhiều truy vấn offline + có cập nhật → Mo's 3D
- Truy vấn trên đường đi cây → Mo's on Trees
Khi nào KHÔNG dùng?
- Cần O(log N) mỗi truy vấn → dùng Segment Tree / BIT
- Có cập nhật đoạn → dùng Segment Tree với lazy propagation
- Dữ liệu nhỏ → dùng cách trực tiếp, không cần phức tạp hóa