Cây chia hợp (Dividing Combined Tree)
Về các đoạn
Chúng ta bắt đầu bằng một vấn đề khá đơn giản:
Cho một hoán vị từ \(1-n\), một đoạn có miền giá trị liên tiếp được gọi là một đoạn (segment). Hỏi số lượng đoạn trong một hoán vị. Ví dụ: \(\{5 ,3 ,4, 1 ,2\}\) có các đoạn là: \([1,1],[2,2],[3,3],[4,4],[5,5],[2,3],[4,5],[1,3],[2,5],[1,5]\).
Nhìn vào điều này, ta có cảm giác cần duy trì tập hợp miền giá trị của một đoạn, độ phức tạp có vẻ không thân thiện. Cây đoạn có thể truy vấn xem một đoạn có phải là đoạn hay không, nhưng không dễ thống kê số lượng đoạn.
Ở đây chúng ta giới thiệu cấu trúc dữ liệu thần kỳ này - Cây Phân Tích Hợp (析合树 - Divisor-Combinator Tree/Segment Tree)!
Đoạn Liên Tiếp (Continuous Segment)
Trước khi giới thiệu Cây Phân Tích Hợp, chúng ta đưa ra một số điều kiện tiên quyết. Vì định nghĩa trong tài liệu về LCA khó hiểu, ở đây đưa ra các định nghĩa không hoàn toàn chặt chẽ (nhưng dễ hiểu hơn) để độc giả dễ tiếp thu.
Hoán vị và Đoạn Liên Tiếp
Hoán vị: Định nghĩa hoán vị cấp \(n\), \(P\), là một dãy có kích thước \(n\), sao cho \(P_i\) lần lượt nhận các giá trị \(1, 2, \ldots, n\). Nói một cách hình thức hơn, hoán vị cấp \(n\), \(P\), là một tập hợp có thứ tự thỏa mãn:
- \(|P|=n\).
- \(\forall i, P_i \in [1,n]\).
- \(\nexists i, j \in [1,n], P_i=P_j\).
Đoạn Liên Tiếp: Đối với hoán vị \(P\), đoạn liên tiếp \((P,[l,r])\) biểu thị một đoạn \([l,r]\) sao cho tập giá trị của \(P_{l\sim r}\) là liên tiếp. Nói một cách hình thức hơn, đối với hoán vị \(P\), đoạn liên tiếp biểu thị một đoạn \([l,r]\) thỏa mãn:
Đặc biệt, khi \(l>r\), ta coi đây là một đoạn rỗng, ký hiệu là \((P,\varnothing)\).
Tập hợp tất cả các đoạn liên tiếp của hoán vị \(P\) được gọi là \(I_P\), và ta coi \((P,\varnothing)\in I_P\).
Phép toán của Đoạn Liên Tiếp
Đoạn liên tiếp phụ thuộc vào đoạn chỉ số và định nghĩa miền giá trị, vì vậy ta có thể định nghĩa các phép toán giao, hợp, trừ của đoạn liên tiếp.
Định nghĩa \(A=(P,[a,b]), B=(P,[x,y])\), và \(A,B\in I_P\). Các quan hệ và phép toán của đoạn liên tiếp có thể biểu diễn như sau:
- \(A\subseteq B\iff x\le a\wedge b\le y\).
- \(A=B\iff a=x\wedge b=y\).
- \(A\cap B=(P,[\max(a,x),\min(b,y)])\).
- \(A\cup B=(P,[\min(a,x),\max(b,y)])\).
- \(A\setminus B=(P,\{i|i\in[a,b]\wedge i\notin[x,y]\})\).
Thực ra các phép toán này chỉ là phép toán giao, hợp, trừ tập hợp thông thường trên các đoạn chỉ số.
Tính chất của Đoạn Liên Tiếp
Một số tính chất hiển nhiên của đoạn liên tiếp. Ta định nghĩa \(A,B\in I_P, A \cap B \neq \varnothing, A \notin B, B \notin A\), thì có \(A\cup B,A\cap B,A\setminus B,B\setminus A\in I_P\).
Chứng minh? Bản chất của chứng minh là các phép toán giao, hợp, trừ tập hợp.
Cây Phân Tích Hợp (Divisor-Combinator Tree)
Được rồi, đến phần trọng tâm. Bạn có thể đoán được, Cây Phân Tích Hợp được tạo thành từ các đoạn liên tiếp. Nhưng ta biết rằng một hoán vị có thể có tới \(O(n^2)\) đoạn liên tiếp, vì vậy chúng ta cần rút ra những đoạn liên tiếp cơ bản hơn để tạo thành Cây Phân Tích Hợp.
Đoạn Nguyên Thủy (Primitive Segment)
Thực ra tên đầy đủ là Đoạn Liên Tiếp Nguyên Thủy (Primitive Continuous Segment). Nhưng người viết cho rằng Đoạn Nguyên Thủy (Primitive Segment) ngắn gọn hơn.
Đối với hoán vị \(P\), ta cho rằng một đoạn nguyên thủy \(M\) biểu thị một đoạn liên tiếp trong tập hợp \(I_P\) mà không tồn tại đoạn liên tiếp nào giao với nó mà không chứa nó. Định nghĩa hình thức, ta cho rằng \(X\in I_P\) và thỏa mãn \(\forall A\in I_P,\ X\cap A= (P,\varnothing)\vee X\subseteq A\vee A\subseteq X\).
Tập hợp tất cả các đoạn nguyên thủy là \(M_P\). Hiển nhiên, \((P,\varnothing)\in M_P\).
Rõ ràng, giữa các đoạn nguyên thủy chỉ có quan hệ rời nhau hoặc bao hàm. Và bạn nhận ra một đoạn liên tiếp có thể được cấu thành từ một vài đoạn nguyên thủy không giao nhau. Đoạn nguyên thủy lớn nhất chính là toàn bộ hoán vị, nó bao hàm tất cả các đoạn nguyên thủy khác, vì vậy ta cho rằng các đoạn nguyên thủy có thể tạo thành một cấu trúc cây, cấu trúc này gọi là Cây Phân Tích Hợp. Nghiêm ngặt hơn, Cây Phân Tích Hợp của hoán vị \(P\) được tạo thành từ tất cả các đoạn nguyên thủy của hoán vị \(P\).
Nói nhiều định nghĩa khô khan như vậy mà không có hình vẽ thì sao được. Xét hoán vị \(P=\{9,1,10,3,2,5,7,6,8,4\}\). Cây Phân Tích Hợp tạo bởi các đoạn nguyên thủy của nó như sau:

