少女祈祷中...

DeMen Blog #4: Wavelet Tree - Khi Segment Tree được đảo nhãn

Tác giả: Võ Khắc Triệu (DeMen100ns)

Khi làm các bài quy hoạch động, ta sẽ các lưu các trạng thái với một số tính chất. Tuy nhiên, đôi khi ta cần thay đổi cách lưu các trạng thái sao cho số trạng thái cần duyệt qua được giảm xuống. Chúng ta gọi đó là quy hoạch động đảo nhãn (Các bạn có thể đọc thêm tại đây).

Trong bài viết này, chúng ta sẽ cùng tìm hiểu về một cấu trúc dữ liệu hay và hữu dụng: Wavelet Tree, hay mình thường gọi là Segment Tree đảo nhãn.

Kiến thức cần biết

Đặt vấn đề

Xét bài toán sau:

Xét mảng aa gồm nn phần tử nguyên dương, trả lời qq truy vấn thuộc hai loại sau:

  • 1 l r v1\ l\ r\ v: Đếm số phần tử có giá trị v\le v trong đoạn [l..r][l .. r] của mảng aa.
  • 2 l r k2\ l\ r\ k: Trả về phần tử lớn thứ kk trong đoạn [l..r][l .. r] của mảng aa.

Giới hạn: 1n,q,ai5×1051 \le n, q, a_i \le 5 \times 10^5

Ta có thể thấy hai loại truy vấn trên đều là các truy vấn liên quan đến thứ tự của giá trị trong đoạn. Merge Sort Tree và Wavelet Tree chính là các cấu trúc dữ liệu được dùng để xử lý các dạng truy vấn như trên.

Tuy nhiên, với bài toán trên, Merge Sort Tree sẽ trả lời mỗi truy vấn 1122 với độ phức tạp là O(log(n)2)O(\log(n)^2) và sẽ dễ bị chạy quá thời gian khi n,qn, q đủ lớn.

Wavelet Tree được tạo ra để giải quyết các truy vấn này chỉ trong O(log(ai))O(\log(a_i)) với mỗi truy vấn.

Ý tưởng

Thông thường, khi chúng ta xây dựng cây Merge Sort Tree, mỗi nút trên cây sẽ là một vector hoặc multiset chứa thông tin giá trị của một đoạn vị trí trên mảng và được sắp xếp tăng dần.

Ví dụ: Với mảng A=[2,1,1,3,1,4,3,2]A = [2, 1, 1, 3, 1, 4, 3, 2], ta có cây Merge Sort Tree đại diện cho mảng AA như sau:

Tuy nhiên, với Wavelet Tree, chúng ta sẽ dùng tư duy đảo nhãn để giải quyết. Trong đó, mỗi nút sẽ quản lý thông tin vị trí của một đoạn giá trị trên mảng. Chúng ta sẽ đi qua hai cải tiến để hiểu cách Wavelet Tree hoạt động.

Cải tiến 1: P-Tree

Mình sẽ gọi cải tiến này là P-Tree.

Gọi mảng PP là mảng vị trí của mảng AA, trong đó, PiP_i là một tập các index có giá trị là ii trên mảng AA.

Ví dụ: A=[2,1,1,3,1,4,3,2]A = [2, 1, 1, 3, 1, 4, 3, 2] P=[{2,3,5},{1,8},{4,7},{6}]P = [\{2, 3, 5\}, \{1, 8\}, \{4, 7\}, \{6\}] (Vì max(A)=4max(A) = 4, nên P=4|P| = 4)

Ta sẽ dựng một cây Merge Sort Tree của mảng PP, đây chính là P-Tree của mảng AA.

Với mảng AAPP như trên, ta sẽ có cây P-Tree như sau:

Ta thấy được, mỗi nút của cây P-Tree là tập các vị trí tương ứng với giá trị trên mảng AA nằm trong giá trị mà nút đó quản lý.

Với P-Tree, ta có thể xử lý các truy vấn 1122 trong O(log(n)×log(ai))O(\log(n) \times \log(a_i)) với mỗi truy vấn (Cách xử lý xin nhường lại cho độc giả tự nghiệm).

Ta thấy thuật toán có độ phức tạp bằng với Merge Sort Tree nhưng dễ cài đặt hơn. Tuy nhiên, chúng ta vẫn có thể cải tiến hơn nữa.

Cải tiến 2: Wavelet Tree

Trong cải tiến này, ta sẽ xử lý lại thông tin có được từ cây P-Tree.

Giả sử nút uu quản lý đoạn giá trị [l..r][l .. r]. Gọi mid=l+r2mid = \frac{l + r}{2}.

Gọi nút ul,urul, ur lần lượt là nút con bên trái và nút con bên bên phải của nút uu.

Ta biết rằng, ulul sẽ quản lý đoạn giá trị [l..mid][l .. mid]urur sẽ quản lý đoạn giá trị [mid+1..r][mid + 1 .. r].

Ta sẽ dựng cây Wavelet Tree như sau: Mỗi nút của cây Wavelet Tree sẽ có một dãy bit. Trong đó, nếu một giá trị xuất hiện trên nút uu của cây P-Tree và cũng xuất hiện trên nút ulul của cây P-Tree thì bit tương ứng với giá trị đó trên nút uu của cây Wavelet Tree sẽ là 11, ngược lại thì là 00.

