---
# System prepended metadata

title: Virtual/Auxiliary Tree (Cây ảo)

---

# Giới thiệu về Virtual/Auxiliary Tree (Cây ảo)

[TOC]
## Bài toán mở đầu 
Cho một cây gồm $N$ đỉnh không trọng số với $Q$ truy vấn. Mỗi truy vấn có dạng : 
- $k \ a_1 \ a_2 \ a_3 \ ... \ a_k$ (với $a_i \neq a_j$). Tính tổng $d(a_i, a_j)$ với mọi $1 \leq i < j \leq k$, trong đó $d(u, v)$ là khoảng cách ngắn nhất từ đỉnh $u$ đến đỉnh $v$ trên cây.

Giới hạn : $1 \leq N, Q \leq 3 \times 10^5$, tổng $k$ trong $Q$ truy vấn không vượt quá $3 \times 10^5$.

## Hướng tiếp cận : Trâu tất cả các cặp
- Trong thuật toán trâu, trong mỗi truy vấn ta chỉ cần duyệt qua $C^2_k$ cặp đỉnh và tính tổng $d(a_i, a_j)$. Nếu ta sử dụng Nhảy nhị phân (Binary jumping) thì ta sẽ tính được một cặp trong độ phức tạp $O(\log_2(n))$, còn sử dụng Đường đi Euler trên cây sẽ cho ta độ phức tạp "ổn hơn" là $O(1)$. Tuy nhiên thì cả 2 phương pháp tính từng cặp này sẽ không hiệu quả.
- Độ phức tạp tệ nhất sẽ là $O(N^2)$


::: spoiler **Code mẫu**

:::

## Hướng tiếp cận : Trâu O(N)

