Phân tách Heavy-Light
Giới thiệu
Phân tích chuỗi nặng nhẹ (Heavy-Light Decomposition, viết tắt HLD) là kỹ thuật chia cây thành các chuỗi để dễ dàng quản lý thông tin trên đường đi trong cây.
Cụ thể, HLD chia cây thành nhiều chuỗi, kết hợp thành cấu trúc tuyến tính, sau đó dùng các cấu trúc dữ liệu khác để quản lý thông tin.
Phân tích chuỗi nặng nhẹ (còn gọi là "phân tích chuỗi", "phân tích đường") có nhiều biến thể như phân tích chuỗi nặng (heavy chain), phân tích chuỗi dài (long chain), và phân tích dùng cho Link/Cut Tree (đôi khi gọi là "phân tích chuỗi thực"). Nếu không nói rõ, "phân tích chuỗi" thường chỉ "phân tích chuỗi nặng".
Phân tích chuỗi nặng nhẹ đảm bảo mọi đường đi trên cây có thể chia thành không quá \(O(\log n)\) chuỗi liên tiếp, các đỉnh trên mỗi chuỗi có độ sâu khác nhau (chuỗi đi từ dưới lên, LCA của chuỗi là một đầu mút).
HLD còn đảm bảo các đỉnh trên mỗi chuỗi có thứ tự DFS liên tiếp, nên có thể dùng các cấu trúc dữ liệu quản lý dãy số (như segment tree) để quản lý thông tin đường đi. Ví dụ:
- Cập nhật giá trị các đỉnh trên đường đi giữa hai đỉnh bất kỳ.
- Truy vấn tổng/giá trị lớn nhất/các thông tin khác trên đường đi giữa hai đỉnh bất kỳ (miễn là có thể quản lý trên dãy số).
Ngoài việc phối hợp với cấu trúc dữ liệu để quản lý đường đi, HLD còn có thể dùng để tìm LCA trong \(O(\log n)\) (với hằng số nhỏ). Một số bài toán còn tận dụng các tính chất đặc biệt của HLD.
Phân tích chuỗi nặng
Một số định nghĩa:
- Con nặng của một đỉnh là con có cây con lớn nhất. Nếu có nhiều con cùng lớn nhất, chọn một. Nếu không có con, không có con nặng.
- Con nhẹ là các con còn lại.
- Cạnh nối tới con nặng gọi là cạnh nặng.
- Cạnh nối tới con nhẹ gọi là cạnh nhẹ.
- Nhiều cạnh nặng nối tiếp tạo thành chuỗi nặng.
- Đỉnh lẻ cũng coi là chuỗi nặng, nên toàn bộ cây được chia thành nhiều chuỗi nặng.
Minh họa:

Cài đặt
HLD gồm hai lần DFS. Pseudocode:
DFS đầu tiên ghi nhận cha (\(\textit{father}\)), độ sâu (\(\textit{depth}\)), kích thước cây con (\(\textit{size}\)), con nặng (\(\textit{hson}\)).
DFS thứ hai ghi nhận đỉnh đầu chuỗi (\(\textit{top}\), khởi tạo là chính nó), thứ tự DFS khi duyệt chuỗi nặng (\(\textit{top}\)), và ánh xạ thứ tự DFS sang đỉnh (\(\textit{rank}\)).
Mã tham khảo:
- \(\operatorname{fa}(x)\): cha của \(x\).
- \(\operatorname{dep}(x)\): độ sâu của \(x\).
- \(\operatorname{siz}(x)\): kích thước cây con của \(x\).
- \(\operatorname{son}(x)\): con nặng của \(x\).
- \(\operatorname{top}(x)\): đầu chuỗi nặng chứa \(x\).
- \(\operatorname{dfn}(x)\): thứ tự DFS của \(x\), cũng là vị trí trên segment tree.
- \(\operatorname{rnk}(x)\): ánh xạ từ thứ tự DFS sang đỉnh, \(\operatorname{rnk}(\operatorname{dfn}(x))=x\).
Hai lần DFS: lần 1 tính \(\operatorname{fa}(x)\), \(\operatorname{dep}(x)\), \(\operatorname{siz}(x)\), \(\operatorname{son}(x)\); lần 2 tính \(\operatorname{top}(x)\), \(\operatorname{dfn}(x)\), \(\operatorname{rnk}(x)\).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Tính chất của phân tích chuỗi nặng
Mỗi đỉnh thuộc duy nhất một chuỗi nặng.
Đầu chuỗi nặng không nhất thiết là con nặng (vì cạnh nặng xác định cho từng đỉnh).
Các chuỗi nặng chia cây thành các chuỗi không giao nhau.
Khi duyệt chuỗi nặng trước, thứ tự DFS trên chuỗi là liên tiếp. Sắp xếp theo DFN sẽ ra chuỗi.
Thứ tự DFS của cây con là liên tiếp.
Khi đi qua một cạnh nhẹ, kích thước cây con giảm ít nhất một nửa.
Với mọi đường đi trên cây, chia thành hai đoạn từ LCA xuống hai đầu, mỗi đoạn đi qua tối đa \(O(\log n)\) chuỗi nặng.
Làm sao để kiểm tra HLD có thực sự \(O(\log n)\)?
Thường thì hằng số của HLD rất nhỏ, khó kiểm tra. Nếu muốn kiểm tra, có thể xây cây nhị phân có độ sâu thấp.
Có thể dùng phương pháp trung gian.
Xây cây nhị phân \(\sqrt{n}\) đỉnh, mỗi cạnh nối tới con thay bằng chuỗi dài \(\sqrt{n}\).
Như vậy, số lần chuyển chuỗi khi truy vấn ngẫu nhiên là trung bình \(\frac{\log n}{2}\), độ sâu \(O(\sqrt{n} \log n)\).
Thêm một số lá ngẫu nhiên có thể kiểm tra HLD. Tuy nhiên, hằng số nhỏ nên khó kiểm tra.
Ứng dụng phổ biến
Quản lý đường đi
Dùng HLD để tính tổng trọng số trên đường đi giữa hai đỉnh, pseudocode:
Thứ tự DFS trên chuỗi là liên tiếp, có thể dùng segment tree, BIT để quản lý.
Mỗi lần nhảy lên chuỗi có độ sâu lớn hơn, đến khi hai đỉnh cùng chuỗi.
Cách nhảy chuỗi này cũng dùng cho các truy vấn khác trên đường đi.
Quản lý thông tin cây con
Đôi khi cần quản lý thông tin trên cây con, ví dụ tăng trọng số mọi đỉnh trong cây con gốc \(x\).
Khi DFS, thứ tự DFS của cây con là liên tiếp.
Mỗi đỉnh lưu bottom là đỉnh cuối cùng của cây con trên DFS.
Như vậy, thông tin cây con chuyển thành một đoạn liên tiếp.
Tìm LCA
Nhảy lên chuỗi nặng, khi hai đỉnh cùng chuỗi, đỉnh có độ sâu nhỏ hơn là LCA.
Khi nhảy, luôn nhảy chuỗi có độ sâu lớn hơn.
Mã tham khảo:
1 2 3 4 5 6 7 8 9 | |
Đổi gốc
Xét bài toán: ngoài các thao tác cơ bản, còn có thao tác đổi gốc.
Vì HLD quản lý thông tin tĩnh, không hỗ trợ cập nhật động. Không thể mỗi lần đổi gốc lại tiền xử lý, quá tốn thời gian. Cần tận dụng thông tin đã có.
Với truy vấn đường đi, vì đường đi giữa hai đỉnh là duy nhất, không bị ảnh hưởng, xử lý như bình thường.
Với truy vấn cây con, cần ánh xạ cây con sau khi đổi gốc về cây con ban đầu. Phải xét vị trí của gốc mới, gốc cũ và đỉnh truy vấn. Chi tiết xem phần ví dụ.
Ví dụ
Dưới đây là một số ví dụ ứng dụng HLD. Đầu tiên là bài mẫu.
「ZJOI2008」Thống kê trên cây
Cho cây \(n\) đỉnh có trọng số, thực hiện \(q\) thao tác:
- Đổi trọng số một đỉnh;
- Truy vấn giá trị lớn nhất trên đường đi giữa hai đỉnh;
- Truy vấn tổng trọng số trên đường đi giữa hai đỉnh.
\(1\le n\le 30000\), \(0\le q\le 200000\).
Giải thích
Theo đề và các tính chất trên, segment tree cần hỗ trợ:
- Đổi trọng số một đỉnh;
- Truy vấn giá trị lớn nhất trên đoạn;
- Truy vấn tổng trên đoạn.
Đổi trọng số một đỉnh rất dễ.
Vì thứ tự DFS của cây con là liên tiếp (không cần HLD), đổi trọng số cây con chỉ cần cập nhật đoạn liên tiếp.
Vấn đề là làm sao cập nhật/truy vấn đường đi giữa hai đỉnh.
Xét cách tìm LCA bằng phương pháp nhảy bậc: đưa hai đỉnh lên cùng độ sâu, rồi nhảy lên cùng nhau. Với HLD cũng vậy.
Khi nhảy, nếu đang ở chuỗi nặng, nhảy lên đầu chuỗi; nếu không, nhảy lên một đỉnh. Lặp lại đến khi hai đỉnh trùng nhau. Cập nhật/truy vấn trên đoạn tương ứng.
Mỗi truy vấn đi qua tối đa \(O(\log n)\) chuỗi nặng, mỗi chuỗi truy vấn segment tree \(O(\log n)\), tổng \(O(n\log n+q\log^2 n)\). Thực tế số chuỗi nặng khó đạt \(O(\log n)\) (chỉ khi cây nhị phân đầy), nên HLD thường có hằng số nhỏ.
Mã tham khảo
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 | |
Tiếp theo là bài mẫu có đổi gốc.
LOJ 139. Phân tích chuỗi trên cây
Cho cây \(n\) đỉnh (gốc ban đầu là \(1\)), thực hiện \(m\) thao tác:
- Đổi gốc, đặt \(u\) làm gốc mới;
- Tăng trọng số các đỉnh trên đường đi giữa \(u\) và \(v\) lên \(w\);
- Tăng trọng số các đỉnh trong cây con gốc \(u\) lên \(w\);
- Truy vấn tổng trọng số các đỉnh trên đường đi giữa \(u\) và \(v\);
- Truy vấn tổng trọng số các đỉnh trong cây con gốc \(u\).
\(1 \le n,m \le 10^5\).
Giải thích
Tiền xử lý DFS với gốc \(1\), lưu thông tin HLD. Gọi cây gốc \(1\) là "cây gốc", cây sau khi đổi gốc là "cây hiện tại". Khi thao tác, cần ánh xạ truy vấn/cập nhật về cây gốc.
Đổi gốc: gán \(\textit{root}\gets u\). Truy vấn/cập nhật đường đi: không bị ảnh hưởng, xử lý như bình thường.
Truy vấn/cập nhật cây con: cần xét vị trí của \(u\) và \(\textit{root}\) trên cây gốc:
- \(u = \textit{root}\): thao tác trên toàn bộ cây, cập nhật/truy vấn segment tree gốc.
-
\(u\) là tổ tiên của \(\textit{root}\) trên cây gốc (nằm trên đường từ \(1\) tới \(\textit{root}\)):
Trường hợp này cần chú ý. Gọi \(v\) là đỉnh trên đường từ \(u\) tới \(\textit{root}\) (trừ \(u\)) có độ sâu nhỏ nhất. Khi đó, phần ngoài cây con gốc \(v\) trên cây gốc chính là cây con gốc \(u\) trên cây hiện tại.
Làm sao tìm \(v\)? Đầu tiên gán \(v\gets\textit{root}\), rồi nhảy lên chuỗi nặng cho tới khi \(\operatorname{dep}(\operatorname{top}(v))\le\operatorname{dep}(u)+1\).
- Nếu \(\operatorname{dep}(\operatorname{top}(v))=\operatorname{dep}(u)+1\), gán \(v\gets\operatorname{top}(v)\), tức \(v\) là con nhẹ của \(u\).
- Nếu \(\operatorname{dep}(\operatorname{top}(v))<\operatorname{dep}(u)+1\), tức \(u,v\) cùng chuỗi nặng, \(v\) có \(\operatorname{dfn}(v)=\operatorname{dfn}(u)+1\), nên gán \(v\gets\operatorname{rnk}(\operatorname{dfn}(u)+1)\).
Hai trường hợp này có thể gộp: sau khi nhảy, gán
\[ v\gets\operatorname{rnk}(\operatorname{dfn}(\operatorname{top}(v))+\operatorname{dep}(u)+1-\operatorname{dep}(\operatorname{top}(v))). \]Có thể kiểm chứng \(v\) tìm được như trên là đúng. Mã tham khảo cũng dùng cách này.
Vì cây con gốc \(v\) là đoạn \([\operatorname{dfn}(v),\operatorname{dfn}(v)+\operatorname{siz}(v))\), nên chỉ cần thao tác trên \([1,\operatorname{dfn}(v))\cup[\operatorname{dfn}(v)+\operatorname{siz}(v),n]\).
-
Trường hợp khác: thao tác trên cây con gốc \(u\) như bình thường.
Độ phức tạp như không đổi gốc, \(O(n\log^2 n)\).
Mã tham khảo
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 | |
Cuối cùng là một bài tương tác, ứng dụng đặc biệt của HLD.
Nauuo và cây nhị phân
Cho cây nhị phân gốc \(1\), bạn có thể hỏi khoảng cách giữa hai đỉnh bất kỳ, yêu cầu xác định cha của mỗi đỉnh.
Số đỉnh không quá \(3000\), tối đa \(30000\) truy vấn.
Giải thích
Đầu tiên hỏi \(n-1\) lần để xác định độ sâu từng đỉnh.
Sau đó, duyệt theo thứ tự tăng dần độ sâu để xác định cha từng đỉnh, khi đó các tổ tiên đều đã biết.
Trước khi xác định cha một đỉnh, xây HLD cho phần cây đã biết.
Giả sử cần tìm vị trí của đỉnh \(k\) trong cây con gốc \(u\), hỏi khoảng cách giữa \(k\) và đỉnh cuối chuỗi nặng gốc \(u\), xác định vị trí \(k\) như hình:

Đường đỏ là chuỗi nặng, \(d\) là kết quả truy vấn \(\textit{dis}(k, \textit{bot}(u))\), độ sâu \(v\) là \((\textit{dep}(k)+\textit{dep}(\textit{bot}(u))-d)/2\).
Nếu \(v\) có một con, cha của \(k\) là \(v\), nếu không thì đệ quy tìm trong cây con gốc \(w\).
Độ phức tạp \(O(n^2)\), số truy vấn \(O(n\log n)\).
Gọi \(T(n)\) là số truy vấn tối đa để tìm vị trí một đỉnh mới trong cây \(n\) đỉnh:
\(2999+\sum_{i=1}^{2999}T(i)\le 29940\), thực tế có thể đạt được nếu dữ liệu xấu, nhưng nếu thêm một chút ngẫu nhiên (ví dụ sắp xếp độ sâu không ổn định), số truy vấn khó vượt quá \(21000\).
Mã tham khảo
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 | |
Phân tích chuỗi dài
Phân tích chuỗi dài là một biến thể khác của phân tích chuỗi.
- Con nặng là con có cây con sâu nhất. Nếu có nhiều con sâu nhất, chọn một. Nếu không có con, không có con nặng.
- Con nhẹ là các con còn lại.
- Cạnh nối tới con nặng gọi là cạnh nặng.
- Cạnh nối tới con nhẹ gọi là cạnh nhẹ.
- Nhiều cạnh nặng nối tiếp tạo thành chuỗi nặng.
- Đỉnh lẻ cũng coi là chuỗi nặng, nên toàn bộ cây được chia thành nhiều chuỗi nặng.
Minh họa (cách chia này cũng là phân tích chuỗi nặng hoặc chuỗi dài):