Ví dụ: Với mảng A=[2,1,1,3,1,4,3,2]A = [2, 1, 1, 3, 1, 4, 3, 2], ta sẽ có Wavelet Tree như sau:

Lưu ý rằng các nút lá không có nút con nên ta không cần lưu thông tin gì trong đó.

Gọi preupre_u là tổng tiền tố (Prefix sum) của dãy bit của nút uu trên Wavelet Tree.

Gọi posuvpos_{u_v} là vị trí của giá trị lớn nhất v\le v trong nút uu của P-Tree. Ta sẽ gọi posuvpos_{u_v} là vị trí tương đối của vv trên nút uu.

Thông thường, nếu ta cần tìm vị trí tương đối của giá trị trên một nút trên P-Tree, ta cần phải dùng chặt nhị phân. Như vậy ta sẽ phải tốn độ phức tạp O(log(n))O(log(n)) cho với mỗi giá trị trên một nút. Với cách tổ chức dữ liệu trên cây Wavelet Tree, chúng ta sẽ làm được điều trên trong O(1)O(1). Thật vậy, chúng ta sẽ thực hiện như sau:

Với nút gốc (root) của cây thì posrootv=vpos_{{root}_v} = v.

Khi duyệt cây Wavelet Tree, ta có thể đi từ uu xuống ulul hoặc uu xuống urur nên ta sẽ chia ra 22 trường hợp để xử lý:

TH1: uu -> ulul

Như định nghĩa ở trên, chỉ có các giá trị 11 trên uu sẽ xuất hiện trong ulul. posulv\rightarrow pos_{{ul}_v} chính là số bit 11 nằm trong đoạn [1..posuv][1 .. pos_{u_v}] hay posulv=preuposuvpos_{{ul}_v} = pre_{u_{pos_{u_v}}}.

TH2: uu -> urur

Tương tự, chỉ có các giá trị 00 trên uu sẽ xuất hiện trong urur. posurv\rightarrow pos_{{ur}_v} chính là số bit 00 nằm trong đoạn [1..posuv][1 .. pos_{u_v}] hay posurv=posuvpreuposuvpos_{{ur}_v} = pos_{u_v} - pre_{u_{pos_{u_v}}}.

Từ các thông tin này, chúng ta sẽ dễ dàng trả lời các truy vấn của bài toán trên. Chúng ta sẽ đi cụ thể vào từng truy vấn.

Truy vấn 11: Đếm số phần tử có giá trị v\le v trong đoạn [l..r][l .. r] của mảng aa.

Xét một nút uu quản lý đoạn giá trị [x..y][x .. y]. Ta có, preuposrpreuposl1pre_{u_{pos_r}} - pre_{u_{pos_{l - 1}}} là số số có giá trị trong đoạn giá trị [x..y][x .. y] nằm trong đoạn vị trí [l..r][l .. r] của mảng aa.

Như vậy đáp án sẽ là ugpreuposrpreuposl1\sum_{u \in g} pre_{u_{pos_r}} - pre_{u_{pos_{l - 1}}} với gg là tập hợp ít nút nhất sao cho tổng tất cả các phạm vi mà các nút đó quản lí đúng bằng đoạn giá trị [1..v][1 .. v].

Vì chỉ có log(n)\log(n) nút được thăm và mỗi nút xử lý trong O(1)O(1) nên độ phức tạp cho truy vấn này là O(log(n))O(\log(n)).

Truy vấn 22: Trả về phần tử lớn thứ kk trong đoạn [l..r][l .. r] của mảng aa.

Ta sẽ sử dụng kỹ thuật Segment Tree Walk cho bài này và chúng ta sẽ bắt đầu từ gốc của cây Wavelet Tree.

Giả sử chúng ta đang ở đỉnh uu (quản lý đoạn giá trị [x..y][x .. y]) và cần tìm phần tử lớn thứ kk của nút uu, ta sẽ xét ba trường hợp sau:

TH1: x=yx = y (và k=1k = 1)

Hiển nhiên, xx chính là đáp án cần tìm.

TH2: Số giá trị của nút ulul nằm trong đoạn vị trí [l..r][l .. r] k\ge k.

Lúc này, ta thấy được phần tử lớn thứ kk của nút uu cũng chính là phần tử lớn thứ kk của nút ulul. Như vậy, ta đi xuống nút ulul và tiếp tục giải quyết bài toán.

TH3: Số giá trị của nút ulul nằm trong đoạn vị trí [l..r][l .. r] <k< k.

Lúc này, phần tử lớn thứ kk của nút uu sẽ là phần tử lớn thứ k(preulposrpreulposl1)k - (pre_{ul_{pos_r}} - pre_{ul_{pos_{l - 1}}}) của nút urur, ta cũng đi xuống nút urur và tiếp tục giải quyết bài toán.

Vì chỉ có log(n)\log(n) nút được thăm và mỗi nút xử lý trong O(1)O(1) nên độ phức tạp cho truy vấn này là O(log(n))O(\log(n)).

Author: Võ Khắc Triệu (DeMen100ns) @ DeMen100ns's Blog

Permalink: https://demen100ns.github.io/blog/2026-07-12-demen-blog-4-wavelet-tree/

Title: DeMen Blog #4: Wavelet Tree - Khi Segment Tree được đảo nhãn

License: All articles on this blog are licensed under the BY-NC-SA license agreement unless otherwise stated. Please indicate the source when reprinting!