tác giả: Backl1ght, Tiphereth-A, Enter-tainer, Ir1d, ksyx, leoleoasd, Xeonacid, aaron20100919
Giới thiệu
Chia căn lồng cây chỉ số nhị phân (Fenwick tree/BIT) trong những điều kiện nhất định có thể được dùng để thực hiện một số việc mà cây lồng cây có thể làm, nhưng so với cây lồng cây, mã nguồn của chia căn lồng cây chỉ số nhị phân ngắn gọn hơn và dễ thực hiện hơn.
Một ví dụ đơn giản
Một ví dụ đơn giản là truy vấn số lượng điểm trong một vùng hình chữ nhật trên mặt phẳng hai chiều.
Truy vấn vùng hình chữ nhật
Cho \(n\) điểm \((x_i, y_i)\) trong mặt phẳng hai chiều, trong đó \(1 \le i \le n, 1 \le x_i, y_i \le n, 1 \le n \le 10^5\), yêu cầu thực hiện các thao tác sau:
Cho \(a, b, c, d\), truy vấn số lượng điểm trong vùng hình chữ nhật có \((a, b)\) là góc trên bên trái và \(c, d\) là góc dưới bên phải.
Cho \(x, y\), thay đổi tung độ của điểm có hoành độ \(x\) thành \(y\).
Bài toán ép buộc trực tuyến (forced online), đảm bảo \(x_i \ne x_j (1 \le i, j \le n, i \ne j)\).
Đối với thao tác 1, có thể thông qua nguyên lý bao hàm - loại trừ hình chữ nhật để chuyển nó thành 4 truy vấn 2D partial sum để giải quyết. Vì ép buộc trực tuyến nên các thuật toán ngoại tuyến như CDQ phân trị không thể giải quyết được, từ đó nghĩ đến cây lồng cây, ví dụ như cây chỉ số nhị phân lồng Treap. Điều này thực sự có thể giải quyết vấn đề, nhưng mã nguồn quá dài và không đặc biệt dễ thực hiện.
Lưu ý rằng, đề bài còn đảm bảo thêm \(x_i \ne x_j (1 \le i, j \le n, i \ne j)\), lúc này có thể dùng chia căn lồng cây chỉ số nhị phân để giải quyết.
Khởi tạo
Đầu tiên, một \(x\) chỉ tương ứng với một \(y\), nên có thể dùng một mảng để ghi lại quan hệ ánh xạ này, ví dụ gọi \(Y_i\) biểu thị tung độ của điểm có hoành độ là \(i\).
Sau đó, lấy \(\sqrt n\) làm kích thước khối để chia căn theo hoành độ. Xây dựng một cây chỉ số nhị phân trọng số cho mỗi khối. Ký hiệu \(T_i\) là cây chỉ số nhị phân tương ứng với khối thứ \(i\), \(T_{i, j}\) biểu thị số lượng điểm trong khối \(i\) có tung độ nằm trong \((j - lowbit(j), j]\).
Truy vấn
Đối với thao tác 1, chuyển nó thành 4 truy vấn 2D partial sum. Bây giờ chỉ cần giải quyết việc cho \(a, b\), hỏi có bao nhiêu điểm thỏa mãn \(1 \le x_i \le a, 1\le y_i \le b\).
Bây giờ cần truy vấn phạm vi hoành độ là \([1, a]\). Vì phía bên phải nhất của phạm vi truy vấn có thể có một đoạn không phải là khối hoàn chỉnh, nên quét qua đoạn này một cách thô bạo (brute-force), xem có thỏa mãn \(Y_i \le b\) hay không, thống kê số lượng điểm thỏa mãn yêu cầu của đoạn này.
Bây giờ chỉ cần xử lý các khối hoàn chỉnh. Quét thô bạo qua các khối phía trước, truy vấn số lượng giá trị nhỏ hơn \(b\) trong cây chỉ số nhị phân tương ứng với mỗi khối, rồi cộng dồn vào đáp án.
Như vậy là xong? Không, lưu ý rằng khi xử lý các khối hoàn chỉnh, thực chất tương đương với việc truy vấn tổng tiền tố của \(T\), nếu khi sửa đổi cũng sử dụng kỹ thuật cây chỉ số nhị phân để xử lý \(T\), thì độ phức tạp khi truy vấn sẽ thấp hơn.
Sửa đổi
Cách làm thông thường là trước tiên tìm khối chứa điểm \(x\), sau đó thực hiện một phép trừ và một phép cộng để sửa đổi điểm trên hai cây chỉ số nhị phân trọng số, rồi đặt \(Y_x\) thành \(y\).
Nếu sử dụng tối ưu hóa đã nói ở trên, thì đó là thực hiện một quy trình sửa đổi cây chỉ số nhị phân đối với \(T\), mỗi lần sửa đổi cũng là một phép trừ và một phép cộng để sửa đổi điểm trên hai cây chỉ số nhị phân trọng số.
Thực hiện một số thay đổi đối với các bước trên, ví dụ như đổi một phép trừ và một phép cộng thành chỉ trừ, chính là xóa điểm; đổi thành chỉ cộng, chính là thêm điểm. Nhưng phải lưu ý một \(x\) chỉ có thể tương ứng với một \(y\).
Độ phức tạp không gian
Chia căn thành \(\sqrt n\) khối, mỗi khối có một cây chỉ số nhị phân chiếm không gian \(O(n)\), nên độ phức tạp không gian là \(O(n \sqrt n)\).
Độ phức tạp thời gian
Đối với truy vấn, duyệt qua đoạn không hoàn chỉnh mất \(O(\sqrt n)\). Sau đó, thực hiện truy vấn cây chỉ số nhị phân trên \(T\), mỗi \(T_i\) đi qua cũng thực hiện truy vấn cây chỉ số nhị phân, bước này có độ phức tạp là \(O(\log (\sqrt n) \log n)\). Vì vậy độ phức tạp thời gian của truy vấn là \(O (\sqrt n + \log (\sqrt n) \log n)\).
Sửa đổi cũng giống như truy vấn, độ phức tạp là \(O (\sqrt n + \log (\sqrt n) \log n)\).
Cho hai hoán vị \(a\) và \(b\), yêu cầu thực hiện hai loại thao tác sau:
Cho \(l_a, r_a, l_b, r_b\), yêu cầu truy vấn số lượng phần tử xuất hiện đồng thời trong \(a[l_a ... r_a]\) và \(b[l_b ... r_b]\).
Cho \(x, y\), thực hiện \(swap(b_x, b_y)\).
Độ dài dãy \(n\) thỏa mãn \(2 \le n \le 2 \cdot 10^5\), số lượng thao tác \(q\) thỏa mãn \(1 \le q \le 2 \cdot 10^5\).
Đối với mỗi giá trị \(i\), ký hiệu \(x_i\) là chỉ số của nó trong hoán vị \(b\), \(y_i\) là chỉ số của nó trong hoán vị \(a\). Như vậy, thao tác 1 trở thành một truy vấn số lượng điểm trong vùng hình chữ nhật, thao tác 2 có thể coi là hai thao tác sửa đổi. Và vì là hoán vị nên thỏa mãn một \(x\) tương ứng với một \(y\), do đó bài này có thể dùng chia căn lồng cây chỉ số nhị phân để viết.
Mã tham khảo (Chia căn lồng cây chỉ số nhị phân - 1s)
Cho một dãy \(a\), lấy MEX của tất cả các dãy con liên tục của \(a\) tạo thành mảng \(b\), hỏi MEX của \(b\). MEX của một dãy là số nguyên dương nhỏ nhất không xuất hiện trong dãy.
Độ dài dãy \(n\) thỏa mãn \(1 \le n \le 10^5\).
Quan sát: MEX của một dãy là \(mex\), khi và chỉ khi dãy này chứa từ \(1\) đến \(mex-1\), nhưng không chứa \(mex\).
Lần lượt phán đoán xem có tồn tại các dãy con liên tục có MEX từ \(1\) đến \(n+1\) hay không. Nếu không có dãy con liên tục nào có MEX là \(i\), thì đáp án chính là \(i\). Nếu đều tồn tại, thì đáp án là \(n + 2\).
Khi phán đoán \(i\), coi dãy như là nhiều đoạn được phân tách bởi không hoặc nhiều số \(i\). Nếu tồn tại một đoạn, đoạn này chứa từ \(1\) đến \(i - 1\), nhưng không chứa \(i\), thì điều đó có nghĩa là tồn tại dãy con liên tục có giá trị \(i\).
Dùng một mảng \(Y_j\) ghi lại vị trí của phần tử có giá trị là \(a_j\) trước đó, lấy \(j\) làm \(x\), \(Y_j\) làm \(y\), \(a_j\) làm \(z\). Như vậy, việc tính xem trong đoạn có chứa từ \(1\) đến \(i - 1\) hay không là một vấn đề ba chiều partial order. Một cách hình thức, phán đoán xem giá trị MEX của đoạn \([l, r]\) có phải là \(i\) hay không, chính là xem số lượng điểm thỏa mãn \(l \le j \le r, Y_j \le l - 1, a_j \le i - 1\) có phải là \(i-1\) hay không.
Nếu sau khi phán đoán xong các phần tử có giá trị \(i\) mới chèn các điểm tương ứng vào, lúc này vì trong \([l, r]\) chỉ tồn tại các phần tử có \(a_j \le i - 1\), nên vấn đề ba chiều partial order nói trên có thể chuyển đổi thành vấn đề hai chiều partial order.
Mã tham khảo (Chia căn lồng cây chỉ số nhị phân - 78ms)
#include<cmath>#include<cstdio>#include<vector>usingnamespacestd;constexprintN=1e5+5;constexprintM=316+5;// sqrt(N) + 5// Chia cănintnn,b[N],block_size,block_cnt,block_id[N],L[N],R[N],T[M][N];voidbuild(intn){nn=n;block_size=sqrt(nn);block_cnt=nn/block_size;for(inti=1;i<=block_cnt;++i){L[i]=R[i-1]+1;R[i]=i*block_size;}if(R[block_cnt]<nn){++block_cnt;L[block_cnt]=R[block_cnt-1]+1;R[block_cnt]=nn;}for(intj=1;j<=block_cnt;++j)for(inti=L[j];i<=R[j];++i)block_id[i]=j;}intlb(intx){returnx&-x;}// d = 1: Thêm điểm (p, v)// d = -1: Xóa điểm (p, v)voidadd(intp,intv,intd){for(inti=block_id[p];i<=block_cnt;i+=lb(i))for(intj=v;j<=nn;j+=lb(j))T[i][j]+=d;}// Hỏi trong [1, r], có bao nhiêu điểm có tung độ nhỏ hơn hoặc bằng valintgetsum(intp,intv){if(!p)return0;intres=0;intid=block_id[p];for(inti=L[id];i<=p;++i)if(b[i]&&b[i]<=v)++res;for(inti=id-1;i;i-=lb(i))for(intj=v;j;j-=lb(j))res+=T[i][j];returnres;}// Hỏi trong [l, r], có bao nhiêu điểm có tung độ nhỏ hơn hoặc bằng valintquery(intl,intr,intval){if(l>r)return-1;intres=getsum(r,val)-getsum(l-1,val);returnres;}// Thêm điểm (p, v)voidupdate(intp,intv){b[p]=v;add(p,v,1);}intn,a[N];vector<int>g[N];intmain(){scanf("%d",&n);// Để giảm bớt thảo luận, thêm nút lính canh// Vì khi thêm vào cây chỉ số nhị phân, nếu là 0 có thể bị lặp vô hạn, nên dịch chuyển sang phải một vị trí// a_1 và a_{n+2} là nút lính canhfor(inti=2;i<=n+1;++i)scanf("%d",&a[i]);for(inti=2;i<=n+1;++i)g[a[i]].push_back(i);// Chia cănbuild(n+2);intans=n+2,lst,ok;for(inti=1;i<=n+1;++i){g[i].push_back(n+2);lst=1;ok=0;for(intpos:g[i]){if(query(lst+1,pos-1,lst)==i-1){ok=1;break;}lst=pos;}if(!ok){ans=i;break;}lst=1;g[i].pop_back();for(intpos:g[i]){update(pos,lst);lst=pos;}}printf("%d\n",ans);return0;}
#include<cstdio>#include<random>#include<vector>usingnamespacestd;constexprintN=1e5+5;vector<int>g[N];intn,a[N];mt19937rng(random_device{}());structTreap{structnode{node*l,*r;unsignedrnd;intsz,v;node(int_v):l(NULL),r(NULL),rnd(rng()),sz(1),v(_v){}};intget_size(node*&p){returnp?p->sz:0;}voidpush_up(node*&p){if(!p)return;p->sz=get_size(p->l)+get_size(p->r)+1;}node*root;node*merge(node*a,node*b){if(!a)returnb;if(!b)returna;if(a->rnd<b->rnd){a->r=merge(a->r,b);push_up(a);returna;}else{b->l=merge(a,b->l);push_up(b);returnb;}}voidsplit_val(node*p,constint&k,node*&a,node*&b){if(!p)a=b=NULL;else{if(p->v<=k){a=p;split_val(p->r,k,a->r,b);push_up(a);}else{b=p;split_val(p->l,k,a,b->l);push_up(b);}}}voidsplit_size(node*p,intk,node*&a,node*&b){if(!p)a=b=NULL;else{if(get_size(p->l)<=k){a=p;split_size(p->r,k-get_size(p->l),a->r,b);push_up(a);}else{b=p;split_size(p->l,k,a,b->l);push_up(b);}}}voidinsert(intval){node*a,*b;split_val(root,val,a,b);a=merge(a,newnode(val));root=merge(a,b);}intquery(intval){node*a,*b;split_val(root,val,a,b);intres=get_size(a);root=merge(a,b);returnres;}intqry(intl,intr){returnquery(r)-query(l-1);}};// Cây phân đoạnTreapT[N<<2];voidinsert(intx,intl,intr,intp,intval){T[x].insert(val);if(l==r)return;intmid=(l+r)>>1;if(p<=mid)insert(x<<1,l,mid,p,val);elseinsert(x<<1|1,mid+1,r,p,val);}intquery(intx,intl,intr,intL,intR,intval){if(l==L&&r==R)returnT[x].query(val);intmid=(l+r)>>1;if(R<=mid)returnquery(x<<1,l,mid,L,R,val);if(L>mid)returnquery(x<<1|1,mid+1,r,L,R,val);returnquery(x<<1,l,mid,L,mid,val)+query(x<<1|1,mid+1,r,mid+1,R,val);}intquery(intl,intr,intval){if(l>r)return-1;returnquery(1,1,n,l,r,val);}intmain(){scanf("%d",&n);for(inti=1;i<=n;++i)scanf("%d",&a[i]);for(inti=1;i<=n;++i)g[a[i]].push_back(i);// a_0 và a_{n+1} là nút lính canhintans=n+2,lst,ok;for(inti=1;i<=n+1;++i){g[i].push_back(n+1);lst=0;ok=0;for(intpos:g[i]){if(query(lst+1,pos-1,lst)==i-1){ok=1;break;}lst=pos;}if(!ok){ans=i;break;}lst=0;g[i].pop_back();for(intpos:g[i]){insert(1,1,n,pos,lst);lst=pos;}}printf("%d\n",ans);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