Mục đích của bài viết là để giới thiệu về một kỹ thuật trên cây xuất hiện khá gần đây: cây ảo (virtual tree). Về cơ bản, cây ảo chính là một cách khác để biểu diễn cây hiện tại thành một dạng "nén" và sử dụng linh hoạt trong một số bài toán.
Trước khi bắt đầu bài viết, tụi mình cần các bạn có một số kiến thức về:
Giờ thì chúc mọi người đọc bài viết vui vẻ.
Cho một cây gồm
Giới hạn :
Trong thuật toán trâu, trong mỗi truy vấn ta chỉ cần duyệt qua
Độ phức tạp tệ nhất cho mỗi truy vấn sẽ là
Ta có một nhận xét quan trọng sau :
Vậy bài toán của ta sẽ trở thành : đếm số lượng các cặp đỉnh
Độ phức tạp của ta bây giờ với mỗi truy vấn là
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+5;
int n, k, a[N], sz[N];
vector<int> g[N];
long long ans = 0;
void dfs_solve(int u, int e){
//duyệt qua cả cây
for(int v : g[u]) if(v != e){
dfs_solve(v, u);
sz[u] += sz[v];
//số lượng đỉnh thuộc truy vấn trong cây con gốc u
}
if(e != -1){
//tính số cặp (a[i], a[j]) đi qua cạnh (u, cha của u)
ans += 1ll * sz[u] * (k-sz[u]);
}
}
void solve_query(){
cin >> k;
for(int i = 1; i <= k; ++i){
cin >> a[i];
sz[a[i]] = 1;
//khởi tạo tại đỉnh a[i] là một đỉnh trong truy vấn
}
ans = 0; dfs_solve(1, -1); //tính truy vấn
cout << ans << '\n';
for(int i = 1; i <= n; ++i) sz[i] = 0; //reset mảng sz
}
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
cin >> n;
for(int i = 1; i < n; ++i){
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
int q; cin >> q;
while(q--) solve_query();
return 0;
}
Với hướng tiếp cận
Giả sử hai đỉnh
Có thể thấy rằng tất cả các cạnh nằm trên đường đi từ
Từ đó, ta có ý tưởng: Thay vì duyệt lại
Bây giờ, mục tiêu của chúng ta là dựng một cây mới (gọi là cây ảo) sao cho cây này chứa:
Tuy nhiên, nếu chỉ sử dụng những đỉnh thuộc truy vấn thì vẫn chưa đủ. Khi nén cạnh ta cần phải có những đỉnh làm "trung gian" cho nhiều cạnh đi qua.
Ví dụ trong hình sau :
Ta không thể cứ để
Thế là một vấn đề mới lại được đặt ra là số đỉnh trong cây ảo (gọi là đỉnh "quan trọng") ta cần dựng ít nhất là bao nhiêu?
Câu trả lời là nhỏ hơn
Đầu tiên, tập hợp những đỉnh "quan trọng" đó chắc chắn sẽ bao gồm
Ta sẽ sắp xếp lại
Bây giờ, ta chỉ cần chứng minh, với mọi cặp đỉnh bất kì thuộc truy vấn, LCA của chúng tồn tại trong danh sách được xây dựng như trên. Khi đó, tất cả những cạnh cần được xét sẽ nằm trong cây ảo dưới dạng cạnh "nén".
Bây giờ ta sẽ đến với việc xây dựng cạnh cho cây ảo.
Sau khi dựng hết tập hợp những đỉnh cần thiết, ta lại tiếp tục sắp xếp chúng theo thứ tự duyệt DFS.
Ta sẽ từ thứ tự DFS và Đường đi Euler trên cây để "tìm" lại các cạnh.
Giả sử ta đang có tập hợp đỉnh sau khi sắp xếp lại là
Tiếp theo, ta cần phải tìm cha cho các đỉnh còn lại. Để tìm cha của một đỉnh, ta cần phải quan tâm nhánh cây.
Khi chuyển từ nhánh stack
.
Ý tưởng là stack
ban đầu của ta chứa stack
hiện tại vẫn không chứa đỉnh stack
cho đến khi nào cây con gốc đầu stack
của ta chứa stack
(hay chính là cha của
Ví dụ, cho cây sau và danh sách
Theo thuật toán được nêu trên, khi xét nhánh
Sau khi xây dựng cạnh stack
và tiếp tục duyệt danh sách
Cuối cùng, ta lại áp dụng thuật toán
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n, q, depth[N], tin[N], tout[N], dfs_timer, par[N][20];
vector<int> g[N];
void dfs(int u, int e){
tin[u] = ++dfs_timer; //thứ tự DFS
for(int v : g[u]) if(v != e){
depth[v] = depth[u] + 1; par[v][0] = u;
for(int i = 1; i <= 17; ++i){
par[v][i] = par[par[v][i-1]][i-1];
} //chuẩn bị cho bước tìm LCA bằng binary jumping
dfs(v, u);
}
tout[u] = dfs_timer;
}
//comparator thứ tự DFS
bool dfsTimer(int u, int v){
return tin[u] < tin[v];
}
//kiểm tra cây con gốc u có chứa v
bool inside(int u, int v){
return tin[u] <= tin[v] and tout[v] <= tout[u];
}
//tìm LCA
int getLCA(int u, int v){
if(inside(u, v)) return u;
if(inside(v, u)) return v;
for(int i = 17; i >= 0; --i){
if(depth[u] >= (1<<i) and !inside(par[u][i], v)){
u = par[u][i];
}
}
return par[u][0];
}
//đây là những mảng, biến sử dụng trong Virtual Tree
//sum[u] là số đỉnh thuộc cây con gốc u và nằm trong k đỉnh truy vấn
//minigraph[u] là danh sách kề của đỉnh u
//st là stack để tìm cạnh
//ans là đáp án cho truy vấn hiện tại
//VT_size là số đỉnh trong Virtual Tree
int k, a[N], sum[N], VT_size;
long long ans;
vector<int> minigraph[N];
stack<int> st;
void solve(int u, int e){
for(int v : minigraph[u]) {
solve(v, u);
//tính tổng số đỉnh thuộc truy vấn trong cây con gốc u
sum[u] += sum[v];
}
if(e != -1){
//tính số cặp x y như mô tả trước đó
ans += 1ll * (depth[u] - depth[e]) * sum[u] * (VT_size - sum[u]);
}
}
void solve_query(){
cin >> k;
for(int i = 1; i <= k; ++i){
cin >> a[i]; sum[a[i]] = 1; //khởi tạo sum[a[i]]=1
}
//sắp xếp theo thứ tự DFS
sort(a + 1, a + 1 + k, dfsTimer);
//thêm vào tập hợp LCA của 2 cặp đỉnh liên kề
for(int i = 1; i < k; ++i){
a[i+k] = getLCA(a[i], a[i+1]);
}
k = 2 * k - 1; //số đỉnh hiện tại sau khi thêm
sort(a + 1, a + 1 + k, dfsTimer);
k = unique(a + 1, a + 1 + k) - a - 1; //sắp xếp và rời rạc hóa
while(!st.empty()) st.pop(); //clear stack
st.push(a[1]); //khởi tạo stack
for(int i = 2; i <= k; ++i){
//kiểm tra nhánh cây
while(!inside(st.top(), a[i])){
st.pop();
}
//thêm cạnh
minigraph[st.top()].push_back(a[i]);
st.push(a[i]);
}
//tính đáp án
VT_size = k; ans = 0; solve(a[1], -1);
cout << ans << '\n';
//khởi tạo lại danh sách kề và mảng sum
for(int i = 1; i <= k; ++i){
minigraph[a[i]].clear(); sum[a[i]] = 0;
}
}
int main(){
ios_base::sync_with_stdio(0);
cin.tie(0);
cin >> n >> q;
for(int i=1; i<n; ++i){
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 1);
while(q--){
solve_query();
}
return 0;
}
Tổng quát lại thuật toán dựng Cây ảo :
stack
. Khi cây con có gốc là đỉnh đầu stack
không chứa đỉnh đang xét hiện tại, thì ta sẽ pop
cho đến khi gặp nhánh mới.stack
(cha) đến đỉnh đang xét (con) với trọng số là khoảng cách của Sau đây là animation minh họa cho quá trình xây dựng virtual tree:
Codeforces Round 339 (Div. 1) D
Bedao OI Contest 1 - Bài 1 : Đếm cầu