Trong hình, chúng ta không đánh dấu các đoạn nguyên thủy. Mà mỗi nút trong hình đại diện cho một đoạn nguyên thủy. Chúng ta chỉ đánh dấu miền giá trị của mỗi đoạn nguyên thủy. Ví dụ, nút \([5,8]\) đại diện cho đoạn nguyên thủy \((P,[6,9])=\{5,7,6,8\}\). Vậy ở đây có một vấn đề: Thế nào là nút Hợp (Combinator Node) và nút Phân Tích (Divisor Node)?
Nút Phân Tích và Nút Hợp
Ở đây chúng ta đưa ra định nghĩa trực tiếp, lát nữa sẽ thảo luận về tính đúng đắn của nó.
- Miền giá trị (Value Range): Đối với một nút \(u\), ký hiệu \([u_l,u_r]\) biểu thị miền giá trị của nút này.
- Dãy con (Son Sequence): Đối với một nút \(u\) trên Cây Phân Tích Hợp, giả sử các nút con của nó là một dãy có thứ tự, dãy này chứa các miền giá trị (một số đơn lẻ \(x\) có thể được hiểu là đoạn \([x,x]\)). Ta gọi dãy này là dãy con. Ký hiệu là \(S_u\).
- Hoán vị con (Son Permutation): Đối với một dãy con \(S_u\), hoán vị được tạo thành sau khi rời rạc hóa các phần tử của nó thành các số nguyên dương được gọi là hoán vị con. Ví dụ, đối với nút \([5,8]\), dãy con của nó là \(\{[5,5],[6,7],[8,8]\}\), nếu đánh số theo thứ tự sắp xếp các đoạn, thì hoán vị con của nó là \(\{1,2,3\}\); tương tự, nút \([4,8]\) có hoán vị con là \(\{2,1\}\). Hoán vị con của nút \(u\) ký hiệu là \(P_u\).
- Nút Hợp (Combinator Node): Ta cho rằng các nút có hoán vị con là tuần tự hoặc nghịch đảo là nút hợp. Nói hình thức: các nút thỏa mãn \(P_u=\{1,2,\cdots,|S_u|\}\) hoặc \(P_u=\{|S_u|,|S_u-1|,\cdots,1\}\) được gọi là nút hợp. Nút lá không có hoán vị con, ta cũng coi nó là nút hợp.
- Nút Phân Tích (Divisor Node): Là nút không phải nút hợp.
Từ hình vẽ có thể thấy, chỉ có \([1,10]\) không phải là nút hợp, vì hoán vị con của \([1,10]\) là \(\{3,1,4,2\}\).
Tính chất của Nút Phân Tích và Nút Hợp
Tên gọi nút Phân Tích và nút Hợp bắt nguồn từ tính chất của chúng. Trước hết ta có một tính chất rất hiển nhiên: đối với bất kỳ nút \(u\) nào trên Cây Phân Tích Hợp, hợp của các đoạn chỉ số của các nút con chính là đoạn chỉ số của nút \(u\). Tức là \(\bigcup_{i=1}^{|S_u|}S_u[i]=[u_l,u_r]\).
Đối với một nút hợp \(u\): Bất kỳ đoạn con nào của dãy con đều tạo thành một đoạn liên tiếp. Nói hình thức: \(\forall S_u[l\sim r]\), thì \(\bigcup_{i=l}^rS_u[i]\in I_P\).
Đối với một nút phân tích \(u\): Bất kỳ đoạn con nào của dãy con có độ dài lớn hơn 1 (độ dài ở đây là số phần tử trong dãy con, không phải độ dài đoạn chỉ số) đều không tạo thành một đoạn liên tiếp. Nói hình thức: \(\forall S_u[l\sim r],l<r\), thì \(\bigcup_{i=l}^rS_u[i]\notin I_P\).
Tính chất của nút hợp không khó chứng minh. Vì hoán vị con của nút hợp hoặc là thứ tự tăng dần, hoặc là thứ tự giảm dần, và các đoạn giá trị cũng liền kề nhau, nên bất kỳ đoạn con liên tiếp nào cũng là một đoạn liên tiếp.
Đối với tính chất của nút phân tích, có lẽ nhiều độc giả sẽ không hiểu: tại sao bất kỳ đoạn con nào có độ dài lớn hơn 1 đều không tạo thành đoạn liên tiếp?
Sử dụng phản chứng. Giả sử đối với một nút \(u\), có một đoạn dài nhất \(S_u[l\sim r]\) tạo thành một đoạn liên tiếp. Khi đó \(A=\bigcup_{i=l}^rS_u[i]\in I_P\), điều này có nghĩa là \(A\) là một đoạn nguyên thủy! (Vì \(A\) là đoạn dài nhất trong các đoạn con, nên không thể tìm được một đoạn liên tiếp giao với nó mà không chứa nó). Khi đó bạn đã không sử dụng tất cả các đoạn nguyên thủy để tạo thành cây này. Mâu thuẫn.
Xây dựng Cây Phân Tích Hợp
Đối với việc xây dựng Cây Phân Tích Hợp cụ thể, LCA cung cấp một thuật toán tuyến tính1, dưới đây là một thuật toán \(O(n\log n)\) dễ hiểu hơn.
Phương pháp Tăng dần (Incremental Method)
Chúng ta xét phương pháp tăng dần. Sử dụng một stack để duy trì rừng Phân Tích Hợp được tạo thành từ \(i-1\) phần tử đầu tiên. Ở đây cần nhấn mạnh rằng rừng Phân Tích Hợp có nghĩa là, tại bất kỳ thời điểm nào, các nút trong stack hoặc là nút phân tích hoặc là nút hợp. Bây giờ xét nút hiện tại \(P_i\).
- Ta xét xem nó có thể trở thành con của nút trên đỉnh stack hay không. Nếu có thì nó trở thành con của nút trên đỉnh stack, sau đó lấy nút trên đỉnh stack ra, làm nút hiện tại. Lặp lại quá trình này cho đến khi stack rỗng hoặc không thể trở thành con của nút trên đỉnh stack.
- Nếu không thể trở thành con của nút trên đỉnh stack, thì xem xét xem có thể hợp nhất một số nút liên tiếp trên đỉnh stack thành một nút hay không (phương pháp phán đoán hợp nhất ở phần sau), nút được hợp nhất này làm nút hiện tại.
- Lặp lại quá trình này cho đến khi không thể tiếp tục. Sau đó kết thúc lần tăng dần này, đẩy nút hiện tại vào stack.
Tiếp theo chúng ta giải thích chi tiết hơn.
Chiến lược cụ thể
Ta cho rằng, nếu nút hiện tại có thể trở thành con của nút trên đỉnh stack, thì nút trên đỉnh stack là một nút hợp. Nếu là nút phân tích, thì sau khi hợp nhất, nút phân tích này sẽ có một đoạn con liên tiếp, không thỏa mãn tính chất của nút phân tích. Do đó, nó chắc chắn là nút hợp.
Nếu không thể trở thành con của nút trên đỉnh stack, thì ta xem xét xem một số nút liên tiếp trên đỉnh stack có thể hợp nhất với nút hiện tại hay không. Đặt \(l\) là mút trái của đoạn mà nút hiện tại nằm trong. Ta tính \(L_i\) là giá trị lớn nhất của mút trái \(< l\) trong đoạn liên tiếp có mút phải là \(i\). Nút hiện tại là \(P_i\), nút trên đỉnh stack là \(t\).
- Nếu \(L_i\) không tồn tại, thì rõ ràng nút hiện tại không thể hợp nhất;
- Nếu \(t_l=L_i\), thì đây là hai nút hợp nhất, nút sau khi hợp nhất là một nút hợp;
- Ngược lại, chắc chắn tồn tại một nút \(t'\) trong stack có mút trái \({t'}_l=L_i\), thì chắc chắn có thể hợp nhất từ nút hiện tại đến \(t'\) tạo thành một nút phân tích;
Phán đoán có thể hợp nhất hay không
Cuối cùng, ta xét cách xử lý \(L_i\). Thực tế, một đoạn liên tiếp \((P,[l,r])\) tương đương với việc hiệu cực đại trừ cực tiểu của đoạn bằng độ dài đoạn trừ 1. Tức là
Hơn nữa, do \(P\) là một hoán vị, nên đối với bất kỳ đoạn \([l,r]\) nào đều có
Vì vậy, chúng ta duy trì \(\max_{l\le i\le r}P_i-\min_{l\le i\le r}P_i-(r-l)\), việc tìm một đoạn liên tiếp tương đương với việc truy vấn giá trị nhỏ nhất!
Với ý tưởng trên, không khó để nghĩ ra thuật toán như sau. Đối với quá trình tăng dần hiện tại là \(i\), ta duy trì một mảng \(Q\) biểu thị hiệu cực đại trừ cực tiểu trừ độ dài cho đoạn \([j,i]\). Tức là
Bây giờ chúng ta muốn biết liệu có tồn tại \(j\) nhỏ nhất trong \(1\sim i-1\) sao cho \(Q_j=0\) hay không. Điều này tương đương với việc tìm giá trị nhỏ nhất của \(Q_{1\sim i-1}\). Giá trị \(j\) nhỏ nhất tìm được chính là \(L_i\). Nếu không có, thì \(L_i=i\).
Tuy nhiên, khi kết thúc lần tăng dần thứ \(i\), ta cần nhanh chóng cập nhật mảng \(Q\) sang trường hợp \(i+1\). Đoạn ban đầu là \([j,i]\) trở thành \([j,i+1]\), nếu \(P_{i+1}>\max\) hoặc \(P_{i+1}<\min\) sẽ làm \(Q_j\) thay đổi. Thay đổi như thế nào? Nếu \(P_{i+1}>\max\), coi như ta lấy \(Q_j\) trừ đi \(\max\) rồi cộng thêm \(P_{i+1}\) là hoàn thành việc cập nhật \(Q_j\); \(P_{i+1}<\min\) tương tự, coi như \(Q_j=Q_j+\min-P_{i+1}\).
Vậy nếu đối với một đoạn \([x,y]\), thỏa mãn \(\max\) của các đoạn \(P_{x\sim i},P_{x+1\sim i},P_{x+2\sim i},\cdots,P_{y\sim i}\) đều giống nhau thì sao? Bạn đã nhận ra, điều này tương đương với việc ta đang thực hiện thao tác cộng trên đoạn; tương tự, khi \(\min\) của các đoạn \(P_{x\sim i},P_{x+1\sim i},\cdots,P_{y\sim i}\) cũng là một thao tác cộng trên đoạn. Đồng thời, việc cập nhật \(\max\) và \(\min\) là độc lập với nhau, do đó có thể cập nhật riêng rẽ.
Do đó, việc duy trì \(Q\) có thể được mô tả như sau:
- Tìm \(j\) lớn nhất sao cho \(P_{j}>P_{i+1}\), thì rõ ràng, đoạn \(P_{j+1\sim i}\) đều nhỏ hơn \(P_{i+1}\), do đó cần cập nhật giá trị lớn nhất của \(Q_{j+1\sim i}\). Do \(\max(P_i), \max(P_i,P_{i-1}), \max(P_i,P_{i-1},P_{i-2}),\cdots,\max(P_i,P_{i-1},\cdots,P_{j+1})\) là (không nghiêm ngặt) đơn điệu tăng, nên có thể làm cập nhật giống nhau cho mỗi đoạn có \(\max\) giống nhau, tức là thao tác cộng trên đoạn.
- Cập nhật \(\min\) tương tự.
- Trừ mỗi \(Q_j\) đi \(1\). Vì độ dài đoạn tăng thêm \(1\).
- Truy vấn \(L_i\): tức là truy vấn chỉ số của giá trị nhỏ nhất trong \(Q\).
Đúng vậy, chúng ta có thể sử dụng cây đoạn để duy trì \(Q\)! Vẫn còn một vấn đề: làm thế nào để tìm một đoạn có \(\max/\min\) giống nhau? Sử dụng stack đơn điệu để duy trì! Duy trì hai stack đơn điệu lần lượt cho \(\max/\min\). Rõ ràng, \(\max/\min\) của các đoạn có đầu mút là hai phần tử liền kề trong stack là giống nhau, do đó trong quá trình duy trì stack đơn điệu, ta đồng thời cập nhật cây đoạn.
Việc duy trì cụ thể xem trong mã nguồn.
Nói nhiều như vậy có vẻ các bạn cũng thấy rối, vậy ta xem hình trước đã. Cảnh báo hình lớn!

Triển khai
Cuối cùng đưa ra một mã nguồn tham khảo. Mã nguồn chuyển từ Blog của Đại Mễ Bính, có thêm một số chú thích.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
Tài liệu tham khảo và Liên kết
Blog của Đại Mễ Bính -【Ghi chép học tập】Cây Phân Tích Hợp
-
Lưu Thừa Áo. Cấu trúc dữ liệu đoạn liên tiếp đơn giản. Giao lưu trại viên WC2019. ↩
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