- Ta có một nhận xét quan trọng sau : 
    + Trong một truy vấn, ta giả sử xét cạnh $(u, v)$, nếu khi **loại bỏ cạnh này** mà **hai thành phần liên thông** sinh ra vẫn tồn tại ít nhất một đỉnh $\in$ $k$ đỉnh trong truy vấn thì ta sẽ cộng đáp án lên **tích** số đỉnh $\in$ thành phần liên thông thứ nhất với số đỉnh $\in$ thành phần liên thông thứ hai.
    + Hãy xem hình sau : ![](https://hackmd.io/_uploads/SkrenMEba.png)
    + Lúc này, cặp $(u, v)$ mà ta đang xét sẽ có $X \times Y$ cặp đỉnh $(a_i, a_j)$ với lúc này $a_i \in$ thành phần liên thông thứ nhất và $a_j \in$ thành phần liên thông thứ hai.
    ![](https://hackmd.io/_uploads/rJC63f4WT.png)

    
- Vậy bài toán của ta sẽ trở thành : đếm số lượng các cặp đỉnh $(a_i, a_j)$ đi qua cạnh $(u, v)$ nào đó. Đáp án của ta chính là tổng số cặp đỉnh thỏa điều kiện với mỗi cạnh $(u, v)$

- Độ phức tạp của ta bây giờ với mỗi truy vấn là $O(N)$, nên độ phức tạp tệ nhất là $O(Q \times N)$

::: spoiler **Code mẫu**

:::

## Hướng tiệp cận : Dựng Cây Ảo

- Từ hướng tiệp cận $O(N)$ với mỗi truy vấn, ta cần có một số tối ưu để thuật toán nhanh hơn. Giả sử hai đỉnh $u$ và $v$ đều $\in k$ đỉnh trong truy vấn và giả thuyết giữa $u$ và $v$ không tồn tại đỉnh nào khác thuộc những đỉnh trong truy vấn.  ![](https://hackmd.io/_uploads/ryqW0fEbT.png)

- Ta thấy rằng giữa $u$ và $v$ vốn không có đỉnh nào khác "đặc biệt", nhưng ta lại phải duyệt cả $T$ cạnh ($T$ là $d(u, v)$). Vậy nên ta cần có một "thứ gì đó" để dựng lại một đồ thị khác gồm không quá nhiều đỉnh (vì nếu không thuật toán sẽ trở lại $O(N)$ cho mỗi truy vấn) và số cạnh cần xét là tối ưu nhất có thể.
 
### Trọng số cạnh
- Thay vì "duyệt lại" $d(u, v)$, ta sẽ **gán** trọng số trong đồ thị mới với cạnh $(u, v)$ là $d(u, v)$. Ta đã giải quyết được **vấn đề** duyệt cạnh.

![](https://hackmd.io/_uploads/rJvYV74Z6.png)

### Đỉnh "quan trọng"

- Tuy nhiên một vấn đề mới lại được đặt ra là **số đỉnh** trong **cây mới** ta cần dựng ít nhất là bao nhiêu?

- **Câu trả lời** là nhỏ hơn $2k$.

- Tập hợp những đỉnh "quan trọng" đó chắc chắn sẽ bao gồm $k$ đỉnh trong truy vấn

- Ta sẽ **sắp xếp** lại $a$ của ta theo **thứ tự duyệt DFS** và thêm tập hợp của ta **tổ tiên chung gần nhất** của $k-1$ cặp $(a_i, a_{i+1})$. Tập hợp những đỉnh đó của ta bây giờ sẽ chính là số đỉnh cần thiết cho cây mới.
 
:::spoiler **Chứng minh**
- Giả sử xét hai đỉnh $u$ và $v$ trong truy vấn hiện tại. Sẽ có hai trường hợp sau : 
    + $u$ hoặc $v$ là tổ tiên của đỉnh còn lại : trường hợp này ta không cần xét vì trong tập hợp gốc của ta vốn chứa đỉnh là tổ tiên của đỉnh còn lại.
    + $u$ và $v$ nằm trong hai cây con $T_u$ và $T_v$ phân biệt. Không mất tính tổng quát, giả sử ta duyệt $u$ trước $v$ theo thứ tự DFS. Nếu $u$ kề $v$ sau khi sắp xếp theo **thứ tự từ điển**, $LCA(u, v)$ sẽ xuất hiện trong tập hợp của ta. Ngược lại, điều này đồng nghĩa với việc khi ta xét đoạn $p_l=u, p_{l+1}, p_{l+2}, ..., p_r=v$, ta có $p_{l+1}, p_{l+2}, ... p_{r-1} \in T_u$, nên $LCA(p_l=u, v) = LCA(p_{l+1}, v) = LCA(p_{l+2}, v) = ... = LCA(p_{r-1}, v)$ 
:::
### Dựng cạnh
- Bây giờ ta sẽ đến với việc xây dựng cạnh cho Cây Ảo của chúng ta.
- 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à $r_1, r_2, r_3, ...r_q$. Ta cần chọn ra một **gốc** để đặt làm đỉnh của cây khi dựng cạnh, để xử lý bước này dễ nhất có thể, ta sẽ chọn **đỉnh có thức tự DFS sớm nhất**, chính là $r_1$.
- 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**, các bạn hãy xem hình sau
- Khi chuyển từ nhánh $r_1 \ - \ ... \ - \ u \ - \ v$ sang nhánh $r_1 \ - \ ... \ - \ u \ - \ w$, ta cần phải có một **cấu trúc dữ liệu** để lưu lại một phần của nhánh trước đó. Đó chính là `stack`.
- Ý tưởng là `stack` ban đầu của ta chứa $r_1$,  ta sẽ xét từ $i=2, 3, ..., q$, nếu đầu `stack` hiện tại vẫn không chứa đỉnh $r_i$ (tức là đã sinh ra nhánh cây mới), thì ta sẽ `pop` cho đến khi nào cây con đầu `stack` của ta chứa $r_i$. Sau đó, ta sẽ thêm cạnh $r_i$ hiện tại với đầu `stack` (hay chính là cha của $p_i$).

- Các bạn có thể xem ví dụ sau để dễ hình sau:

![](https://hackmd.io/_uploads/S1FNeMB-T.png)


- Ta gọi `stack` của ta là $s$. hiện tại, nhánh của ta đang là $s=${$r_1, ..., u, ..., v$}. Lúc này $u$ là tổ tiên chung gần nhất giữa $v$ và $w$. Khi tới đỉnh $w$ ta sẽ lần lượt `pop` các đỉnh sau đỉnh $u$, sau đó `stack` của ta sẽ trở thành $s=${$r_1, ..., u$}, sau đó ta sẽ thêm cạnh $(u, w)$ và `push` $w$ vào $s$.

- Vậy ta đã kết thúc việc xây dựng ý tưởng của thuật toán mở đầu.

:::spoiler **Code mẫu**
```cpp=
#include<bits/stdc++.h>

using namespace std;

const int N=3e5+5;

int n, q, dfs_timer, VirtualTreeSize, h[N], tin[N], tout[N], par[N][20], sz[N];
vector<int> g[N], minigraph[N];

/* NOTE
sz[u] : tổng số đỉnh thuộc truy vấn trong cây con gốc u của Cây ảo

g là danh sách kề của cây gốc
minigraph là danh sách kề của Cây ảo 
*/

void dfs(int u, int e){
    tin[u]=++dfs_timer;
    for(int v:g[u]) if(v!=e){
        h[v]=h[u]+1; par[v][0]=u;

        for(int i=1; i<=17; ++i){
            par[v][i]=par[par[v][i-1]][i-1];
        }

        dfs(v, u);
    }
    tout[u]=dfs_timer;
}

//Kiểm tra nếu cây con gốc u chứa đỉnh v
bool inside(int u, int v){
    return tin[u]<=tin[v] and tout[v]<=tout[u];
}

//Tìm tổ tiên chung gần nhất giữa u và v
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(h[u]>=(1<<i) and !inside(par[u][i], v)){
            u=par[u][i];
        }
    }

    return par[u][0];
}

//Hàm so sánh thứ tự duyệt DFS
bool dfsTimer(int u, int v){
    return tin[u]<tin[v];
}

//Duyệt xử lý trên Cây ảo được nén từ Cây gốc
long long dfs_minigraph(int u, int e){
    long long res=0;

    for(int v:minigraph[u]) if(v!=e){
        //Tổng đáp án của mỗi cạnh trước đó
        res+=dfs_minigraph(v, u);
        
        //tính tổng số đỉnh thuộc truy vấn trong cây con gốc u
        sz[u]+=sz[v]; 
    }

    if(e!=-1){
        // Dễ thấy do e là cha của u trên Cây ảo
        // nên d(e, u) cũng chính là h[u]-h[e]
        // sz[u] là số đỉnh thuộc truy vấn trong cây con gốc u
        // VirtualTreeSize - sz[u] là số đỉnh thuộc truy vấn
        // nằm ngoài cây con gốc 
        res+=1ll*(h[u]-h[e])*sz[u]*(VirtualTreeSize-sz[u]);
    }
    return res;
}

void solve_query(){
    //Input truy vấn
    int k; cin>>k;
    vector<int> nodes(k);
    for(int i=0; i<k; ++i) {
        cin>>nodes[i];
        //Do trong Cây ảo, ta sử dụng a[i] hay lúc này là nodes[i]
        //Nên cần phải đánh dấu lại
        sz[nodes[i]]=1;
    }

    //Sắp xếp theo thứ tự DFS
    sort(nodes.begin(), nodes.end(), dfsTimer);
    
    //Thêm vào tập hợp LCA của nodes[i] và nodes[i+1]
    for(int i=0; i+1<k; ++i){
        nodes.push_back(getLCA(nodes[i], nodes[i+1]));
    }

    //Lọc các giá trị phân biệt
    sort(nodes.begin(), nodes.end(), dfsTimer); nodes.erase(unique(nodes.begin(), nodes.end()), nodes.end());

    //Khởi tạo stack bằng gốc của Cây ảo
    vector<int> s={nodes[0]}; 

    for(int i=1; i<nodes.size(); ++i){
        //Khi cây con đầu stack không chứa nodes[i] thì pop
        while(!inside(s.back(), nodes[i])){
            s.pop_back();
        }
        //Thêm cạnh đầu stack hiện tại với nodes[i]
        int p=s.back();
        minigraph[p].push_back(nodes[i]);
        minigraph[nodes[i]].push_back(p);

        s.push_back(nodes[i]);
    }

    //Xử lý bài toán mở đầu
    VirtualTreeSize=nodes.size();
    cout<<dfs_minigraph(s[0], -1)<<'\n';

    //Reset lại đồ thị Cây ảo
    for(int i=0; i<VirtualTreeSize; ++i){
        minigraph[nodes[i]].clear(); sz[nodes[i]]=0;
    }
}

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(NULL); cout.tie(NULL);

    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;
}
```
:::

# Thuật toán Virtual Tree (Cây ảo)
- Sắp xếp $a_1, a_2, a_3, ..., a_k$ theo thứ tự DFS.
- Thêm vào tập hợp những đỉnh quan trọng trong Cây ảo ta dựng $a_1, a_2, a_3, ..., a_k, LCA(a_1, a_2), LCA(a_2, a_3), ..., LCA(a_{k-1}, a_k)$.
- Dựng lại các cạnh với trọng số bằng `stack`. Khi cây con đầu `stack` của ta không chứa đỉnh đang xét hiện tại thì ta sẽ `pop` cho đến khi gặp.

- Độ phức tạp : $O(3 \times 10^5 \times \log n)$ với mỗi truy vấn sử dụng độ phức tạp $O(k \times \log n)$

:::spoiler **Code mẫu**
```cpp=
#include<bits/stdc++.h>

using namespace std;

const int N=3e5+5;

int n, q, dfs_timer, VirtualTreeSize, h[N], tin[N], tout[N], par[N][20], sz[N];
vector<int> g[N], minigraph[N];

void dfs(int u, int e){
    tin[u]=++dfs_timer;
    for(int v:g[u]) if(v!=e){
        h[v]=h[u]+1; par[v][0]=u;

        for(int i=1; i<=17; ++i){
            par[v][i]=par[par[v][i-1]][i-1];
        }

        dfs(v, u);
    }
    tout[u]=dfs_timer;
}

bool inside(int u, int v){
    return tin[u]<=tin[v] and tout[v]<=tout[u];
}

int getLCA(int u, int v){
    if(inside(u, v)) return u;
    if(inside(v, u)) return u;

    for(int i=17; i>=0; --i){
        if(h[u]>=(1<<i) and !inside(par[u][i], v)){
            u=par[u][i];
        }
    }

    return par[u][0];
}

int getDist(int u, int v){
    return h[u]+h[v]-2*h[getLCA(u, v)];
}

bool dfsTimer(int u, int v){
    return tin[u]<tin[v];
}

void solve_query(){
    int k; cin>>k;
    vector<int> nodes(k);
    for(int i=0; i<k; ++i) {
        cin>>nodes[i];
        sz[nodes[i]]=1;
    }

    sort(nodes.begin(), nodes.end(), dfsTimer);
    for(int i=0; i+1<k; ++i){
        nodes.push_back(getLCA(nodes[i], nodes[i+1]));
    }

    sort(nodes.begin(), nodes.end(), dfsTimer); nodes.erase(unique(nodes.begin(), nodes.end()), nodes.end());

    vector<int> s={nodes[0]}; 

    for(int i=1; i<nodes.size(); ++i){
        while(!inside(s.back(), nodes[i])){
            s.pop_back();
        }
        int p=s.back();
        minigraph[p].push_back(nodes[i]);
        minigraph[nodes[i]].push_back(p);

        s.push_back(nodes[i]);
    }

    //Xử lý bài toán sau khi dựng xong Cây ảo
}

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(NULL); cout.tie(NULL);

    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;
}
```
:::

# Bài tập áp dụng

[Codeforces Round 339 (Div. 1) D](https://codeforces.com/contest/613/problem/D)
- Tóm tắt đề bài : Cho cây $n$ đỉnh không trọng số và $q$ truy vấn dạng $k \ a_1 \ a_2 \ ... \ a_k$. Với mỗi truy vấn, cần đếm số lượng đỉnh **ít nhất và khác** các đỉnh trong truy vấn cần được bỏ đi để $k$ đỉnh trong truy vấn **đôi một không liên thông** với nhau.

[Bedao OI Contest 1 - Bài 1 : Đếm cầu](https://oj.vnoi.info/problem/bedao_oi1_a)
- Tóm tắt đề bài : Cho **đa đồ thị** $n$ đỉnh $m$ cạnh và $q$ truy vấn. Mỗi truy vấn dạng $a \ b \ c \ d$, cần đếm số lượng **cầu** từ bất kì đường đi đơn từ $c$ đến $d$ sau khi đã thêm cạnh $(a, b)$ vào đồ thị gốc. (Lưu ý, sau mỗi truy vấn, đồ thị gốc không thay đổi)  