Cách cài đặt giống phân tích chuỗi nặng, không trình bày lại.
Ứng dụng phổ biến
Phân tích chuỗi dài đảm bảo số lần chuyển chuỗi khi đi từ một đỉnh tới gốc là \(O(\sqrt{n})\).
Làm sao kiểm tra số lần chuyển chuỗi đạt tối đa?
Có thể xây cây nhị phân như sau:
Gọi tham số cây là \(D\).
Nếu \(D \neq 0\), xây cây nhị phân trái với tham số \(D-1\), cây phải là chuỗi dài \(2D-1\).
Nếu \(D = 0\), xây một lá riêng và kết thúc.
Như vậy, các lá sẽ có đường đi tới gốc toàn cạnh nhẹ, số đỉnh \(D^2\).
Chọn \(D=\sqrt{n}\) là đạt tối đa.
Tối ưu DP bằng phân tích chuỗi dài
Các bài DP trên cây có một chiều là độ sâu có thể tối ưu bằng phân tích chuỗi dài.
Ý tưởng: mỗi đỉnh kế thừa trạng thái DP của con nặng, các con nhẹ thì hợp trạng thái vào.
Codeforces 1009 F. Dominant Indices
Cho cây \(n\) đỉnh gốc \(1\).
Định nghĩa mảng độ sâu của đỉnh \(x\) là \([d_{x, 0}, d_{x, 1}, d_{x, 2}, \dots]\), với \(d_{x, i}\) là số đỉnh \(y\) thỏa:
- \(x\) là tổ tiên của \(y\);
- Đường đi từ \(x\) tới \(y\) có đúng \(i\) cạnh.
Chỉ số chủ đạo của \(x\) là chỉ số \(j\) thỏa:
- Với mọi \(k < j\), \(d_{x, k} < d_{x, j}\);
- Với mọi \(k > j\), \(d_{x, k} \le d_{x, j}\).
Tính chỉ số chủ đạo cho mọi đỉnh.
Giải thích
Gọi \(f_{i,j}\) là số đỉnh trong cây con \(i\) cách \(i\) đúng \(j\) cạnh.
Nếu chuyển trạng thái brute-force thì \(O(n^2)\).
Ý tưởng: mỗi lần chuyển trạng thái, kế thừa mảng DP của con nặng và đáp án, sau đó cập nhật.
Đầu tiên, thêm 1 vào đầu mảng DP của con nặng (đỉnh hiện tại).
Sau đó, hợp mảng DP của các con nhẹ vào mảng hiện tại.
Vì mảng DP của con nhẹ có độ dài bằng chuỗi nặng của nó, tổng độ dài các chuỗi nặng là \(n\).
Do đó, hợp mảng DP của các con nhẹ tổng \(O(n)\).
Mã tham khảo
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 | |
Lưu ý: thường mảng DP được cấp phát cho cả chuỗi nặng, mỗi đỉnh có con trỏ đầu riêng.
Độ dài mảng DP có thể xác định theo độ sâu lớn nhất của cây con.
Kỹ thuật tối ưu DP bằng phân tích chuỗi dài còn nhiều biến thể, như đánh dấu,... không trình bày thêm.
Tham khảo blog của zhoushuyu.
Truy vấn tổ tiên cấp k
Tức là hỏi một đỉnh nhảy lên cha \(k\) lần thì tới đâu.
Giả sử đã tiền xử lý tổ tiên \(2^i\) cho mỗi đỉnh.
Giả sử tìm được tổ tiên \(2^i\) của đỉnh truy vấn, với \(2^i \le k < 2^{i+1}\).
Tìm vị trí trên chuỗi nặng theo độ sâu. Gọi độ dài chuỗi là \(d\).
Tiền xử lý cho mỗi chuỗi nặng, lưu tổ tiên từ \(1\) tới \(d\) cho gốc chuỗi.
Theo tính chất phân tích chuỗi dài, \(k-2^i \le 2^i \leq d\), nên có thể \(O(1)\) truy vấn tổ tiên cấp \(k\) trên chuỗi.
Tiền xử lý cần truy vấn tổ tiên \(2^i\), và lưu bảng cho mỗi chuỗi nặng.
Tiền xử lý \(O(n\log n)\), truy vấn \(O(1)\).
Bài tập
- 「Luogu P3379」【Mẫu】LCA (HLD tìm LCA không cần cấu trúc dữ liệu, có thể luyện tập)
- 「JLOI2014」Nhà mới của sóc (cũng có thể dùng phân tích hiệu trên cây)
- 「HAOI2015」Thao tác trên cây
- 「Luogu P3384」【Mẫu】Phân tích chuỗi nặng nhẹ
- 「Luogu P1505」[Đội tuyển quốc gia] Du lịch
- 「NOI2015」Quản lý phần mềm
- 「SDOI2011」Tô màu
- 「SDOI2014」Du lịch
- 「Luogu P3979」Vương quốc xa xôi
- 「POI2014」Hotel bản nâng cao (phân tích chuỗi dài tối ưu DP)
- Chiến thuật (phân tích chuỗi dài tối ưu tham lam)
Last updated on this page:, Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:GoodCoder666, Ir1d, Marcythm, ouuan, hsfzLZH1, Xeonacid, greyqz, Chrogeek, ftxj, sshwy, LuoshuiTianyi, hyp1231, sun2snow
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply