Bài 24.4. Sparse Table và ứng dụng trong LCA

Bài 24.4. Sparse Table và ứng dụng trong LCA
Trong thế giới của Cấu trúc dữ liệu và Giải thuật, việc xử lý các truy vấn trên một đoạn (range queries) là một yêu cầu cực kỳ phổ biến. Từ việc tìm giá trị nhỏ nhất, lớn nhất, tổng, hay thậm chí là tổ tiên chung gần nhất trong cây, chúng ta thường xuyên cần trích xuất thông tin từ một phạm vi nhất định của dữ liệu.
Có nhiều cấu trúc dữ liệu mạnh mẽ cho bài toán này, như Segment Tree hay Fenwick Tree. Tuy nhiên, khi dữ liệu của chúng ta là tĩnh (không thay đổi sau khi khởi tạo), Sparse Table nổi lên như một ứng dụng đặc biệt, cho phép chúng ta trả lời các truy vấn trên đoạn với tốc độ đáng kinh ngạc: O(1)!
Bài viết này sẽ đưa bạn đi sâu vào thế giới của Sparse Table: nó là gì, hoạt động như thế nào, và đặc biệt là cách nó được sử dụng như một "chìa khóa" để giải quyết bài toán tìm Tổ tiên chung gần nhất (LCA) một cách hiệu quả.
Sparse Table là gì?
Sparse Table là một cấu trúc dữ liệu được sử dụng để trả lời các truy vấn trên đoạn trên một mảng tĩnh. Ưu điểm vượt trội của nó là khả năng trả lời truy vấn trong thời gian O(1), đổi lại là thời gian tiền xử lý (preprocessing) O(N log N) và không hỗ trợ cập nhật dữ liệu.
Nó đặc biệt hiệu quả với các toán tử khả mỹ (idempotent operations), tức là các toán tử mà việc áp dụng nó nhiều lần trên cùng một phần tử không làm thay đổi kết quả. Các ví dụ điển hình là:
- Tìm giá trị nhỏ nhất (Range Minimum Query - RMQ)
- Tìm giá trị lớn nhất (Range Maximum Query - RMQ)
- Tìm ước chung lớn nhất (GCD)
- Tìm hợp (union)
- Tìm giao (intersection)
Toán tử như tính tổng thì không khả mỹ (1+1+1 khác 1+1). Sparse Table không tối ưu cho các toán tử không khả mỹ nếu đoạn truy vấn có thể bị chia nhỏ và hợp lại.
Cấu trúc và cách xây dựng
Sparse Table dựa trên ý tưởng chia để trị và quy hoạch động để lưu trữ kết quả của các truy vấn trên các đoạn có độ dài là lũy thừa của 2.
Chúng ta sử dụng một bảng 2 chiều, gọi là ST
, với kích thước khoảng N x log N
. ST[i][j]
sẽ lưu trữ kết quả của toán tử trên đoạn bắt đầu từ chỉ số i
và có độ dài 2^j
. Tức là đoạn [i, i + 2^j - 1]
.
Cơ sở (Base Case): Đối với độ dài
2^0 = 1
,ST[i][0]
đơn giản là giá trị tại chỉ sối
trong mảng gốc.ST[i][0] = arr[i]
.Bước quy hoạch động: Để tính
ST[i][j]
, tức là kết quả trên đoạn[i, i + 2^j - 1]
, chúng ta chia đoạn này làm hai nửa có độ dài2^(j-1)
:- Nửa đầu:
[i, i + 2^(j-1) - 1]
. Kết quả đã được tính và lưu ởST[i][j-1]
. - Nửa sau:
[i + 2^(j-1), i + 2^j - 1]
. Đoạn này bắt đầu tại chỉ sối + 2^(j-1)
. Kết quả đã được tính và lưu ởST[i + 2^(j-1)][j-1]
.
Vậy,
ST[i][j]
sẽ là kết quả khi áp dụng toán tử trênST[i][j-1]
vàST[i + 2^(j-1)][j-1]
.ST[i][j] = op(ST[i][j-1], ST[i + 2^(j-1)][j-1])
Ví dụ với toán tử tìm giá trị nhỏ nhất (min):
ST[i][j] = min(ST[i][j-1], ST[i + (1 << (j-1))][j-1])
(Ở đây1 << (j-1)
chính là2^(j-1)
)- Nửa đầu:
Việc xây dựng bảng ST
được thực hiện bằng cách lặp j
(từ 1 đến log N) và sau đó lặp i
(từ 0 đến N - 2^j) để điền vào bảng.
Để tính log N
một cách hiệu quả cho mỗi độ dài đoạn, chúng ta có thể tiền xử lý một bảng logarithm cơ số 2. log_2[k]
sẽ lưu floor(log2(k))
. Bảng này có thể được tính trong O(N): log_2[k] = log_2[k/2] + 1
.
Thời gian tiền xử lý để xây dựng Sparse Table là O(N log N) vì chúng ta có log N
cột (cho j
) và khoảng N
hàng (cho i
) trong bảng ST
, mỗi ô mất O(1) để tính.
Truy vấn
Giả sử chúng ta muốn trả lời truy vấn cho đoạn [L, R]
(bao gồm cả L và R). Độ dài của đoạn là len = R - L + 1
.
Để trả lời truy vấn bằng Sparse Table, chúng ta cần tìm một hoặc hai đoạn có độ dài là lũy thừa của 2 để phủ lấp toàn bộ đoạn [L, R]
. Vì Sparse Table hoạt động tốt nhất với các toán tử khả mỹ, chúng ta có thể sử dụng hai đoạn con có độ dài 2^k
mà chúng có thể chồng lấn nhau, miễn là chúng cùng nhau bao phủ đoạn [L, R]
.
Chúng ta tìm số nguyên lớn nhất k
sao cho 2^k <= len
. Giá trị k
này chính là floor(log2(len))
. Chúng ta có thể lấy giá trị k
từ bảng log_2
đã tiền xử lý: k = log_2[len]
.
Hai đoạn con có độ dài 2^k
để phủ lấp [L, R]
là:
- Đoạn bắt đầu từ
L
, có độ dài2^k
:[L, L + 2^k - 1]
. Kết quả cho đoạn này được lưu tạiST[L][k]
. - Đoạn kết thúc tại
R
, có độ dài2^k
:[R - 2^k + 1, R]
. Đoạn này bắt đầu tại chỉ sốR - 2^k + 1
. Kết quả cho đoạn này được lưu tạiST[R - 2^k + 1][k]
.
Vì toán tử là khả mỹ, kết quả trên đoạn [L, R]
chính là kết quả áp dụng toán tử lên ST[L][k]
và ST[R - 2^k + 1][k]
.
query(L, R) = op(ST[L][k], ST[R - 2^k + 1][k])
với k = log_2[R - L + 1]
Thời gian trả lời một truy vấn là O(1) vì chúng ta chỉ cần vài phép tính đơn giản và tra bảng log_2
(O(1)) và ST
(O(1)).
Ví dụ minh họa: Range Minimum Query (RMQ)
Bài toán: Cho một mảng A
gồm N
số nguyên. Trả lời truy vấn: tìm giá trị nhỏ nhất trong đoạn A[L..R]
.
Giả sử mảng A = [7, 2, 3, 0, 5, 10, 8]
(N = 7
).
log2(7) ≈ 2.8
, nên giá trị k
tối đa cần xét là floor(log2(N))
= 2. Tuy nhiên, để an toàn, chúng ta có thể dùng ceil(log2(N))
hoặc một hằng số đủ lớn, ví dụ MAX_LOG = 18
vì 2^18 > 2e5
. Mảng A
có 7 phần tử, chỉ số từ 0 đến 6.
Tiền xử lý (Build ST):
Bảng log_2:
log_2[1] = 0
log_2[2] = 1
log_2[3] = log_2[1] + 1 = 1
log_2[4] = log_2[2] + 1 = 2
log_2[5] = log_2[2] + 1 = 2
log_2[6] = log_2[3] + 1 = 2
log_2[7] = log_2[3] + 1 = 2
Bảng ST (lưu giá trị min): Kích thước ví dụ
ST[7][3]
(7 hàng, 3 cột 0..2).j = 0
(độ dài 2^0 = 1):ST[i][0] = A[i]
ST[0][0] = 7
ST[1][0] = 2
ST[2][0] = 3
ST[3][0] = 0
ST[4][0] = 5
ST[5][0] = 10
ST[6][0] = 8
j = 1
(độ dài 2^1 = 2):ST[i][1] = min(ST[i][0], ST[i+1][0])
ST[0][1] = min(ST[0][0], ST[1][0]) = min(7, 2) = 2
(đoạn [7, 2])ST[1][1] = min(ST[1][0], ST[2][0]) = min(2, 3) = 2
(đoạn [2, 3])ST[2][1] = min(ST[2][0], ST[3][0]) = min(3, 0) = 0
(đoạn [3, 0])ST[3][1] = min(ST[3][0], ST[4][0]) = min(0, 5) = 0
(đoạn [0, 5])ST[4][1] = min(ST[4][0], ST[5][0]) = min(5, 10) = 5
(đoạn [5, 10])ST[5][1] = min(ST[5][0], ST[6][0]) = min(10, 8) = 8
(đoạn [10, 8])j = 2
(độ dài 2^2 = 4):ST[i][2] = min(ST[i][1], ST[i+2][1])
ST[0][2] = min(ST[0][1], ST[2][1]) = min(2, 0) = 0
(đoạn [7, 2, 3, 0])ST[1][2] = min(ST[1][1], ST[3][1]) = min(2, 0) = 0
(đoạn [2, 3, 0, 5])ST[2][2] = min(ST[2][1], ST[4][1]) = min(0, 5) = 0
(đoạn [3, 0, 5, 10])ST[3][2] = min(ST[3][1], ST[5][1]) = min(0, 8) = 0
(đoạn [0, 5, 10, 8])
Truy vấn: Tìm min trong đoạn A[1..5]
(L=1, R=5
). Đoạn: [2, 3, 0, 5, 10]
.
Độ dài len = R - L + 1 = 5 - 1 + 1 = 5
.
k = log_2[5] = 2
. 2^k = 2^2 = 4
.
Hai đoạn con độ dài 4:
- Bắt đầu từ
L=1
:[1, 1 + 4 - 1] = [1, 4]
. Kết quả tạiST[1][2]
. - Kết thúc tại
R=5
:[5 - 4 + 1, 5] = [2, 5]
. Kết quả tạiST[2][2]
.
Kết quả truy vấn: min(ST[1][2], ST[2][2]) = min(0, 0) = 0
.
Kiểm tra lại mảng A[1..5] = [2, 3, 0, 5, 10]
, giá trị nhỏ nhất đúng là 0.
C++ Code Minh họa cho RMQ
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
const int MAXN = 200005; // Kích thước mảng tối đa
const int MAX_LOG = 18; // log2(MAXN) khoảng 18
int arr[MAXN];
int st[MAXN][MAX_LOG];
int log_table[MAXN];
// Hàm tiền xử lý Sparse Table
void build_sparse_table(int n) {
// Tiền xử lý bảng log_2
log_table[1] = 0;
for (int i = 2; i <= n; i++) {
log_table[i] = log_table[i / 2] + 1;
}
// Khởi tạo cột j=0
for (int i = 0; i < n; i++) {
st[i][0] = arr[i];
}
// Xây dựng các cột j > 0
for (int j = 1; j < MAX_LOG; j++) {
for (int i = 0; i + (1 << j) <= n; i++) {
st[i][j] = std::min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
}
// Hàm truy vấn RMQ trên đoạn [l, r] (0-based index)
int query_sparse_table(int l, int r) {
int len = r - l + 1;
int k = log_table[len];
return std::min(st[l][k], st[r - (1 << k) + 1][k]);
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n; // Kích thước mảng
std::cout << "Nhap so phan tu cua mang: ";
std::cin >> n;
std::cout << "Nhap cac phan tu cua mang:\n";
for (int i = 0; i < n; i++) {
std::cin >> arr[i];
}
build_sparse_table(n);
int q; // Số lượng truy vấn
std::cout << "Nhap so luong truy van: ";
std::cin >> q;
std::cout << "Nhap cac truy van (l, r) (0-based index):\n";
for (int i = 0; i < q; i++) {
int l, r;
std::cin >> l >> r;
if (l > r || l < 0 || r >= n) {
std::cout << "Khoang truy van khong hop le!\n";
} else {
std::cout << "Gia tri nho nhat trong doan [" << l << ", " << r << "] la: "
<< query_sparse_table(l, r) << "\n";
}
}
return 0;
}
Giải thích Code RMQ:
- Constants
MAXN
,MAX_LOG
: Đặt kích thước tối đa cho mảng và số cột tối đa trong bảng Sparse Table.MAX_LOG
là giá trịk
lớn nhất sao cho2^k <= MAXN
. - Arrays
arr
,st
,log_table
:arr
: Mảng gốc chứa dữ liệu.st[i][j]
: Bảng Sparse Table lưu kết quả min trên đoạn[i, i + 2^j - 1]
.log_table[k]
: Lưufloor(log2(k))
.
build_sparse_table(int n)
:- Tính
log_table
: Lặp từ 2 đếnn
,log_table[i]
dựa trênlog_table[i/2]
. Đây là cách hiệu quả O(N) để tính log2 cho mọi số từ 1 đến N. - Khởi tạo
j=0
: Cột đầu tiên củast
được điền trực tiếp từ mảngarr
. - Xây dựng các cột
j > 0
: Vòng lặp ngoài đi từj=1
đếnMAX_LOG-1
. Vòng lặp trong đi từi=0
. Điều kiệni + (1 << j) <= n
đảm bảo đoạn[i, i + 2^j - 1]
không vượt quá kích thước mảng.st[i][j]
được tính bằng cách lấymin
của hai đoạn con:[i, i + 2^(j-1) - 1]
(lưu ởst[i][j-1]
) và[i + 2^(j-1), i + 2^j - 1]
(lưu ởst[i + (1 << (j - 1))][j - 1]
).
- Tính
query_sparse_table(int l, int r)
:- Tính
len = r - l + 1
. - Tìm
k = log_table[len]
. Đây là lũy thừa lớn nhất của 2 nhỏ hơn hoặc bằng độ dài đoạn. - Trả về giá trị nhỏ nhất giữa hai đoạn độ dài
2^k
: đoạn bắt đầu từl
(st[l][k]
) và đoạn kết thúc tạir
(st[r - (1 << k) + 1][k]
).
- Tính
main
: Đọc kích thước mảng và các phần tử, gọibuild_sparse_table
. Đọc số lượng truy vấn và các truy vấn, gọiquery_sparse_table
và in kết quả.
Ứng dụng của Sparse Table: Tìm Tổ Tiên Chung Gần Nhất (LCA)
Một trong những ứng dụng kinh điển và mạnh mẽ của Sparse Table là trong bài toán tìm Tổ tiên chung gần nhất (Lowest Common Ancestor - LCA) trên một cây gốc (rooted tree).
Bài toán LCA: Cho một cây có gốc R
, và hai nút bất kỳ u
và v
. Tìm nút w
là tổ tiên chung của cả u
và v
, và w
có độ sâu lớn nhất (tức là gần u
và v
nhất).
Có nhiều thuật toán để giải quyết bài toán LCA, bao gồm cả phương pháp ngoại tuyến (offline) sử dụng DSU và phương pháp trực tuyến (online) sử dụng Binary Lifting (O(N log N) preproc, O(log N) query) hoặc Segment Tree/Sparse Table trên biến đổi Euler Tour. Phương pháp dùng Sparse Table cho phép truy vấn O(1) sau tiền xử lý O(N log N).
Ý tưởng chính để áp dụng Sparse Table vào LCA là biến đổi bài toán LCA trên cây thành bài toán RMQ trên một mảng tuyến tính. Kỹ thuật biến đổi này được gọi là Euler Tour kết hợp với RMQ trên mảng độ sâu.
Biến đổi bằng Euler Tour
Thực hiện Euler Tour (DFS traversal): Duyệt cây bằng DFS. Mỗi khi thăm một nút (lần đầu vào, hoặc quay lại từ nút con), ghi lại ID của nút đó và độ sâu của nó vào hai mảng riêng biệt. Ví dụ: Cây có gốc A, con B, C. B có con D.
A (độ sâu 0) / \ B(1) C(1) / D(2)
Một Euler Tour có thể là: A -> B -> D -> (quay về B) -> B -> (quay về A) -> A -> C -> (quay về A) -> A. Mảng nút trong Euler Tour:
[A, B, D, B, A, C, A]
Mảng độ sâu tương ứng:[0, 1, 2, 1, 0, 1, 0]
Ngoài ra, trong quá trình DFS, chúng ta cần ghi lại chỉ số lần xuất hiện đầu tiên của mỗi nút trong mảng Euler Tour.
first_occurrence[A] = 0
first_occurrence[B] = 1
first_occurrence[C] = 5
first_occurrence[D] = 2
Mảng Euler Tour có kích thước khoảng
2N - 1
(với N là số nút).Kết nối LCA và RMQ: Nút có độ sâu nhỏ nhất trên đường đi giữa hai nút
u
vàv
trong cây chính là LCA(u, v). Khi chúng ta thực hiện Euler Tour, đường đi ngắn nhất trong cây giữau
vàv
sẽ tương ứng với một đoạn trong mảng Euler Tour, cụ thể là đoạn giữa lần xuất hiện đầu tiên củau
và lần xuất hiện đầu tiên củav
.Ví dụ: Tìm LCA(D, C). Lần xuất hiện đầu tiên của D là ở chỉ số 2. Lần xuất hiện đầu tiên của C là ở chỉ số 5. Xét đoạn trong mảng Euler Tour từ chỉ số 2 đến 5: Mảng nút:
[D, B, A, C]
Mảng độ sâu:[2, 1, 0, 1]
Chúng ta cần tìm nút có độ sâu nhỏ nhất trong đoạn độ sâu
[2, 5]
của mảngeuler_depths
. Độ sâu nhỏ nhất ở đây là 0, tại chỉ số 4 trong mảngeuler_depths
(tương ứng với chỉ số 4 trong mảngeuler_nodes
). Nút tại chỉ số 4 trongeuler_nodes
là A. Vậy LCA(D, C) là A.Bài toán tìm nút có độ sâu nhỏ nhất trong một đoạn của mảng
euler_depths
chính là bài toán RMQ! Và chúng ta có thể sử dụng Sparse Table để giải quyết RMQ này trong O(1).
Để sử dụng Sparse Table cho RMQ trên mảng độ sâu này, Sparse Table cần lưu trữ chỉ số của phần tử nhỏ nhất trong một đoạn, thay vì giá trị nhỏ nhất đó.
ST[i][j]
sẽ lưu trữ chỉ số trong mảngeuler_depths
(hoặceuler_nodes
) của nút có độ sâu nhỏ nhất trong đoạn[i, i + 2^j - 1]
của mảngeuler_depths
.Khi xây dựng:
ST[i][0] = i
(chỉ số của phần tử tại vị tríi
).ST[i][j]
: So sánheuler_depths[ST[i][j-1]]
vàeuler_depths[ST[i + (1 << (j-1))][j-1]]
.ST[i][j]
sẽ là chỉ số của phần tử có độ sâu nhỏ hơn trong hai chỉ số đó.Khi truy vấn
LCA(u, v)
:- Tìm
idx_u = first_occurrence[u]
vàidx_v = first_occurrence[v]
. - Nếu
idx_u > idx_v
, hoán đổi chúng. - Truy vấn Sparse Table trên mảng
euler_depths
cho đoạn[idx_u, idx_v]
. Sparse Table sẽ trả về chỉ sốmin_idx
trong mảng Euler Tour nơi độ sâu là nhỏ nhất. - Nút LCA(u, v) chính là
euler_nodes[min_idx]
.
- Tìm
C++ Code Minh họa cho LCA sử dụng Sparse Table
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
const int MAXN_TREE = 100005; // Số nút tối đa trong cây
const int MAXN_EULER = MAXN_TREE * 2; // Kích thước tối đa mảng Euler Tour (~2N)
const int MAX_LOG_EULER = 18; // log2(MAXN_EULER)
std::vector<int> adj[MAXN_TREE]; // Danh sách kề của cây
int depth[MAXN_TREE]; // Độ sâu của các nút
int euler_nodes[MAXN_EULER]; // Mảng lưu ID nút trong Euler Tour
int euler_depths[MAXN_EULER]; // Mảng lưu độ sâu trong Euler Tour
int first_occurrence[MAXN_TREE]; // Lưu chỉ số lần xuất hiện đầu tiên của mỗi nút trong mảng Euler
int euler_size = 0; // Kích thước hiện tại của mảng Euler Tour
int lca_st[MAXN_EULER][MAX_LOG_EULER]; // Sparse Table cho LCA (lưu chỉ số trong mảng Euler)
int log_table_lca[MAXN_EULER]; // Bảng log_2 cho kích thước mảng Euler
// DFS để xây dựng Euler Tour, độ sâu, và first_occurrence
void dfs_euler(int u, int p, int d) {
depth[u] = d;
first_occurrence[u] = euler_size;
euler_nodes[euler_size] = u;
euler_depths[euler_size] = d;
euler_size++;
for (int v : adj[u]) {
if (v != p) {
dfs_euler(v, u, d + 1);
// Ghi lại nút u khi quay lại từ nút con v
euler_nodes[euler_size] = u;
euler_depths[euler_size] = d;
euler_size++;
}
}
}
// So sánh độ sâu tại hai chỉ số trong mảng euler_depths
// Trả về chỉ số của phần tử có độ sâu nhỏ hơn
int compare_depth_indices(int idx1, int idx2) {
if (idx1 == -1) return idx2; // Trường hợp biên khi khởi tạo ST
if (idx2 == -1) return idx1;
return (euler_depths[idx1] < euler_depths[idx2]) ? idx1 : idx2;
}
// Xây dựng Sparse Table trên mảng euler_depths (lưu chỉ số)
void build_lca_st() {
// Tiền xử lý bảng log_2 cho kích thước mảng Euler
log_table_lca[1] = 0;
for (int i = 2; i <= euler_size; i++) {
log_table_lca[i] = log_table_lca[i / 2] + 1;
}
// Khởi tạo cột j=0: lưu chỉ số của chính nó trong mảng Euler
for (int i = 0; i < euler_size; i++) {
lca_st[i][0] = i;
}
// Xây dựng các cột j > 0
for (int j = 1; j < MAX_LOG_EULER; j++) {
for (int i = 0; i + (1 << j) <= euler_size; i++) {
// So sánh độ sâu tại chỉ số lca_st[i][j-1] và lca_st[i + 2^(j-1)][j-1]
// và lưu chỉ số có độ sâu nhỏ hơn vào lca_st[i][j]
lca_st[i][j] = compare_depth_indices(
lca_st[i][j - 1],
lca_st[i + (1 << (j - 1))][j - 1]
);
}
}
}
// Truy vấn Sparse Table trên mảng Euler để tìm chỉ số của nút có độ sâu nhỏ nhất trong đoạn [l, r]
// l, r là chỉ số trong mảng Euler (0-based index)
int query_lca_st(int l, int r) {
int len = r - l + 1;
int k = log_table_lca[len];
// So sánh độ sâu tại chỉ số lca_st[l][k] và lca_st[r - 2^k + 1][k]
return compare_depth_indices(
lca_st[l][k],
lca_st[r - (1 << k) + 1][k]
);
}
// Hàm chính tìm LCA của hai nút u và v
// u, v là ID nút (0-based index)
int get_lca(int u, int v) {
// Lấy chỉ số lần xuất hiện đầu tiên của u và v trong mảng Euler
int idx_u = first_occurrence[u];
int idx_v = first_occurrence[v];
// Đảm bảo idx_u <= idx_v
if (idx_u > idx_v) {
std::swap(idx_u, idx_v);
}
// Truy vấn Sparse Table để tìm chỉ số min_idx trong mảng Euler
// trong đoạn từ idx_u đến idx_v
int min_depth_euler_idx = query_lca_st(idx_u, idx_v);
// Nút tại chỉ số min_depth_euler_idx trong mảng euler_nodes chính là LCA
return euler_nodes[min_depth_euler_idx];
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n; // Số nút trong cây (0 đến n-1)
std::cout << "Nhap so nut trong cay: ";
std::cin >> n;
std::cout << "Nhap cac canh cua cay (dinh cha, dinh con - 0-based index):\n";
for (int i = 0; i < n - 1; i++) {
int u, v;
std::cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u); // Cây vô hướng
}
// Xây dựng Euler Tour bắt đầu từ gốc (giả sử gốc là 0)
dfs_euler(0, -1, 0);
// Xây dựng Sparse Table trên mảng độ sâu Euler
build_lca_st();
int q; // Số lượng truy vấn LCA
std::cout << "Nhap so luong truy van LCA: ";
std::cin >> q;
std::cout << "Nhap cac truy van LCA (u, v - 0-based index):\n";
for (int i = 0; i < q; i++) {
int u, v;
std::cin >> u >> v;
if (u < 0 || u >= n || v < 0 || v >= n) {
std::cout << "ID nut khong hop le!\n";
} else {
std::cout << "LCA(" << u << ", " << v << ") la: " << get_lca(u, v) << "\n";
}
}
return 0;
}
Giải thích Code LCA:
- Constants and Global Arrays: Tăng kích thước tối đa cho các mảng để chứa cả dữ liệu cây gốc và dữ liệu từ Euler Tour (khoảng 2N).
adj
là danh sách kề cho cây.depth
lưu độ sâu nút.euler_nodes
,euler_depths
lưu thông tin từ Euler Tour.first_occurrence
lưu chỉ số lần xuất hiện đầu tiên.euler_size
là biến đếm kích thước mảng Euler.lca_st
là Sparse Table, vàlog_table_lca
là bảng log cho kích thước Euler. dfs_euler(int u, int p, int d)
:- Thực hiện DFS từ nút
u
, nút cha làp
, độ sâu hiện tại làd
. - Ghi lại độ sâu của
u
và chỉ số lần xuất hiện đầu tiên củau
trong mảng Euler. - Thêm
u
và độ sâud
vào cuối mảngeuler_nodes
vàeuler_depths
, tăngeuler_size
. - Duyệt qua các con
v
củau
(trừ nút chap
). Gọi đệ quydfs_euler(v, u, d+1)
. - Sau khi hoàn thành DFS cho cây con gốc
v
, chúng ta quay lại nútu
. Ghi lạiu
và độ sâud
vào mảng Euler một lần nữa. Điều này hoàn thành "tour" qua nútv
và quay vều
.
- Thực hiện DFS từ nút
compare_depth_indices(int idx1, int idx2)
: Hàm trợ giúp đơn giản để so sánh độ sâu tại hai chỉ sốidx1
vàidx2
trong mảngeuler_depths
. Trả về chỉ số có độ sâu nhỏ hơn. Được sử dụng trong quá trình xây dựng và truy vấn Sparse Table.build_lca_st()
:- Tính
log_table_lca
cho kích thướceuler_size
hiện tại. - Khởi tạo cột
j=0
củalca_st
:lca_st[i][0] = i
. Điều này có nghĩa là đoạn độ dài 1 ([i, i]
) có phần tử nhỏ nhất (chính nó) tại chỉ sối
trong mảng Euler. - Xây dựng các cột
j > 0
tương tự như RMQ Sparse Table thông thường, nhưng sử dụng hàmcompare_depth_indices
để so sánh độ sâu và lưu chỉ số của phần tử nhỏ nhất.
- Tính
query_lca_st(int l, int r)
:- Đây là hàm RMQ trên mảng
euler_depths
nhưng trả về chỉ số của phần tử nhỏ nhất. - Tính độ dài đoạn
len = r - l + 1
và tìmk = log_table_lca[len]
. - Sử dụng
compare_depth_indices
để tìm chỉ số có độ sâu nhỏ nhất giữa hai đoạn con độ dài2^k
:[l, l + 2^k - 1]
(đại diện bởi chỉ sốlca_st[l][k]
) và[r - 2^k + 1, r]
(đại diện bởi chỉ sốlca_st[r - (1 << k) + 1][k]
).
- Đây là hàm RMQ trên mảng
get_lca(int u, int v)
:- Lấy chỉ số lần xuất hiện đầu tiên của
u
vàv
từ mảngfirst_occurrence
. - Đảm bảo chỉ số bên trái nhỏ hơn hoặc bằng chỉ số bên phải.
- Gọi
query_lca_st
trên đoạn[idx_u, idx_v]
để tìm chỉ sốmin_depth_euler_idx
trong mảng Euler nơi độ sâu nhỏ nhất xuất hiện. - Kết quả LCA chính là nút tại chỉ số
min_depth_euler_idx
trong mảngeuler_nodes
.
- Lấy chỉ số lần xuất hiện đầu tiên của
main
: Đọc thông tin cây (số nút, các cạnh). Giả sử cây có gốc tại nút 0 và độ sâu 0. Gọidfs_euler
để tiền xử lý Euler Tour. Gọibuild_lca_st
để xây dựng Sparse Table trên dữ liệu Euler. Đọc số lượng truy vấn LCA và thực hiện các truy vấn bằng cách gọiget_lca
, sau đó in kết quả.
Comments