0% found this document useful (0 votes)
5K views56 pages

Bài toán: "Tổ tiên chung gần nhất"-LCA

Chuyên đề này trình bày về bài toán tìm tổ tiên chung gần nhất (LCA) trên cây. Nó giới thiệu các khái niệm cơ bản và nhiều phương pháp giải bài toán LCA như duyệt tham lam, chia cắt, bảng thưa, dùng Euler tour, xử lý kiểu Off-line. Sau đó là 15 bài tập ví dụ về ứng dụng của LCA cùng phân tích và giải thuật.

Uploaded by

Lê Thương
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5K views56 pages

Bài toán: "Tổ tiên chung gần nhất"-LCA

Chuyên đề này trình bày về bài toán tìm tổ tiên chung gần nhất (LCA) trên cây. Nó giới thiệu các khái niệm cơ bản và nhiều phương pháp giải bài toán LCA như duyệt tham lam, chia cắt, bảng thưa, dùng Euler tour, xử lý kiểu Off-line. Sau đó là 15 bài tập ví dụ về ứng dụng của LCA cùng phân tích và giải thuật.

Uploaded by

Lê Thương
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

HỘI THẢO KHOA HỌC


CÁC TRƯỜNG THPT CHUYÊN
KHU VỰC DUYÊN HẢI VÀ ĐỒNG BẰNG BẮC BỘ
NĂM 2020

Môn: Tin học

Bài toán: “Tổ tiên chung gần nhất”-LCA

Tháng 9/2020

Trang 1
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

MỤC LỤC
Bảng chú thích một số tên, thuật ngữ viết tắt .................................................................... 4
1. Mở đầu ........................................................................................................................... 5
2. Một số khái niệm, kiến thức cơ bản............................................................................... 5
3. Dạng bài toán nào có thể cần đến LCA ......................................................................... 6
4. Các phương pháp giải bài toán LCA ............................................................................. 6
4.1. Duyệt tham lam....................................................................................................... 7
4.2. Chia căn .................................................................................................................. 8
4.3. Kĩ thuật bảng thưa (Sparse table) ......................................................................... 10
4.4. Dùng Euler tour .................................................................................................... 11
4.5. Xử lí kiểu Off-line (Tarjan's off-line LCA) .......................................................... 12
5. Một số bài tập ví dụ ..................................................................................................... 13
5.1. Bài 1: Bài tập cơ bản............................................................................................. 14
5.1.1. Đề bài: LCA ................................................................................................... 14
5.1.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 14
5.1.3. Test kèm theo ................................................................................................. 15
5.2. Bài 2: Tổ chức thi chạy Marathon ........................................................................ 15
5.2.1. Đề bài: Marathon ........................................................................................... 15
5.2.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 16
5.2.3. Test kèm theo ................................................................................................. 17
5.3. Bài 3: Du lịch thành phố (NAIPC 2016) .............................................................. 17
5.3.1. Đề bài: Tourist ............................................................................................... 17
5.3.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 18
5.3.3. Test kèm theo ................................................................................................. 19
5.4. Bài 4: VOTREE (VNOI online 2015) .................................................................. 19
5.4.1. Đề bài ............................................................................................................. 19
5.4.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 20
5.4.3. Test kèm theo ................................................................................................. 22
5.5. Bài 5: Tăng lương (Chọn đội tuyển IOI CROATIAN 2010) ............................... 22
5.5.1. Đề bài POVISICE .......................................................................................... 22
5.5.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 23
5.5.3. Test kèm theo ................................................................................................. 26
5.6. Bài 6: Nâng cấp mạng (VOI 2011) ....................................................................... 26
5.6.1. Đề bài: UPGRANET ..................................................................................... 26
5.6.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 27
5.6.3. Test kèm theo ................................................................................................. 29
5.7. Bài 7: Dạo chơi đồng cỏ (PWALK – Spoj) .......................................................... 29
5.7.1. Đề bài: PWALK ............................................................................................ 29
5.7.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 30

Trang 2
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.7.3. Test kèm theo ................................................................................................. 31


5.8. Bài 8: TREEDGE ................................................................................................. 31
5.8.1. Đề bài TREEDGE ......................................................................................... 31
5.8.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 32
5.8.3. Test kèm theo ................................................................................................. 33
5.9. Bài 9: Đường đi qua K cạnh ................................................................................. 34
5.9.1. Đề bài: TREEQ .............................................................................................. 34
5.9.2. Phân tích đề bài và đề xuất thuật toán ........................................................... 34
5.9.3. Test kèm theo ................................................................................................. 36
5.10. Bài 10: Tom & Jerry ........................................................................................... 36
5.10.1. Đề bài ........................................................................................................... 36
5.10.2. Phân tích đề bài và đề xuất thuật toán ......................................................... 37
5.10.3. Test kèm theo ............................................................................................... 39
5.11. Bài 11: Cập nhật thông tin trên cây 1 ................................................................. 39
5.11.1. Đề bài: Update tree ...................................................................................... 39
5.11.2. Phân tích đề bài và đề xuất thuật toán ......................................................... 40
5.11.3. Test kèm theo ............................................................................................... 42
5.12. Bài 12: Cập nhật thông tin trên cây 2 ................................................................. 42
5.12.1. Đề bài: Update tree2 .................................................................................... 42
5.12.2. Phân tích đề bài và đề xuất thuật toán ......................................................... 42
5.12.3. Test kèm theo ............................................................................................... 46
5.13. Bài 13: Dạo chơi trên cây ................................................................................... 46
5.13.1. Đề bài Walking ............................................................................................ 46
5.13.2. Phân tích đề bài và đề xuất thuật toán ......................................................... 46
5.13.3. Test kèm theo ............................................................................................... 48
5.14. Bài 14: Cây đổi gốc ............................................................................................ 48
5.14.1. Đề bài: ROOTLESS .................................................................................... 49
5.14.2. Phân tích đề bài và đề xuất thuật toán ......................................................... 49
5.14.3. Test kèm theo ............................................................................................... 51
5.15. Bài 15: Cây đổi gốc 2 ......................................................................................... 51
5.15.1. Đề bài: Rootless2 ......................................................................................... 51
5.15.2. Phân tích đề bài và đề xuất thuật toán ......................................................... 52
5.15.3. Test kèm theo ............................................................................................... 55
6. Một số bài tập tự luyện ................................................................................................ 55
7. Kết luận ........................................................................................................................ 56
8. Tài liệu tham khảo ....................................................................................................... 56

Trang 3
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Bảng chú thích một số tên, thuật ngữ viết tắt


Thuật ngữ, tên viết tắt Giải thích
𝑎𝑑𝑗 Kề (adjacent)
ancestor Tổ tiên
Binary lifting Nâng nhị phân, nhảy nhị phân
𝐵𝐼𝑇 Cây chỉ số nhị phân (Bianry index tree)
BFS Duyệt đồ thị theo chiều rộng
Cây con gốc u Là bộ phận của cây ban đầu trong đó các đỉnh được
gọi đến từ nó trong duyệt DFS(u)
𝑑𝑒𝑝𝑡ℎ độ sâu
DFS Duyệt đồ thị theo chiều sâu (Depth first search)
𝑑𝑖𝑠𝑡 Khoảng cách (distance)
DSU Tập hợp không giao nhau (Disjoint set union)
𝐿𝐶𝐴 Tổ tiên chung gần nhất (Lowest ancestor common)
𝑝𝑎𝑟 Cha (parents)
root Gốc của cây, là đỉnh được duyệt đầu tiên trên cây khi
gọi DFS.
Segment tree Cây phân đoạn (cây quản lí đoạn)
Sparse table Bảng thưa

Trang 4
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

1. Mở đầu
Dạng bài về “Tổ tiên chung gần nhất” cũng khá phổ biến, đây đều là các
dạng bài không dễ, đòi hỏi học sinh có tư duy khá, nhiều bài đòi hỏi học sinh sáng
tạo mới vận dụng được.
Học sinh cần có một số kiến thức để đảm bảo học được chuyên đề này là:
cơ bản về phương pháp Quy hoạch động trên cây; Đồ thị cơ bản, duyệt đồ thị; kĩ
thuật bảng thưa (Sparse table), cấu trúc dữ liệu (Segment tree, BIT, DSU).
Trong quá trình dạy đội tuyển lớp HSG lớp 11, đội tuyển HSG Quốc gia. Từ
các bài toán dạng này, cũng cho học sinh ôn lại các kiến thức liên quan khác để
giải bài toán LCA như: cấu trúc dữ liệu sparse table, segment tree,…; ôn lại bài
toán RMQ; kĩ thuật Heavy light decomposition; kĩ thuật duỗi cây thành mảng
(Euler tour); duyệt đồ thị; Quy hoạch động trên cây;…

2. Một số khái niệm, kiến thức cơ bản


- Cây DFS: Quá trình duyệt đồ thị theo chiều sâu (DFS)
bắt đầu từ đỉnh 𝑠 cho ta một cây DFS gốc 𝑠.
- Quan hệ cha-con trên cây DFS: Khi duyệt DFS, nếu
từ đỉnh 𝑢 ta gọi hàm tới thăm đỉnh 𝑣 thì đỉnh 𝑢 là đỉnh
cha đỉnh 𝑣. Ví dụ: Đỉnh 1 là cha của các đỉnh 2,3,4.
Đỉnh 12,13 là con của đỉnh 10.
- Quan hệ tổ tiên-con cháu: Được định nghĩa đệ quy
như sau:
+ Đỉnh 𝑢 là tổ tiên của chính nó.
+ Đỉnh cha của đỉnh 𝑢 là tổ tiên của đỉnh 𝑢.
+ Cha của tổ tiên của 𝑢 cũng là tổ tiên của 𝑢.
Ta thấy, đỉnh 𝑢 là tổ tiên của tất cả các đỉnh trong nhánh cây DFS gốc 𝑢.
Ví dụ:
Đỉnh 7 là tổ tiên của các đỉnh 7,10,11,12,13 và cả chính nó.
Đỉnh 6 là tổ tiên của đỉnh 6,8,9.
- Độ sâu: Là khoảng cách đến đỉnh gốc (được tính bằng số đỉnh, hoặc số cạnh,
không phải là trọng số của cạnh). Đỉnh gốc của cây DFS quy ước độ sâu là 1. Ví dụ:
Đỉnh 3 có độ sâu là 2; đỉnh 12,13 có độ sâu là 5.
- Tổ tiên chung: Đỉnh 𝑤 là tổ tiên của đỉnh 𝑢 và 𝑣 thì 𝑤 được gọi là tổ tiên chung
của 𝑢 và 𝑣. Ví dụ: Đỉnh 3 là tổ tiên chung của đỉnh 12 và đỉnh 13.
- Định nghĩa tổ tiên chung gần nhất (LCA) trên wiki: Trong lý thuyết đồ thị và
khoa học máy tính, LCA của 2 đỉnh 𝑢, 𝑣 trên cây hoặc đồ thị có hướng không chu
trình (DAG) gốc 𝑇 là đỉnh 𝑤 sâu nhất (hay đỉnh xa gốc nhất) mà nhận cả 𝑢, 𝑣 làm
con cháu, chúng ta coi một đỉnh cũng chính là tổ tiên của chính nó. Ví dụ: Đỉnh 3
là tổ tiên chung gần nhất của đỉnh 7 và 9. Đỉnh 10 là tổ tiên chung gần nhất của
12,13.
Trang 5
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

- Nhận xét thấy 𝐿𝐶𝐴(𝑢, 𝑣) là đỉnh đầu tiên gặp nhau của đường đi từ 𝑢 về gốc và
đường đi từ 𝑣 về gốc. Từ nhận xét này ta sẽ hình thành các cách giải bài toán LCA.

3. Dạng bài toán nào có thể cần đến LCA


Trong chuyên đề này, tôi tập trung vào các dạng bài sau:
- Các bài tập về tìm kiếm, cập nhật thông tin của các đỉnh nằm trên đường
đi đơn từ 𝑢 đến 𝑣. Ví dụ như tính khoảng cách giữa các cặp đỉnh trên cây (cây có
trọng số, không có trọng số).
- Dạng bài LCA kết hợp cấu trúc dữ liệu như: Segment tree, Binay index
tree, Disjoint set union,….
- Dạng bài LCA kết hợp với quy hoạch động trên cây, cây khung, cầu-khớp,…
- Dạng bài LCA có đổi gốc.
- Đánh dấu, cộng dồn trên cây có áp dụng LCA.
Theo nhận định của tác giả thì các dạng bài khó sẽ là dạng bài phải biết kết
hợp thêm các dạng bài khác về cây, kết hợp cấu trúc dữ liệu; các bài cần học sinh
có sự sáng tạo mới phát hiện ra cần áp dụng dạng bài toán LCA như thế nào,…

4. Các phương pháp giải bài toán LCA


Để trình bày một số phương pháp giải bài toán LCA, tác giả đưa ra ví dụ cây
có gốc là 1 như dữ liệu cho sau:
Dữ liệu vào Kết quả ra Giải thích
13 lca(2,4)=1
12 lca(12,13)=10
13 lca(2,5)=1
14 lca(8,9)=6
35 lca(12,11)=7
36 lca(6,7)=3
37
68
69
7 10
7 11
10 12
10 13
6
24 Cho đồ thị gồm 13 đỉnh, 12
12 13 cạnh, và 6 câu hỏi truy vấn tìm
25 LCA.
89
12 11
67

Trang 6
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

4.1. Duyệt tham lam


Nhận xét rằng để tìm tổ tiên chung gần nhất của 2 đỉnh 𝑢, 𝑣 thì từ 2 đỉnh
này, ta đi lên từng bước một về phía gốc cây. Đến vị trí gặp nhau đầu tiên thì đó
chính là tổ tiên chung gần nhất.
Phương pháp:
Bước 1: Di chuyển đỉnh có độ sâu lớn hơn đến khi 2 đỉnh có cùng độ sâu.
Bước 2: Nếu 2 đỉnh chưa gặp nhau thì ta cùng di chuyển chúng đến khi gặp
nhau thì lập tức dừng lại, đó chính là LCA của chúng.
Các làm này khá lâu nếu trong trường hợp cây DFS suy biến thành dạng
đường thẳng .
Đánh giá độ phức tạp của thuật toán:
- Độ phức tạp tiền xử lí (duyệt DFS): 𝑶(𝑵).
- Độ phức tạp của một truy vấn là: 𝑶(𝑵).
 Độ phức tạp thuật toán chung: 𝑶(𝑵 ∗ 𝑸).
Chương trình tham khảo:
#include <bits/stdc++.h>
using namespace std;
int n, q, par[100005], depth[100005];
vector<int> adj[100005];
///duyet DFS de xac dinh do xau va tim cha cua cac dinh
void dfs(int u, int p, int d) {
depth[u] = d;
par[u] = p;
for(int v : adj[u]) {
if(par[u] == v)
continue;
dfs(v, u, d + 1);
}
}
int lca(int u, int v) { ///Tim LCA theo kieu Brute force
if(depth[u] < depth[v])
swap(u, v);
while(depth[u] > depth[v])
u = par[u];
while(u != v) {
u = par[u];
v = par[v];
}
return u;
}

int main() {
cin >> n;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}

Trang 7
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

dfs(1, 0, 1);
cin>>q;
while(q--){
int u,v;
cin>>u>>v;
cout<<"lca("<<u<<","<<v<<")="<<lca(u,v)<<"\n";
}
}
4.2. Chia căn
Nhận xét rằng cách làm Brute Force trên khá lâu vì mỗi lần ta chỉ đi lên
được một đỉnh. Trong cách làm sau đây, ta tiền xử lí bằng cách chia cây có độ cao
depth[u ]  1
H thành các √𝐻 tầng. Đỉnh có độ sâu 𝑑𝑒𝑝𝑡ℎ[𝑢] thì được xếp vào tầng .
H
Có thể chọn 𝐻 = 𝑁. Khi đó nếu 𝑢, 𝑣 chưa cùng tầng thì ta nhảy theo tầng, nếu cùng
tầng thì ta nhảy theo cha của nó để đến khi gặp nhau.
Đánh giá độ phức tạp của thuật toán:
- Độ phức tạp tiền xử lí: 𝑶(𝑵 ∗ √𝑵 ).
- Độ phức tạp của một truy vấn là: 𝑶(√𝑵 ).
 Độ phức tạp thuật toán chung: 𝑶(𝑵 ∗ √𝑵 + 𝑸 ∗ √𝑵 ).
Ví dụ: H=5 và [√𝐻] = 2
Tầng 1

Tầng 2

Tầng 3

Chương trình tham khảo:


#include <bits/stdc++.h>
using namespace std;
int n, q, H, s, par[100005], depth[100005], T[100005];
vector<int> adj[100005];
///duyet DFS de xac dinh do xau va tim cha cua cac dinh
void dfs0(int u, int p, int d) {
depth[u] = d;
H = max(H, d);
par[u] = p;
for(int v : adj[u]) {
if(par[u] == v)

Trang 8
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

continue;
dfs0(v, u, d + 1);
}
}
void dfs(int u, int p, int d) { ///tim to tien o tang tren cua u
depth[u] = d;
par[u] = p;
if(depth[u] < s)
T[u] = 1;
else
if((depth[u] + 1) % s)
T[u] = T[par[u]];
else
T[u] = par[u];
for(int v : adj[u]) {
if(par[u] == v)
continue;
dfs(v, u, d + 1);
}
}

int lca(int u, int v) { ///Tim LCA chia can


while(T[u] != T[v])
if(depth[u] > depth[v])
u = T[u];
else
v = T[v];
while(u != v) {
if(depth[u] > depth[v])
u = par[u];
else
v = par[v];
}
return u;
}

int main() {
//freopen("LCA_sqrt.inp", "r", stdin);
cin >> n;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs0(1, 0, 1);
s = sqrt(H);
dfs(1, 0, 1);
cin >> q;
while(q--) {
int u, v;
cin >> u >> v;
cout << "lca(" << u << "," << v << ")=" << lca(u, v) << "\n";
}
}

Trang 9
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

4.3. Kĩ thuật bảng thưa (Sparse table)


Đây là kĩ thuật hay dùng nhất trong giải các bài toán về LCA.
Nhận xét rằng mọi số nguyên dương đều có thể biểu dưới dạng nhị phân.
Nhận xét này khá quan trọng, từ đó ta có thể cải tiến cách làm của các thuật toán
đã giới thiệu ở trên bằng cách nhảy lên các lũy thừa của 2 (binary lifting), từ đó
việc tìm 𝐿𝐶𝐴(𝑢, 𝑣) chỉ có độ phức tạp 𝑂(log(𝑁)).
Giả sử 𝑑𝑒𝑝𝑡ℎ(𝑢) − 𝑑𝑒𝑝𝑡ℎ(𝑣) = 5 = 5(10) = 101(2) = 1. 22 + 0. 21 + 1. 20
Khi đó để 𝑢 nhảy lên tổ tiên 𝑣 trên nó 5 bậc thì ta nhảy đến cha trước đó 22
bậc, sau đó nhảy tiếp cha 20 bậc.
Ta sẽ sử dụng bảng thưa (Sparse table), cha thứ 2 𝑗 của đỉnh 𝑖 là 𝑇[𝑖][𝑗].
Định nghĩa Sparse table theo công thức truy hồi như sau:
𝑇[𝑢][0] = 𝑝𝑎𝑟[𝑢] Cha cấp 20
{ 𝑇[𝑢][𝑖] = 𝑇[𝑇[𝑢][𝑖 − 1]][𝑖 − 1] Cha cấp 2𝑖
(Vì 2𝑖 = 2𝑖−1 + 2𝑖−1 )
Đánh giá độ phức tạp của thuật toán:
- Độ phức tạp tiền xử lí (xây dựng bảng thưa) : 𝑶(𝑵 ∗ 𝒍𝒐𝒈(𝑵))
- Độ phức tạp của một truy vấn là: 𝑶(𝒍𝒐𝒈(𝑵)).
 Độ phức tạp thuật toán chung: 𝑶(𝑵 ∗ 𝒍𝒐𝒈(𝑵) + 𝑸 ∗ 𝒍𝒐𝒈(𝑵)).
Chương trình tham khảo:
#include <bits/stdc++.h>
using namespace std;
int n, q, depth[100005], T[100005][17];
vector<int> adj[100005];
void dfs(int u, int p) { ///tim to tien o tang tren cua u
depth[u] = depth[p] + 1;
T[u][0] = p;
for(int i = 1; i < 17; i++)
T[u][i] = T[T[u][i - 1]][i - 1];
for(int v : adj[u]) {
if(v == p)
continue;
dfs(v, u);
}
}
int lca(int u, int v) { ///Tim LCA Sparse Table
if(depth[u] < depth[v])
swap(u, v);
for(int i = 16; i >= 0; i--) ///nhay den cung do sau
if(depth[T[u][i]] >= depth[v])
u = T[u][i];
if(u == v)
return u;
for(int i = 16; i >= 0; i--)///nhay den LCA
if(T[u][i] != T[v][i]) {
u = T[u][i];
v = T[v][i];

Trang 10
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

}
return T[u][0];
}
int main() {
cin >> n;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1, 0);
cin >> q;
while(q--) {
int u, v;
cin >> u >> v;
cout << "lca(" << u << "," << v << ")=" << lca(u, v) << "\n";
}
}
Để ý thấy rằng dù là Brute Force, chia căn, hay sparse table đều dùng
chung một cách làm đó là: đưa 2 đỉnh về cùng tầng đã, sau đó lại đưa tiếp
chúng về đến LCA.
4.4. Dùng Euler tour
Quan sát đồ thị bên. Thứ tự các đỉnh
được gọi đến theo đường nét đứt.
Với đồ thị trên ta có dãy các đỉnh được
gọi đến như sau:

Độ sâu tương ứng là:


1 2 4 2 3 4 3 4 5 2 1 2 3 2 3 2 1
Nhận xét thấy rằng 𝐿𝐶𝐴(𝑢, 𝑣) là đỉnh được gọi đến trong đoạn duyệt đến
𝑢, 𝑣 mà có độ sâu nhỏ nhất.
Ví dụ: 𝐿𝐶𝐴(4,9) = 2 (đỉnh có độ sâu nhỏ nhất).
Kĩ thuật này có thể gọi nôm na là duỗi cây thành mảng. Đây là cách làm cũng
rất hay, có nhiều ứng dụng. Kĩ thuật này có nhiều biến thể, nhưng chung quy nó
đều kết hợp với DFS, trong khi DFS thì xác định một thứ tự duyệt đến, duyệt xong
các đỉnh,…
Việc tìm đỉnh có độ sâu nhỏ nhất sau khi duỗi cây thành mảng là dạng bài
toán Range Minimum Query (RMQ). Để giải quyết vấn đề này, chúng ta có thể sử
dụng cấu trúc dữ liệu Sparse table hoặc Segment tree để giải quyết. Khi đó độ
phức tạp thuật toán để trả lời 𝑸 truy vấn là: 𝑶(𝑸 ∗ 𝒍𝒐𝒈(𝑵)).
Vì đây là dạng quy bài toán LCA về RMQ, nên trong chuyên đề này không
thể hiện lại chương trình mẫu. Bạn đọc có thể tự tham khảo tại:
[Link]
Common-Ancestor
Trang 11
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

4.5. Xử lí kiểu Off-line (Tarjan's off-line LCA)


Đây là thuật toán được Tarjan đề xuất năm 1979. Cách làm rất hay bằng
cách kết hợp với cấu trúc dữ liệu DSU (Disjoint set union), độ phức tạp có thể coi
gần như là hàm tuyến tính với kích thước cây (N) và số câu truy vấn (Q), thực tế
nó cỡ hàm ngược của hàm Ackermann hoặc đánh giá tương đương 𝑙𝑜𝑔(𝑙𝑜𝑔(𝑁)).
Dựa theo có một số nhận xét sau:
- Khi duyệt một nhánh DFS gốc 𝑤 thì các đỉnh con trong nhánh sẽ luôn
thuộc thành phần DSU mà có tổ tiên là 𝑤 khi duyệt xong nhánh DFS gốc 𝑤.
- Khi duyệt DFS, thì 𝑤 = 𝐿𝐶𝐴(𝑢, 𝑣) luôn được duyệt trước 𝑢, 𝑣. Giả sử khi
duyệt xong một đỉnh 𝑢 thì luôn kiểm tra xem đỉnh 𝑣 đã duyệt chưa? Nếu duyệt
rồi và 𝑣 sẽ thuộc thành phần DSU có phần tử đại diện luôn là 𝐿𝐶𝐴(𝑢, 𝑣).
- Khi duyệt xong 𝑤 thì ta sẽ trả lời xong tất cả các truy vấn 𝐿𝐶𝐴(𝑢, 𝑣) với
mọi 𝑢, 𝑣 thuộc nhánh DFS gốc 𝑤. Do đó các truy vấn được trả lời theo thứ tự duyệt
DFS chứ không phải thứ tự truy vấn cho ban đầu. Do vậy thuật toán này được ghi
nhận là thuật toán kiểu Offline.
Giả sử 𝑢 lại được duyệt xong trước 𝑣, vậy thì trong nhánh của cây DFS gốc
𝑤 thì ta đã duyệt qua 𝑢 và khi vừa duyệt xong 𝑣 ta làm cách nào để in ra 𝑤?. Để
tìm nhanh ra 𝑤, ta luôn lưu lại các nhánh đã được duyệt trong nhánh DFS gốc 𝑤
bằng cấu trúc DSU, trong đó tổ tiên của nhánh đó chính là 𝑤. Khi duyệt xong 𝑣 thì
ta cần in ra tổ tiên của tập mà chứa 𝑢.
cout<< ancestor[find_set(u)];
Chương trình tham khảo:
#include<bits/stdc++.h>
using namespace std;
vector<int> adj[100005], queries[100005];
int n, q, par[100005], rnk[100005], ancestor [100005];
bool visited[100005];
int find_set(int u) { ///cau truc DSU
while(par[u] != u)
u = par[u];
return u;
}
void union_set(int x, int y) { ///cau truc DSU
int xroot = find_set(x);
int yroot = find_set(y);
if(xroot == yroot)
return;
if(rnk[xroot] < rnk[yroot])
par[xroot] = yroot;
else
if(rnk[xroot] > rnk[yroot])
par[yroot] = xroot;
else {
par[xroot] = yroot;
rnk[yroot]++;
}
}
void dfs(int w) {

Trang 12
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

visited[w] = true;
ancestor[w] = w;
for(int u : adj[w]) {
if(!visited[u]) {
dfs(u);
union_set(w, u); ///hợp các đỉnh trong nhánh w
ancestor[find_set(u)] = w;
}
}
///tra loi cac truy van
for(int u : queries[w]) {
if(visited[u])
cout << "lca(" << w << "," << u
<< ")= " << ancestor[find_set(u)] << "\n";
}
}
int main() {
//freopen("LCA_Tarjan.inp", "r", stdin);
cin >> n;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for(int i = 1; i <= n; i++)
par[i] = i;
cin >> q;
while(q--) {
int u, v;
cin >> u >> v;
queries[u].push_back(v);
queries[v].push_back(u);
}
dfs(1);
}
Để trả lời các truy vấn theo thứ tự dữ liệu cho ban đầu, chúng ta cần bổ
sung thông tin vào mảng queries có thêm thông tin về thứ tự câu hỏi và in ra kết
quả theo thứ tự đề bài yêu cầu.
Tóm lại, chuyên đề này giới thiệu 3 cách làm cơ bản một là greedy, quy về
bài toán RMQ bằng Euler tour, xử lí Off-line theo kiểu của Tarjan. Ngoài ra kiểu
Greedy có rất nhiều biến thể, chẳng hạn có thể kết hợp với kĩ thuật Heavy light
decomposition,… ta không đi sâu ở đây.

5. Một số bài tập ví dụ


Trong các kĩ thuật giải bài toán LCA, ta tập trung chính vào kĩ thuật áp dụng
bảng thưa (Sparse table), vì đây là kĩ thuật khá tốt, đơn giản trong cài đặt, đảm
bảo tính hiệu quả. Các bài trong chuyên đề này tôi đều dùng Sparse table. Có một
số bài tôi chọn cài đặt duyệt cây bằng BFS hoặc DFS. Ngay cả trong một số bài cần
cập nhật thông tin ngược và xuôi trên cây (dạng quy hoạch động) chúng ta vẫn có
thể không dùng DFS (ví dụ bài số 10- Tom và Jerry, để làm như vậy đương nhiên
bạn cần phải lưu trữ thông tin đỉnh duyệt trước, sau của đỉnh bất kì 𝑢).

Trang 13
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.1. Bài 1: Bài tập cơ bản


5.1.1. Đề bài: LCA
Cho một cây 𝑁 đỉnh có gốc là 1. Có 𝑄 truy vấn tìm LCA của hai đỉnh 𝑥 và 𝑦.
Dữ liệu cho đảm bảo 1 ≤ 𝑁, 𝑄 ≤ 105 và 1 ≤ 𝑥, 𝑦 ≤ 𝑁.
Dữ liệu vào: Đọc vào từ tệp [Link]
Dòng đầu tiên là số 𝑁. Sau đó 𝑁 − 1 cạnh của cây.
Số 𝑄. Sau đó 𝑄 cặp số (𝑥, 𝑦).
Kết quả ra: Ghi kết quả ra tệp [Link]
Với mỗi truy vấn, in ra kết quả cần tìm trên từng dòng.
Ví dụ:
[Link] [Link] Hình minh họa
7 3
61 7
64 1
47 1
34 6
12 1
25
6
33
77
13
57
76
24
5.1.2. Phân tích đề bài và đề xuất thuật toán
Đây là bài cơ bản, có thể giải bằng rất nhiều cách khác nhau đã nêu trên. Để
đảm bảo AC thì cần chọn thuật toán có độ phức tạp 𝑂(𝑄𝑙𝑜𝑔𝑁 + 𝑁𝑙𝑜𝑔𝑁).
Do Sparse table được tạo ngay trong quá trình duyệt đồ thị, nên chúng ta
có thể dùng DFS hoặc BFS đều được, tuy nhiên ta hay dùng DFS đệ quy để cho
việc code đơn giản và ngắn gọn hơn.
Nhiều trường hợp số lần gọi đệ quy quá nhiều dẫn tới tràn bộ nhớ, chúng
ta có thể cài đặt bằng DFS không đệ quy, hoặc BFS như sau:
void bfs() {
queue<int> Q;
[Link](1);
depth[1]=1;
while(![Link]()) {
int u=[Link]();
[Link]();
if(!visited[u]) {
visited[u]=1;
for(int v:adj[u]) {

Trang 14
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

if(v==up[u][0])
continue;
up[v][0]=u;
depth[v]=depth[u]+1;
for(int i=1; i<=16; i++) ///sparse table
up[v][i]=up[up[v][i-1]][i-1];
[Link](v);
}
}
}
}
5.1.3. Test kèm theo
[Link]
p?usp=sharing
5.2. Bài 2: Tổ chức thi chạy Marathon
5.2.1. Đề bài: Marathon
Thầy giáo dạy giáo dục thể chất tại trường chuyên XYZ đang cần tổ chức
cuộc thi chạy cho học sinh, biết địa bàn thành phố là đồ thị vô hướng dạng cây
gồm N đỉnh và N-1 cạnh. Do cần giám sát, đảm bảo an toàn, giáo sư đã nhờ một
chuyên gia khoa học máy tính thiết kế một camera để giám sát trên đoạn đường
chạy. Chuyên gia đã đưa cho thầy giáo Q phương án. Mỗi phương án là bộ 3 số
𝑢, 𝑣, 𝑤 trong đó 𝑢, 𝑣 là điểm đầu, cuối của đoạn đường chạy, 𝑤 là vị trí đặt camera.
Bạn hãy giúp xem chuyên gia đã thực hiện đúng yêu cầu của thầy giáo đặt ra chưa.
Dữ liệu vào: Từ tệp [Link]
- Dòng đầu số đỉnh của đồ thị N, và số phương án chọn đường chạy Q.
- Các dòng tiếp theo thể hiện cạnh của đồ thị.
- Các dòng sau đó là Q phương án
Kết quả ra: Ghi ra tệp [Link]
Gồm Q dòng, nếu phương án đảm bảo việc lắp camera trên đường chạy thì
in ra 1, ngược lại in ra 0.
Ràng buộc: 1 ≤ 𝑁, 𝑄 ≤ 105 .
Ví dụ:
[Link] [Link] Giải thích
53 1
12 1
13 0
35
45
231
545
234

Trang 15
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.2.2. Phân tích đề bài và đề xuất thuật toán


Đây là bài dễ về LCA. Bài toán cần kiểm tra xem 𝑤 có nằm trên đường đi từ
𝑢 đến 𝑣 không. Có 3 cách để giải quyết vấn đề trên:
Cách 1: Không áp dụng LCA.
Mỗi truy vấn (𝑢, 𝑣, 𝑤) ta duyệt DFS từ gốc u để xác định quan hệ cha con
khi duyệt DFS. Sau đó đi ngược từ 𝑣 tới gốc 𝑢 nếu gặp 𝑤 trên đó thì in ra 1, còn lại
in ra 0. Độ phức tạp: 𝑂(𝑁. 𝑄)
Cách 2: Áp dụng LCA dùng khoảng cách
Nếu 𝑤 nằm trên đường đi 𝑢 → 𝑣 thì:
𝑑𝑖𝑠𝑡(𝑢, 𝑣) = 𝑑𝑖𝑠𝑡(𝑣, 𝑤) + 𝑑𝑖𝑠𝑡(𝑢, 𝑤)
Cách 3: Áp dụng LCA không dùng khoảng cách
Nếu 𝑤 nằm trên đường đi 𝑢 → 𝑣 thì:
TH1: Nếu 𝑤 = 𝐿𝐶𝐴(𝑢, 𝑤) thì 𝐿𝐶𝐴(𝑢, 𝑣) = 𝐿𝐶𝐴(𝑤, 𝑣)
TH2: Nếu 𝑤 = 𝐿𝐶𝐴(𝑣, 𝑤) thì 𝐿𝐶𝐴(𝑢, 𝑣) = 𝐿𝐶𝐴(𝑤, 𝑢)
Độ phức tạp của cách có áp dụng LCA: 𝑂(𝑁 ∗ log(𝑁)).
Code tham khảo dựa theo cách dùng dùng khoảng cách, duyệt BFS.
#include<bits/stdc++.h>
using namespace std;
int n, q, depth[100005], up[100005][17],visited[100005];
vector<int> adj[100005];
void bfs() {
queue<int> Q;
[Link](1);
depth[1]=1;
while(![Link]()) {
int u=[Link]();
[Link]();
if(!visited[u]) {
visited[u]=1;
for(int v:adj[u]) {
if(v==up[u][0])
continue;
up[v][0]=u;
depth[v]=depth[u]+1;
for(int i=1; i<=16; i++)
up[v][i]=up[up[v][i-1]][i-1];
[Link](v);
}
}
}
}
int lca(int u, int v) { ///Tim LCA Sparse upable
if(depth[u] < depth[v])
swap(u, v);
for(int i = 16; i >= 0; i--) ///nhay den cung do sau
if(depth[up[u][i]] >= depth[v])
u = up[u][i];
if(u == v)
Trang 16
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

return u;
for(int i = 16; i >= 0; i--)///nhay den LCA
if(up[u][i] != up[v][i]) {
u = up[u][i];
v = up[v][i];
}
return up[u][0];
}
int dist(int u, int v) {
return (depth[u]+depth[v]-2*depth[lca(u,v)]);
}
int main() {
ios_base::sync_with_stdio(0);
// freopen("[Link]", "r", stdin);
cin >> n>>q;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
bfs();
while(q--) {
int u, v,w;
cin >> u >> v>>w;
if(dist(u,v)==dist(u,w)+dist(v,w))
cout<<"1\n";
else
cout<<"0\n";
}
}
5.2.3. Test kèm theo
[Link]
p?usp=sharing
5.3. Bài 3: Du lịch thành phố (NAIPC 2016)
5.3.1. Đề bài: Tourist
Tại thành phố cây, có N điểm du lịch hấp dẫn được đánh số từ 1 đến N.
Thành phố có N-1 con đường 2 chiều để nối các điểm du lịch. Thị trưởng thành
phố phát hiện ra là việc tổ chức các tour đi từ địa điểm 𝑢 đến các địa điểm được
đánh số là bội của nó sẽ rất thú vị, các tour như vậy thì du khách sẽ được thăm tất
cả các địa điểm trên đường đi đơn giữa 2 địa điểm này. Hỏi với tất cả cách tổ chức
tour như vậy thì tổng số địa điểm được thăm là bao nhiêu?
Dữ liệu vào: Từ tệp [Link]
Dòng đầu tiên là số địa điểm du lịch N của thành phố
N-1 dòng tiếp theo thể hiện đường nối giữa các thành phố
Kết quả ra: Ghi ra tệp [Link]
Ghi tổng số địa điểm du lịch được thăm với tất cả các tour được xây dựng.
Ràng buộc: 1 ≤ 𝑁 ≤ 105 .

Trang 17
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Ví dụ:
[Link] [Link] Giải thích
10 55
34
37
14
46
1 10 Chúng ta có tất cả các con đường và số địa
8 10 điểm có thể thăm được như sau: 1→2=4 ;
28 1→3=3; 1→4=2; 1→5=2; 1→6=3; 1→7=4;
15 1→8=3; 1→9=3; 1→10=2; 2→4=5; 2→6=6;
49 2→8=2; 2→10=3; 3→6=3; 3→9=3 ; 4→8=4 ;
5→10=3.
Do đó tổng số địa điểm du lịch được thăm sẽ
là: 55.
5.3.2. Phân tích đề bài và đề xuất thuật toán
Bài này liên quan đến việc tính tổng khoảng cách giữa các cặp (𝑢, 𝑣) mà 𝑣
là bội của 𝑢.
Duyệt tất cả các cặp tạo được tour du lịch với điểm cuối là bội của điểm
đầu. Tính tổng (khoảng cách +1) vào kết quả.
n
ans   dist (u, v) với mọi 𝑣 là bội của 𝑢.
u 1

Chương trình tham khảo:


#include<bits/stdc++.h>
using namespace std;
int n, q, depth[100005], T[100005][17];
vector<int> adj[100005];
void dfs(int u, int p)
{
depth[u] = depth[p] + 1;
T[u][0] = p;
for(int i = 1; i < 17; i++) ///xay dung bang thua
T[u][i] = T[T[u][i - 1]][i - 1];
for(int v : adj[u])
{
if(v == p)
continue;
dfs(v, u);
}
}
int lca(int u, int v) ///Tim LCA Sparse Table
{
if(depth[u] < depth[v])
swap(u, v);
for(int i = 16; i >= 0; i--) ///nhay den cung do sau
if(depth[T[u][i]] >= depth[v])
u = T[u][i];
if(u == v)

Trang 18
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

return u;
for(int i = 16; i >= 0; i--)///nhay den LCA
if(T[u][i] != T[v][i])
{
u = T[u][i];
v = T[v][i];
}
return T[u][0];
}
int dist(int u, int v)
{
return (depth[u]+depth[v]-2*depth[lca(u,v)]);
}
int main()
{
freopen("[Link]", "r", stdin);
cin >> n;
for(int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1, 0);
long long ans = 0;
for(int u = 1; u <= n; ++u)
{
for(int i = 2; u * i <= n; ++i)
{
int v=u*i;
int c = lca(u, v);
ans += dist(u,v)+1;
}
}
cout << ans;
}
5.3.3. Test kèm theo
[Link]
p?usp=sharing
Qua 3 bài ví dụ đầu giúp học sinh làm quen với các ứng dụng cơ bản nhất
của bài toán LCA. Tiếp sau đây, tôi đưa ra dạng bài có kết hợp với cấu trúc dữ liệu.
5.4. Bài 4: VOTREE (VNOI online 2015)
5.4.1. Đề bài
Cho cây gồm 𝑁 đỉnh (𝑁 ≤ 70000), có gốc là đỉnh 1. Bạn cần trả lời 𝑄 truy
vấn, mỗi truy vấn gồm 2 số 𝑢, 𝑣. Bạn cần tìm đỉnh xa gốc nhất, mà là tổ tiên của
tất cả các đỉnh 𝑢, 𝑢 + 1, … , 𝑣.
Dữ liệu vào: Từ tệp [Link]
Dòng đầu ghi 2 số nguyên dương 𝑁 và 𝑄 (1 ≤ 𝑁, 𝑄 ≤ 70000).

Trang 19
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

𝑁 − 1 dòng tiếp theo, mỗi dòng chứa 2 số nguyên dương 𝑢 và 𝑣, thể hiện
có 1 cạnh nối giữa 2 đỉnh 𝑢 và 𝑣. (𝑢 ≠ 𝑣; 1 ≤ 𝑢, 𝑣 ≤ 𝑁).
𝑄 dòng tiếp theo, mỗi dòng gồm 2 số nguyên dương 𝑢 và 𝑣 (1 ≤ 𝑢 ≤ 𝑣 ≤
𝑁), thể hiện 1 truy vấn.
Kết quả ra: Ghi ra tệp [Link]
Với mỗi truy vấn, in ra 1 dòng duy nhất là đáp số của truy vấn.
Ví dụ:
[Link] [Link] Hình vẽ
53 2
12 1
23 3
34
35
25
13
45
5.4.2. Phân tích đề bài và đề xuất thuật toán
Việc tìm LCA của 2 đỉnh là bài cơ bản, tuy nhiên ở đây lại cần tìm LCA của
các đỉnh từ 𝑢, 𝑢 + 1, … 𝑣. Nên nếu mỗi truy vấn ta tìm LCA đoạn [𝑢, 𝑣] như sau:
𝐿𝐶𝐴(𝑢, 𝐿𝐶𝐴(𝑢 + 1, 𝐿𝐶𝐴(𝑢 + 2, … )) thì sẽ không đảm bảo về mặt thời gian.
Nhận thấy xuất hiện truy vấn của một đoạn nên ta nghĩ đến sử dụng cấu
trúc dữ liệu Segment tree để xử lí. Segment tree sẽ lưu thông tin về LCA của đoạn.
Thời gian xây dựng trong 𝑂(𝑁. log 2 (𝑁)). Mỗi truy vấn xử lí trong thời gian
𝑂(log2 (𝑁)).
Mỗi đỉnh id có 2 đỉnh con là 2*id và 2*id+1. Thông tin cập nhật như sau:
𝑆𝑇[𝑖𝑑] = 𝐿𝐶𝐴(𝑆𝑇[2 ∗ 𝑖𝑑], 𝑆𝑇[2 ∗ 𝑖𝑑 + 1])
Chương trình tham khảo:
#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int n,q,depth[maxn],up[maxn][20],ST[4*maxn];
vector<int> adj[maxn];
void dfs(int u, int p) {
depth[u]=depth[p]+1;
up[u][0]=p;
for(int i=1; i<=17; i++)
up[u][i]=up[up[u][i-1]][i-1];
for(int v:adj[u]) {
if(v==p)
continue;
dfs(v,u);
}
}
int lca(int u, int v) {
if(u<1)

Trang 20
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

return v;
if(v<1)
return u;
if(depth[u]<depth[v])
swap(u,v);
for(int i=16; i>=0; i--)
if(depth[up[u][i]]>=depth[v])
u=up[u][i];
if(u==v)
return u;
for(int i=16; i>=0; i--)
if(up[u][i]!=up[v][i])
u=up[u][i],v=up[v][i];
return up[u][0];
}
void update(int id, int L, int R, int vt, int val) {
if(vt<L || R<vt)
return;
if(L==R) {
ST[id]=val;
return;
}
int mid=(L+R)/2;
update(2*id,L,mid,vt,val);
update(2*id+1,mid+1,R,vt,val);
ST[id]=lca(ST[2*id],ST[2*id+1]);
}
int get(int id, int L, int R, int u, int v) {
if(v<L || R<u)
return 0;
if(u<=L && R<=v)
return ST[id];
int mid =(L+R)/2;
int x=get(2*id,L,mid,u,v);
int y=get(2*id+1,mid+1,R,u,v);
return lca(x,y);
}

int main() {
ios_base::sync_with_stdio(0);
cin>>n>>q;
for(int i=1; i<n; i++) {
int u,v;
cin>>u>>v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1,0);
for(int i=1; i<=n; i++)
update(1,1,n,i,i);
for(int i=1; i<=q; i++) {
int u,v;
cin>>u>>v;
if(u>v)
swap(u,v);
cout<<get(1,1,n,u,v)<<"\n";
}
}

Trang 21
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.4.3. Test kèm theo


[Link]
p?usp=sharing
5.5. Bài 5: Tăng lương (Chọn đội tuyển IOI CROATIAN 2010)
5.5.1. Đề bài POVISICE
Mirko là ông chủ kiêu hãnh của một công ty phần mềm lớn. Ban đầu công
ty chỉ có một mình Mirko. Công việc làm ăn phát đạt và công ty thuê n công nhân,
lần lượt từng người, từng người một. Mirko được đánh số là 0. Các công nhân
khác – đánh số từ 1 đến n theo trình tự thuê.
Mỗi người mới vào có một mức lương khởi điểm và chịu sự chỉ đạo của một
ai đó đã có mặt trong công ty. Nếu lương công nhân cao hơn lương thủ trưởng
trực tiếp của mình thì lương của người thủ trưởng đó được nâng lên bằng lương
người dưới quyền mình. Quá trình điều chỉnh này được tiếp diễn cho đến khi đảm
bảo được trong toàn công ty lương thủ trưởng không thấp hơn lương công nhân
dưới quyền.
Yêu cầu: Với mỗi công nhân được tuyển chọn vào công ty hãy xác định số
người phải điều chỉnh lương cho phù hợp với người mới được tuyển chọn.
Dữ liệu vào: Từ tệp [Link]:
 Dòng đầu tiên chứa số nguyên 𝒏 (1 ≤ 𝒏 ≤ 3.105 ),
 Dòng thứ 2 chứa một số nguyên – lương khởi điểm của Mirko,
 Dòng thứ i trong n dòng sau chứa 2 số nguyên S và B – lương khởi
điểm và thủ trưởng của người công nhân thứ i.
Lương khởi điểm nằm trong phạm vi từ 1 đến 109.
Kết quả ra: Ghi ra tệp [Link]
n số số nguyên, mỗi số trên một dòng, là kết quả tính được đối với mỗi người.
Ràng buộc:
 30% số test tương ứng 30% số điểm có 𝑛 ≤ 5000
 30% số test tương ứng 30% số điểm có 5000 < 𝑛 ≤ 50000
 40% số test tương ứng 40% số điểm có 50000 < 𝑛 ≤ 300000
Ví dụ:
[Link] [Link] Hình vẽ
7 0
5000 1
4500 0 0
6000 0 2
4000 1 4
5500 3 1
7000 4 0
6300 2
6300 2

Trang 22
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.5.2. Phân tích đề bài và đề xuất thuật toán


- Dựa theo dữ liệu đầu vào, ta hoàn toàn có thể xây dựng trước một cây.
Mỗi nhân viên là một đỉnh, quan hệ cha con được thể hiện qua thứ tự khi duyệt
cây bằng DFS từ gốc 0.
- Ban đầu lương của tất cả các nhân viên coi như bằng 0 (Tất cả các đỉnh
được gán giá trị 0).
- Sau đó là các thao tác thêm nhân viên mới vào ta coi đó là thao tác tăng
lương của nhân viên. Hỏi cần điều chỉnh mức lương của bao nhiêu quản lý cấp
trên của nhân viên đó.
Để trả lời N truy vấn ta có 2 cách như sau:
Cách 1: Duyệt trâu
Xử lí các khá đơn giản như sau: Với mỗi truy vấn, ta đi ngược về gốc, nếu
quản lý cấp trên nào của nó có mức lương thấp hơn thì ta cập nhật lại mức lương
và tăng kết quả, đến khi quản lý cấp trên đã có mức lương không thấp hơn nhân
viên thì dừng lại.
Độ phức tạp thuật toán: 𝑂(𝑁 2 ).
Cách 2: Sử dụng LCA kết hợp cấu trúc dữ liệu Segment tree.
Để phát hiện ra cần dùng LCA ở đâu trong bài này là không dễ dàng.
Quan sát thấy rằng, các quản lý cấp trên nếu bị thay đổi sẽ là đoạn liên tục
các đỉnh tính từ vị trí nhân viên mới tăng lương ngược về gốc. Vấn đề là làm sao
để xác định được đoạn cần điều chỉnh và tránh việc phải cập nhật lại đoạn phải
điều chỉnh?
Thông thường, trong quá trình dạy HSG tôi
thường định hướng cách giải quyết vấn đề như sau: khi
gặp bài toán phức tạp, ta nên tìm cách chia nó ra thành
các bài toán nhỏ hơn, hoặc tìm ra điểm đặc biệt của bài
toán sau đó phán đoán, tìm ra điểm mấu chốt để giải bài
Với bài này, tình huống: nếu việc tăng lương diễn
ra theo đúng thứ tự duyệt DFS thì sao? Khi đó tìm vị trí
cần điểu chỉnh như thế nào? Câu trả lời là ta chỉ quan
tâm vị trí gần nhất 𝒖 mà nó có mức tăng lớn hơn hoặc
bằng vị trí 𝒗 đang xét. Toàn bộ các vị trí từ 𝒖 →
𝑳𝑪𝑨(𝒖, 𝒗) → 𝒓𝒐𝒐𝒕 đều đã được tăng khi thêm 𝒖. Các vị
trí cần tăng khi thêm 𝒗 chỉ là từ 𝒗 → 𝑳𝑪𝑨(𝒖, 𝒗).
Nếu 𝒖 = 𝑳𝑪𝑨(𝒖, 𝒗) thì hiển nhiên vẫn đúng.
Từ đó thấy rằng duyệt DFS không nhất thiết đã
theo đúng thứ tự từ điển (là cách mà tăng lương diễn
ra). Có thể đỉnh 𝒖 ở trước hoặc sau 𝒗 theo thứ tự từ
điển. Gọi 𝒖𝑳 , 𝒖𝑹 là đỉnh bên trái, phải gần 𝒗 nhất mà giá
trị không nhỏ hơn tại 𝒗. Khi đó hiển nhiên số đỉnh bị tác động bởi quá trình tăng
lương khi xét 𝒗 là:
Trang 23
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

𝒎𝒊𝒏{𝒅𝒊𝒔𝒕(𝑳𝑪𝑨(𝒖𝑳 , 𝒗)), 𝒅𝒊𝒔𝒕(𝑳𝑪𝑨(𝒖𝑹 , 𝒗)}


Để tìm ra 𝒖𝑳 , 𝒖𝑹 ta có thể nghĩ đến sử dụng Segment tree lưu giá trị max
của đoạn khi cập nhật tăng lương.
Chương trình tham khảo:
#include <bits/stdc++.h>
#define N 333333
#define PB push_back
using namespace std;
int n, sal[N], in[N], h[N], p[N][20], dem;
vector<int> adj[N];
struct IT {
int tree[N << 2], node[N << 2];
void update(int l, int r, int id, int x, int val, int u) {
if(l > x || r < x)
return;
if(l == r) {
tree[id] = max(tree[id], val);
node[id] = u;
return;
}
int mid = (l + r) / 2;
update(l, mid, id * 2, x, val, u);
update(mid + 1, r, id * 2 + 1, x, val, u);
tree[id] = max(tree[id * 2], tree[id * 2 + 1]);
}
int get_max(int l, int r, int id, int x, int y) {
if(l > y || r < x)
return 0;
if(l >= x && r <= y)
return tree[id];
int mid = (l + r) / 2;
int a = get_max(l, mid, id * 2, x, y);
int b = get_max(mid + 1, r, id * 2 + 1, x, y);
return max(a, b);
}
int get_left(int l, int r, int id, int x, int y, int u) {
if(tree[id] < sal[u])
return -1;
if(l == r && tree[id] >= sal[u])
return node[id];
int mid = (l + r) / 2;
if(y <= mid)
return get_left(l, mid, id * 2, x, y, u);
else {
int res = get_left(mid + 1, r, id*2+1, mid+1, y, u);
if(res != -1)
return res;
return get_left(l, mid, id * 2, x, mid, u);
}
}
int get_right(int l, int r, int id, int x, int y, int u) {
if(tree[id] < sal[u])
return -1;
if(l == r && tree[id] >= sal[u])

Trang 24
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

return node[id];
int mid = (l + r) / 2;
if(x > mid)
return get_right(mid + 1, r, id * 2 + 1, x, y, u);
else {
int res = get_right(l, mid, id * 2, x, mid, u);
if(res != -1)
return res;
return get_right(mid + 1, r, id * 2 + 1, mid + 1, y, u);
}
}
} t;
void nhap() {
scanf("%d", &n);
n++;
scanf("%d", &sal[1]);
for(int u = 2; u <= n; u++) {
int v;
scanf("%d %d", &sal[u], &v);
v++;
adj[v].PB(u);
}
}
void DFS(int u) {
in[u] = ++dem;
for(int i = 0; i < adj[u].size(); i++) {
int v = adj[u][i];
h[v] = h[u] + 1;
p[v][0] = u;
for(int j = 1; j < 20; j++)
p[v][j] = p[p[v][j - 1]][j - 1];
DFS(v);
}
}
void setup() {
nhap();
DFS(1);
}
int lca(int u, int v) {
if(h[u] < h[v])
swap(u, v);
int diff = h[u] - h[v];
for(int i = 19; i >= 0; i--)
if(diff & (1 << i))
u = p[u][i];
if(u == v)
return u;
for(int i = 19; i >= 0; i--)
if(p[u][i] != p[v][i]) {
u = p[u][i];
v = p[v][i];
}
return p[u][0];
}
void solve() {
[Link](1, n, 1, 1, sal[1], 1);

Trang 25
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

for(int i = 2; i <= n; i++) {


int l = t.get_left(1, n, 1, 1, in[i] - 1, i);
int r = t.get_right(1, n, 1, in[i] + 1, n, i);
int p = -1, q = -1;
if(l != -1)
p = lca(l, i);
if(r != -1)
q = lca(r, i);
cout << p << " " << q << endl;
if((h[p] < h[q] && q != -1) || p == -1)
swap(p, q);
if(l == -1 && r == -1)
printf("%d\n", h[i]);
else
printf("%d\n", h[i] - h[p] - 1);
[Link](1, n, 1, in[i], sal[i], i);
}
}
int main() {
freopen("[Link]", "r", stdin);
freopen("[Link]", "w", stdout);
setup();
solve();
}
5.5.3. Test kèm theo
[Link]
p?usp=sharing
5.6. Bài 6: Nâng cấp mạng (VOI 2011)
5.6.1. Đề bài: UPGRANET
Một hệ thống gồm N máy tính đánh số từ 1 đến N được kết nối thành một
mạng bởi M đoạn cáp mạng đánh số từ 1 đến M. Đoạn cáp mạng thứ 𝑖 có thông
lượng 𝑤𝑖 kết nối hai máy 𝑢𝑖 , 𝑣𝑖 cho phép truyền dữ liệu theo cả hai chiều giữa hai
máy này.
Một dãy các máy 𝑥1 , 𝑥2 , … 𝑥𝑝 trong đó giữa hai máy 𝑥𝑖 và 𝑥𝑖+1 (𝑖=1,2,…,𝑝−1)
có đoạn cáp nối được gọi là một đường truyền tin từ máy 𝑥1 tới máy 𝑥𝑝 . Thông
lượng của đường truyền tin được xác định như là thông lượng nhỏ nhất trong số
các thông lượng của các đoạn cáp mạng trên đường truyền. Giả thiết là mạng được
kết nối sao cho có đường truyền tin giữa hai máy bất kì và giữa hai máy có không
quá một đoạn cáp mạng nối chúng.
Người ta muốn nâng cấp mạng bằng cách tăng thông lượng của một số đoạn
cáp nối trong mạng. Để tăng thông lượng của mỗi đoạn cáp mạng thêm một lượng
𝑑 (𝑑 > 0) ta phải trả một chi phí đúng bằng 𝑑. Việc nâng cấp mạng phải đảm bảo
là sau khi hoàn tất, thông lượng của mỗi đoạn cáp mạng 𝑖 đều bằng thông lượng
của đường truyền tin có thông lượng lớn nhất từ máy 𝑢𝑖 tới máy 𝑣𝑖 .
Yêu cầu: Tìm phương án nâng cấp các đoạn cáp mạng sao cho tổng chi phí
nâng cấp là nhỏ nhất.
Dữ liệu vào: Từ tệp [Link]
Trang 26
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Dòng thứ nhất: Chứa hai số nguyên dương 𝑁, 𝑀 (𝑁, 𝑀 ≤ 105 ).


Dòng thứ 𝑖 trong số 𝑀 dòng tiếp theo chứa ba số nguyên dương
𝑢𝑖 , 𝑣𝑖 , 𝑤𝑖 (𝑤𝑖 ≤ 106 ), 𝑖 = 1,2 … 𝑀. Các số trên cùng một dòng được ghi cách nhau
ít nhất một dấu cách.
Kết quả ra: Ghi ra tệp [Link]
Ghi ra một số nguyên duy nhất là tổng chi phí nâng cấp thấp nhất.
Ví dụ:
[Link] [Link] Giải thích
67 5
126
135
243
349
454
468
567
5.6.2. Phân tích đề bài và đề xuất thuật toán
Nhận xét:
- Giữa 2 máy tính bất kì 𝑢, 𝑣 chỉ có một đoạn cáp nối, nhưng có thể có nhiều
đường đi giữa chúng.
- Đường đi có thông lượng lớn nhất giữa 2 máy tính 𝑢, 𝑣 chính là đường đi
trên cây khung lớn nhất.
- Với mỗi cạnh (𝑢, 𝑣) không nằm trên khung lớn nhất, ta cần nâng cấp nó
đảm bảo lớn hơn thông lượng của đường đi giữa chúng trên cây khung (hay là
cạnh có trọng số nhỏ nhất trên đường đi từ 𝑢 → 𝑣 trên cây khung lớn nhất).
Qua đó ta đã dễ dàng hình dung thuật toán để giải bài trên.
Có thể sử dụng nhiều kĩ thuật để giải bài này như: Heavy light
decomposition; Tarjan offline LCA;…Nhưng kĩ thuật dễ dàng cài đặt nhất ta vẫn
sử dụng bảng thưa. Bảng thưa lưu thông tin về đỉnh tổ tiên và cả cạnh có trọng số
nhỏ nhất trên đường đi đến đỉnh tổ tiên của nó.
Độ phức tạp thuật toán là: 𝑂(𝑁. log(𝑁))
Chương trình tham khảo:
#include <bits/stdc++.h>
#define maxn 1000005
#define S second
#define F first
#define maxc 1000000007
#define fort(i, a, b) for(int i = (a); i <= (b); i++)
#define ford(i, a, b) for(int i = (a); i >= (b); i--)
#define ll long long
using namespace std;
ll n, m, root[maxn], h[maxn], res;
pair<long long, long long> par[maxn][20];
Trang 27
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

bool dd[maxn];
vector<pair<ll, ll> > ke[maxn];
struct edge {
ll u, v, w;
} ed[maxn];
bool cmp(edge p, edge q) {
return p.w > q.w;
}
ll getroot(ll u) {
if(root[u] == 0)
return u;
return root[u] = getroot(root[u]);
}
void MST() {
sort(ed+1, ed+m+1, cmp);
fort(i, 1, m) {
ll p = getroot(ed[i].u);
ll q = getroot(ed[i].v);
if(p == q)
continue;
root[p] = q;
dd[i] = 1;
ke[ed[i].u].push_back(make_pair(ed[i].v, ed[i].w));
ke[ed[i].v].push_back(make_pair(ed[i].u, ed[i].w));
}
}
void dfs(ll u, ll tr) {
fort(i, 0, int(ke[u].size()) - 1) {
ll v = ke[u][i].F;
if(v == tr)
continue;
h[v] = h[u] + 1;
par[v][0] = make_pair(u,ke[u][i].S);
fort(j, 1, 18) {
par[v][j].F = par[par[v][j-1].F][j-1].F;
par[v][j].S = min(par[par[v][j-1].F][j-1].S, par[v][j-1].S);
}
dfs(v, u);
}
}
pair<long long, long long> lca(ll u, ll v) {
pair<long long, long long> p;
p.S = 1ll* maxc * maxc;
if(h[u] > h[v])
swap(u, v);
ll diff = h[v] - h[u];
ford(i, 18, 0)
if((diff >> i) & 1) {
p.S = min(p.S, par[v][i].S);
v = par[v][i].F;
}
if(v == u)
return make_pair(u, p.S);
ford(i, 18, 0)
if(par[u][i].F != par[v][i].F) {
p.S = min(p.S, min(par[v][i].S, par[u][i].S));

Trang 28
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

v = par[v][i].F;
u = par[u][i].F;
}
return make_pair(par[u][0].F,min(p.S,min(par[u][0].S,par[v][0].S)));
}
void solve() {
MST();
h[1] = 1;
dfs(1, 0);
fort(i, 1, m)
if(!dd[i]) {
pair<long long, long long> l = lca(ed[i].u, ed[i].v);
res += max(0ll, l.S - ed[i].w);
}
cout << res;
}
int main() {
ios_base::sync_with_stdio(0);
freopen("[Link]", "r", stdin);
freopen("[Link]", "w", stdout);
cin >> n >> m;
fort(i, 1, m)
cin >> ed[i].u >> ed[i].v >> ed[i].w;
solve();
}
5.6.3. Test kèm theo
[Link]
p?usp=sharing
5.7. Bài 7: Dạo chơi đồng cỏ (PWALK – Spoj)
5.7.1. Đề bài: PWALK
Có 𝑁 con bò (1 ≤ 𝑁 ≤ 105 ), để thuận tiện ta đánh số từ 1 → 𝑁, đang ăn cỏ
trên 𝑁 đồng cỏ, để thuận tiện ta cũng đánh số các đồng cỏ từ 1 → 𝑁. Biết rằng con
bò 𝑖 đang ăn cỏ trên đồng cỏ 𝑖.
Một vài cặp đồng cỏ được nối với nhau bởi 1 trong 𝑁 − 1 con đường 2 chiều
mà các con bò có thể đi qua. Con đường 𝑖 nối 2 đồng cỏ 𝐴𝑖 và 𝐵𝑖 (1 ≤ 𝐴𝑖 , 𝐵𝑖 ≤ 𝑁)và
có độ dài 𝐿𝑖 (1 ≤ 𝐿𝑖 ≤ 104 ).
Các con đường được thiết kế sao cho với 2 đồng cỏ bất kỳ đều có duy nhất
1 đường đi giữa chúng. Như vậy các con đường này đã hình thành 1 cấu trúc cây.
Các chú bò rất có tinh thần tập thể và muốn được thăm thường xuyên. Vì
vậy lũ bò muốn bạn giúp chúng tính toán độ dài đường đi giữa 𝑄 (1 ≤ 𝑄 ≤
1000) cặp đồng cỏ (mỗi cặp được mô tả là 2 số nguyên 𝑢, 𝑣 (1 ≤ 𝑢, 𝑣 ≤ 𝑁).
Dữ liệu vào: Nhập vào từ tệp [Link]
Dò ng đầu ghi 2 số nguyê n cá ch nhau bở̛i dấ u cá ch: 𝑁 và 𝑄
𝑁 − 1 dòng tiếp theo: Mỗi dòng chứa 3 số nguyê n cá ch nhau bở̛i dấ u
cá ch:𝐴𝑖 , 𝐵𝑖 và 𝐿𝑖 , mô tả có đường đi trực tiếp giữa 𝐴𝑖 , 𝐵𝑖 và độ dài của nó là 𝐿𝑖

Trang 29
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

𝑄 dòng tiếp theo: Mỗ i dò ng chứa 2 số nguyê n (𝑢, 𝑣) khá c nhau yê u cầ u tính
toá n độ̂ dà i 2 đồ ng cở mà lũ bò muố n đi thă m qua lậ i.
Kết quả ra: Ghi ra tệp [Link]
Ghi Q số là kết quả của từng yêu cầu theo thứ tự, mỗi số viết trên một dòng.
Ví dụ:
[Link] [Link] Giải thích
42 2 Đường đi từ 12 độ
212 7 dài 2. Đường đi từ 3
432 đến 2 độ dài 7.
143
12
32

5.7.2. Phân tích đề bài và đề xuất thuật toán


Đây là bài toán cũng khá đơn giản, thực chất chỉ là truy vấn tính tổng độ dài
trên đường đi giữa 2 đỉnh trên cây.
Giới bạn của bài toán được tác giả điều chỉnh tăng lên so với đề gốc (đề thi
USACO 2008) khi đó việc giải các bài toán về đồ thị dạng cây chưa phổ biến trong
các cuộc thi lập trình thi đấu.
Để giải quyết bài toán trên ta cần bổ sung thêm cập nhật thông tin khoảng
cách đến gốc (tính theo độ dài đường đi) khi duyệt DFS (dạng bài toán quy hoạch
động trên cây).
Độ phức tạp bài toán này là 𝑂(𝑁 ∗ 𝑙𝑜𝑔(𝑁) + 𝑄 ∗ 𝑙𝑜𝑔(𝑁))
Chương trình tham khảo:
#include<bits/stdc++.h>
using namespace std;
struct edge {
int u,w;
};
int n,q,up[100005][18],depth[100005],len[100005];
vector<edge> adj[100005];
void DFS(int u, int p, int w) {
len[u]=len[p]+w; ///khoang cach tu u den goc
depth[u]=depth[p]+1; ///do sau
up[u][0]=p; ///sparse table
for(int i=1; i<17; i++)
up[u][i]=up[up[u][i-1]][i-1];
for(auto v:adj[u]) {
if(v.u!=p)
DFS(v.u,u,v.w);
}
}
int lca(int u, int v) {
if(depth[u]<depth[v])
swap(u,v);
for(int i=16; i>=0; i--) ///Binary lifting

Trang 30
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

if(depth[up[u][i]]>=depth[v])
u=up[u][i];
if(u==v)
return u;
for(int i=16; i>=0; i--) ///Binary lifting
if(up[u][i]!=up[v][i]) {
u=up[u][i];
v=up[v][i];
}
return up[u][0];
}
int main() {
ios_base::sync_with_stdio(0);
// freopen("[Link]","r",stdin);
cin>>n>>q;
for(int i=1; i<n; i++) {
int u,v,w;
cin>>u>>v>>w;
adj[u].push_back({v,w});
adj[v].push_back({u,w});
}
DFS(1,0,0);
while(q--) {
int u,v;
cin>>u>>v;
cout<<(len[u]+len[v]-2*len[lca(u,v)])<<"\n";
}
}
5.7.3. Test kèm theo
[Link]
p?usp=sharing
Mở rộng một chút bài 7, ta có bài toán sau:
5.8. Bài 8: TREEDGE
5.8.1. Đề bài TREEDGE
Cho một cây có trọng số gồm N đỉnh, N-1 cạnh, đỉnh gốc là đỉnh 1. Có Q truy
vấn, mỗi truy vấn cho dưới dạng (𝑢, 𝑣, 𝑥) hỏi đường đi có trọng số lớn nhất giữa
cặp (𝑢, 𝑣) trên cây nếu được phép nối một đỉnh thuộc cây con gốc 𝑢 với một đỉnh
cây con gốc 𝑣 bởi một cạnh có trọng số là 𝑥 (𝑥 ≥ 0) bằng bao nhiêu?
Dữ liệu vào: Từ tệp [Link]
Dòng đầu ghi số 𝑁, 𝑄 (1 ≤ 𝑁, 𝑄 ≤ 2 ∗ 105 ) là số đỉnh của cây, số truy vấn.
𝑁 − 1 dòng tiếp ghi bố số 𝑢, 𝑣, 𝑤 (1 ≤ 𝑢, 𝑣 ≤ 𝑁, |𝑤| ≤ 109 ) mô tả các cạnh
của cây, đỉnh đầu 𝑢, đỉnh cuối 𝑣, trọng số 𝑤 của cạnh.
𝑄 dòng tiếp theo ghi bộ số 𝑢, 𝑣, 𝑥 thể hiện truy vấn tìm trọng số đường đi
từ 𝑢 đến 𝑣 lớn nhất khi được thêm một cạnh trọng số 𝑥 (0 ≤ 𝑥 ≤ 109 ) nối một
đỉnh thuộc cây con gốc 𝑢 với 1 đỉnh thuộc cây con gốc 𝑣.
Kết quả ra: Ghi ra tệp [Link]
Ví dụ:

Trang 31
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

[Link] [Link] Giải thích


73 10 Với truy vấn đầu tiên
121 7 (2,3,1): Tối ưu khi
1 3 -2 5 thêm cạnh nối giữa 4
243 và 6. Tạo ra đường đi
2 5 -4 2463 với tổng
575 trọng số: 3+1+6=10.
366
231
542
560
5.8.2. Phân tích đề bài và đề xuất thuật toán
Nhận xét:
Đồ thị dạng cây, nên nếu không nối thêm cạnh thì đường đi 𝑢 → 𝑣 là duy
nhất và ta tính trọng số giống bài 7.
𝑑𝑖𝑠𝑡(𝑢, 𝑣) = 𝑙𝑒𝑛(𝑢) + 𝑙𝑒𝑛(𝑣) − 2 ∗ 𝑙𝑒𝑛(𝐿𝐶𝐴(𝑢, 𝑣))
Với 𝑙𝑒𝑛(𝑢) là khoảng cách đến 𝑢 → 𝑟𝑜𝑜𝑡.
Trong bài này được phép bổ sung thêm một cạnh có trọng số 𝑥, nên ta quan
tâm thêm một đường đi nữa từ 𝑢 đến con của nó sang con của 𝑣 rồi đến 𝑣. Để tính
nhanh tổng trọng số đường này, ta áp dụng dạng bài quy hoạch động trên cây
(dạng cơ bản) để cập nhật đường đi lớn nhất từ một đỉnh 𝑢 đến con của nó.
𝑑𝑖𝑠𝑡2(𝑢, 𝑣) = 𝑙𝑒𝑛2(𝑢) + 𝑙𝑒𝑛2(𝑣) + 𝑥
Với 𝑙𝑒𝑛2(𝑢) là đường đi lớn nhất từ 𝑢 đến con của nó.
Kết quả của bài toán là: 𝑚𝑎𝑥{𝑑𝑖𝑠𝑡(𝑢, 𝑣), 𝑑𝑖𝑠𝑡2(𝑢, 𝑣)}
Độ phức tạp thuật toán:
- Tiền xử lí để tính khoảng cách với LCA: 𝑂(𝑁 ∗ log(𝑁))
- Tiền xử lí để tính độ dài đường khi thêm cạnh: 𝑂(𝑁).
- Để tính đường đi thứ nhất: 𝑂(log(𝑁))
- Để tính đường đi thư hai: 𝑂(1).
Chương trình tham khảo:
#include<bits/stdc++.h>
#define maxn 200005
#define int long long
using namespace std;
struct edge {
int u,w;
};
int T,n,q,up[maxn][20],depth[maxn],len[maxn], len2[maxn];
vector<edge> adj[maxn];
void DFS(int u, int p, int w) {
len[u]=len[p]+w;
depth[u]=depth[p]+1;
up[u][0]=p;
Trang 32
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

for(int i=1; i<=18; i++)


up[u][i]=up[up[u][i-1]][i-1];
for(auto v:adj[u]) {
if(v.u!=p)
DFS(v.u,u,v.w);
}
}
void DFS2(int u, int p, int w) {
len2[u]=0;
for(auto v:adj[u]) {
if(v.u!=p) {
DFS2(v.u,u,v.w);
len2[u]=max(len2[u],len2[v.u]+v.w);
}
}
}
int lca(int u, int v) {
if(depth[u]<depth[v])
swap(u,v);
for(int i=18; i>=0; i--)
if(depth[up[u][i]]>=depth[v])
u=up[u][i];
if(u==v)
return u;
for(int i=18; i>=0; i--)
if(up[u][i]!=up[v][i]) {
u=up[u][i];
v=up[v][i];
}
return up[u][0];
}
main() {
ios_base::sync_with_stdio(0);
// freopen("[Link]","r",stdin);
cin>>n>>q;
for(int i=1; i<=n; i++)
adj[i].clear();
for(int i=1; i<n; i++) {
int u,v,w;
cin>>u>>v>>w;
adj[u].push_back({v,w});
adj[v].push_back({u,w});
}
DFS(1,0,0);
DFS2(1,0,0);
while(q--) {
int u,v,x;
cin>>u>>v>>x;
int res1=len[u]+len[v]-2*len[lca(u,v)];
int res2=len2[u]+len2[v]+x;
cout<<max(res1,res2)<<"\n";
}
}
5.8.3. Test kèm theo
[Link]
p?usp=sharing

Trang 33
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.9. Bài 9: Đường đi qua K cạnh


5.9.1. Đề bài: TREEQ
Cho một cây gồm N đỉnh, N-1 cạnh. Có Q truy vấn, mỗi truy vấn cho bởi bộ
(𝑥, 𝑦, 𝑎, 𝑏, 𝑘) hỏi rằng có cách nào đi từ 𝑎 → 𝑏 qua đúng 𝑘 cạnh nếu cây được nối
thêm cạnh (𝑥, 𝑦), đường đi có thể qua các đỉnh, các cạnh nhiều lần.
Dữ liệu vào: Từ tệp [Link]
Dòng đầu tiên là ghi số 𝑁, 𝑄 (3 ≤ 𝑁, 𝑄 ≤ 105 ) là số đỉnh, số truy vấn.
N-1 dòng tiếp theo thể hiện các cạnh của cây.
Q dòng tiếp theo thể hiện các truy vấn mô tả như trên.
Kết quả ra: Ghi ra tệp [Link]
Ghi ra Q dòng trả lời cho mỗi truy vấn, nếu tồn tại đường đi thỏa mãn thì
ghi 1, ngược lại thì ghi 0.
Ví dụ:
[Link] [Link] Giải thích
5 1
12 1
23 0
34 1
45 0
5
13122
14132 Truy vấn 1: Đi như sau: 1 → 3 → 2
14133 Truy vấn 2: Đi như sau: 1 → 2 → 3
42339 Truy vấn 4: Đi như sau:
52339 3 → 4→2→3→4→2→3→ 4→ 2→ 3
5.9.2. Phân tích đề bài và đề xuất thuật toán
Nhận xét 1: Khi chưa thêm cạnh (𝑥, 𝑦) thì giữa 𝑎, 𝑏 luôn có đường đi qua 𝑘
cạnh nếu
𝑑𝑖𝑠𝑡(𝑎, 𝑏) ≤ 𝑘 (đường đi trực tiếp a → b phải nhỏ hơn k)
{
𝑑𝑖𝑠𝑡(𝑎, 𝑏)%2 == 𝑘%2 (khoảng cách a → b cùng tính chẵn lẻ với k)
Khi 𝑑𝑖𝑠𝑡(𝑎, 𝑏) ≤ 𝑘 và 𝑑𝑖𝑠𝑡(𝑎, 𝑏) cùng tính chẵn lẻ với 𝑘 thì để đi từ 𝑎 → 𝑏
qua đúng 𝑘 cạnh thì ta chỉ cần lặp đi lặp lại một cạnh với số lần phù hợp thì sẽ
đảm bảo yêu cầu của bài toán.
Nhận xét 2: Khi nối thêm cạnh (𝑥, 𝑦) thì hiển nhiên ta có thêm cơ hội để
thay đổi tính chẵn lẻ của đường đi 𝑎 → 𝑏. Khi đó ta quan tâm đến cách đi sau:
Cách 1: Đi từ 𝑎 → 𝑥 mà không qua cạnh nối thêm, rồi đến 𝑦 và từ 𝑦 về 𝑏
không qua cạnh nối thêm.
Cách 2: Đi từ 𝑎 → 𝑦 mà không qua cạnh nối thêm, rồi đến 𝑥, và từ 𝑥 về 𝑏
không qua cạnh nối thêm.

Trang 34
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Cách 3: Đi từ 𝑎 → 𝑏 theo cách đi trên cây ban đầu.


Trong 3 cách đi, nếu có cách đi nào thỏa mãn ràng buộc trong nhận xét 1
thì bài toán có kết quả là 1, ngược lại thì là 0.
Bài toán này đương nhiên cần kết hợp cách giải bài toán LCA.
Độ phức tạp thuật toán: 𝑂(𝑁 ∗ 𝑙𝑜𝑔(𝑁) + 𝑄 ∗ 𝑙𝑜𝑔(𝑁))
Chương trình tham khảo:
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 100005;
const int LIM = 17;
const int INF = (int)1e9+7;
vector<int> adj[MAX_N];
int depth[MAX_N];
int up[MAX_N][LIM+1];
void DFS(int u, int p) {
depth[u] = depth[p]+1;
up[u][0] = p;
for(int i = 1; i <= LIM; i++)
up[u][i] = up[up[u][i - 1]][i - 1];
for(int x : adj[u])
if(x != p)
DFS(x, u);
}
int lca(int a, int b) {
if(depth[a] > depth[b])
swap(a, b);
for(int i = LIM; i >= 0; i--) {
if(depth[up[b][i]] >= depth[a]) {
b = up[b][i];
}
}
if(a == b)
return a;
for(int i = LIM; i >= 0; i--) {
if(up[a][i] != up[b][i]) {
a = up[a][i];
b = up[b][i];
}
}
return up[a][0];
}
int dist(int u, int v) {
return depth[u]+depth[v]-2*depth[lca(u,v)];
}
int main() {
ios::sync_with_stdio(0);
// freopen("[Link]","r",stdin);
int n, q;
cin >> n;
for(int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);

Trang 35
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

}
DFS(1, 0);
cin >> q;
while(q--) {
int x, y, a, b, k;
cin >> x >> y >> a >> b >> k;
int without=dist(a,b); ///chua them canh
int with=min(dist(a,x)+dist(y,b),dist(a,y)+dist(x,b))+1;
int ans = INF;
if(without % 2 == k % 2)
ans = without;
if(with % 2 == k % 2)
ans = min(ans, with);
cout << (ans <= k ? "1" : "0") << '\n';
}
}
5.9.3. Test kèm theo
[Link]
p?usp=sharing
5.10. Bài 10: Tom & Jerry
5.10.1. Đề bài
Chuột Jerry khá nhanh nhẹn và thông minh, mỗi lần chơi đuổi bắt với mèo
Tom thì ban đầu Jerry đều cố gắng thoát khỏi sự đuổi bắt của Tom lâu nhất có thể,
trong trường hợp bị mèo Tom tóm được, Jerry bao giờ cũng nghĩ ra cách để troll
mèo Tom. Lần này cũng vậy, Tom và Jerry chơi đuổi bắt trong một ngôi nhà có N
căn phòng, mỗi căn phòng chỉ có một con đường duy nhất nối giữa chúng, từ hai
căn phòng bất kì luôn chỉ có một hành lang giữa chúng (hay nói cách khác nó có
dạng đồ thị cây N đỉnh, N-1 cạnh, mỗi căn phòng là một đỉnh, hành lang là cạnh
và có độ dài như nhau).
Mỗi một đơn vị thời gian thì Tom và Jerry có thể lựa chọn chạy từ phòng
này sang phòng kia, hoặc có thể đứng im trong phòng, biết rằng chúng nhìn thấy
nhau trong cả ngôi nhà. Khi Tom và Jerry cùng ở một phòng, thì Jerry lại phải nghĩ
cách troll Tom để chạy thoát. Cho Q truy vấn, mỗi truy vấn cho vị trí của Tom và
Jerry, hỏi cuộc dượt đuổi được lâu nhất là bao nhiêu thì Jerry lại phải troll Tom.
Dữ liệu vào: Đọc vào từ tệp [Link]
Dòng đầu là số N, Q (1 ≤ 𝑁, 𝑄 ≤ 105 ).
N-1 dòng tiếp theo, mỗi dòng chứa cặp (𝑢, 𝑣) là có hành lang nối 2 phòng.
Q dòng tiếp theo mô tả truy vấn, mỗi dòng chứa cặp (𝑢, 𝑣) là vị trí của Tom
và Jerry tương ứng.
Kết quả ra: Ghi ra tệp [Link]
Ghi Q số là thời gian lâu nhất Tom và Jerry chạy dượt đuổi tương ứng với
các truy vấn đã cho.
Ví dụ:

Trang 36
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

[Link] [Link]
Giải thích
32 2 Truy vấn 1: Tom ở 1, Jerry ở 2.
12 1 Jerry sẽ chạy sang 3. Sau đó đứng
23 tại đấy. Tom chạy sang 2, rồi sang
12 3 thì bắt được Jerry
23 Truy vấn 2: Jerry chỉ đứng yên và
Tom chạy đến bắt.
5.10.2. Phân tích đề bài và đề xuất thuật toán
Nhận xét: Tại một thời điểm bất kì xét đường
đi từ Tom (T) đến Jerry (J) thì thấy rằng tất cả các vị
trí bên từ giữa đến J thì Jerry đều có thể chạy đến mà không bị bắt. Do đó để kéo
dài trò chơi thì J nên chọn vị trí xa vị trí giữa nhất tính về nửa bên phải của mình
(như hình).
Các cách chưa tối ưu (duyệt trâu), tôi sẽ không đề cập ở đây.
Như nhận xét, ta thấy để giải quyết mỗi truy vấn, ta cần giải quyết 2 bài
toán nhỏ:
- Bài toán 1: Tìm điểm ở giữa nhanh nhất (cần tính khoảng cách 𝑇 → 𝐽, cần
xác định nhanh đỉnh nào ở giữa,...), đây liên quan đến LCA. Độ phức tạp
𝑂(log(𝑁)), nhưng tiền xử lí cho toàn bài trong 𝑂(𝑁. log(𝑁)).
Ta sẽ tính khoảng cách 𝑇 → 𝐽
𝑑𝑖𝑠𝑡 = 𝑑𝑒𝑝𝑡ℎ[𝑡] + 𝑑𝑒𝑝𝑡ℎ[𝑗] − 𝑑𝑒𝑝𝑡ℎ[𝑙𝑐𝑎(𝑡, 𝑗)] ∗ 2
Khoảng cách từ 𝑇 → 𝑚𝑖𝑑: 𝑑𝑖𝑠𝑡𝑇 = 𝑑𝑖𝑠𝑡/2
Khoảng cách từ 𝐽 → 𝑚𝑖𝑑: 𝑑𝑖𝑠𝑡𝐽 = (𝑑𝑖𝑠𝑡 − 1)/2
- Bài toán 2: Xét trong nửa bên phải đường đi phía J, cần tìm điểm xa điểm giữa
nhất, đây là bài toán quy hoạch động trên cây. Độ phức tạp mỗi truy vấn 𝑂(1),
nhưng tiền xử lí cho toàn bài trong 𝑂(𝑁).
Ta sẽ dùng DFS để duyệt cây, cập nhật khoảng cách xa
nhất từ dưới lên. Với mỗi đỉnh 𝑢 cần có được thông tin đỉnh xa
nhất trong nhánh DFS gốc 𝑢, và thông tin đỉnh xa nhất từ 𝑢 ra
ngoài nhánh DFS gốc 𝑢.
Trường hợp 1: 𝑑𝑒𝑝𝑡ℎ[𝑇] > 𝑑𝑒𝑝𝑡ℎ[𝐽]
Kết quả sẽ là khoảng cách từ 𝑇 → 𝑚𝑖𝑑 cộng với khoảng
cách xa nhất từ 𝑚𝑖𝑑 ra ngoài cây con DFS gốc 𝑚𝑖𝑑
𝑟𝑒𝑠1 = 𝑑𝑖𝑠𝑡𝑇 + 𝑢𝑝[𝑚𝑖𝑑]
Trường hợp 2: 𝑑𝑒𝑝𝑡ℎ[𝑇] > 𝑑𝑒𝑝𝑡ℎ[𝐽]
Kết quả sẽ là khoảng cách từ 𝐽 → 𝑚𝑖𝑑 cộng với khoảng
cách xa nhất từ 𝑚𝑖𝑑 tới đỉnh trong cây con DFS gốc 𝑚𝑖𝑑
𝑟𝑒𝑠2 = 𝑑𝑖𝑠𝑡𝑇 + 𝑑𝑜𝑤𝑛[𝑚𝑖𝑑]
Độ phức tạp thuật toán: 𝑂(𝑁. log(𝑁) + 𝑄. log(𝑁))
Trang 37
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Chương trình tham khảo:


#include <bits/stdc++.h>
using namespace std;
#define L 20
#define N 100005
int n,q,depth[N],anc[N][L],downj[N],down1[N],down2[N],up[N];
vector<int> adj[N];
int ascend(int u, int d) { ///to tien thu d cua u
for(int k = 0; k < L; k++) {
if(d == 0)
break;
if(d & 1)
u = anc[u][k];
d >>= 1;
}
return u;
}
int lca(int u, int v) {
if(depth[u]<depth[v])
swap(u,v);
for(int i=17; i>=0; i--) ///Binary lifting
if(depth[anc[u][i]]>=depth[v])
u=anc[u][i];
if(u==v)
return u;
for(int i=17; i>=0; i--) ///Binary lifting
if(anc[u][i]!=anc[v][i]) {
u=anc[u][i];
v=anc[v][i];
}
return anc[u][0];
}
void DFS(int u, int p) {
depth[u]=depth[p]+1;
anc[u][0]=p; ///sparse table
for(int i=1; i<17; i++)
anc[u][i]=anc[anc[u][i-1]][i-1];
for(int v:adj[u]) {
if(v==p)
continue;
DFS(v,u);
int d=down1[v]+1;
if(d>down1[u]) {
down2[u]=down1[u];
down1[u]=d;
downj[u]=v;
} else
if(d>down2[u]) {
down2[u]=d;
}
}
}
void DFS2(int u, int p) {
for(int v:adj[u]) {
if(v==p)
continue;
up[v]=up[u]+1;
if(downj[u]==v)
Trang 38
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

up[v]=max(up[v],down2[u]+1);
else
up[v]=max(up[v],down1[u]+1);
DFS2(v,u);
}
}
int answer_query(int t, int j) {
if(t == j)
return 0;
int dist = depth[t] + depth[j] - depth[lca(t,j)] * 2;
int distj = (dist - 1) >> 1;
int distt = dist >> 1;
return distt + (depth[t] > depth[j] ? up[ascend(t, distt)] :
down1[ascend(j, distj)]+ 1);
}
int main() {
scanf("%d%d", &n, &q);
for(int i = 0; i < n; i++) {
adj[i].clear();
}
for(int i = 1; i < n; i++) {
int a, b;
scanf("%d%d", &a, &b);
adj[a].push_back(b);
adj[b].push_back(a);
}
DFS(1,0);
DFS2(1,0);
while(q--) {
int t, j;
scanf("%d%d", &t, &j);
printf("%d\n", answer_query(t, j));
}
}
5.10.3. Test kèm theo
[Link]
p?usp=sharing
5.11. Bài 11: Cập nhật thông tin trên cây 1
5.11.1. Đề bài: Update tree
Cho một cây có N đỉnh. Ban đầu đỉnh 𝑖 có giá trị 𝑎𝑖 . Có Q truy vấn, mỗi truy
vấn tăng tất cả các đỉnh nằm trên đường đi từ 𝑢 đến 𝑣 một lượng là 𝑥. Hãy in ra
giá trị của các đỉnh của cây sau khi xử lí hết truy vấn.
Dữ liệu vào: Đọc dữ liệu từ tệp [Link]
Dòng đầu là số N (1 ≤ 𝑁 ≤ 105 ).
Dòng tiếp theo ghi N số 𝑎1 , 𝑎2 , … 𝑎𝑁 (1 ≤ 𝑎𝑖 ≤ 104 ) là giá trị ban đầu của
các đỉnh.
N-1 dòng tiếp theo ghi các cặp (𝑢, 𝑣) thể hiện cạnh của cây.
Dòng tiếp theo ghi số 𝑄 (1 ≤ 𝑄 ≤ 105 ).
Q dòng tiếp theo ghi bộ số (𝑢, 𝑣, 𝑥) với 1 ≤ 𝑢, 𝑣, 𝑥 ≤ 𝑁 thể hiện truy vấn.
Trang 39
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Kết quả ra: Ghi ra tệp [Link]


Ghi ra một dòng N số là giá trị tại các đỉnh sau khi thực hiện Q truy vấn.
Ví dụ:
[Link] [Link] Giải thích
5 13123
00000
12
13
24
25
2
452 Sau truy vấn 1: 0 2 0 2 2
351 Sau truy vấn 2: 1 3 1 2 3
5.11.2. Phân tích đề bài và đề xuất thuật toán
*) Nhận xét:
Xét bài toán: Cho một mảng 1 chiều có N số 𝑎1 , 𝑎2 , … 𝑎𝑁 ban đầu đều bằng
0, có các truy vấn tăng đoạn [𝑢, 𝑣] lên 𝑥. Khi đó mỗi truy vấn ta có thể gán 𝑎[𝑢] =
𝑎[𝑢] + 𝑥 (tăng đầu đoạn) và gán 𝑎[𝑣 + 1] = 𝑎[𝑣 + 1] − 𝑥, thì sau Q truy vấn, ta
thực hiện cộng dồn từ 𝑎1 → 𝑎𝑁 thì ta được một mảng là dãy số cuối cùng.
*) Tương tự, với cây thì ta thấy:
- Dữ liệu giá trị cho ban đầu của các đỉnh chỉ có tác dụng làm tăng, giảm độ
khó của bài toán.
- Mỗi truy vấn tăng giá trị các đỉnh trên đường đi 𝑢 đến 𝑣 chắc chắn ta không
thể tăng luôn tất cả các giá trị trên đường đi. Do đó việc lưu trữ thông tin để truy
vết đường đi là không hợp lí. Vì đường đi giữa các cặp (𝑢, 𝑣)là duy nhất, nên mỗi
truy vấn thay đổi giá trị các đỉnh ta cần phải lưu thông tin tại ít nhất ở 3 đỉnh là:
𝑢, 𝑣, 𝐿𝐶𝐴(𝑢, 𝑣).
- Thông tin có thể cập nhật từ đỉnh đầu đỉnh cuối về LCA kiểu cộng dồn từ
các đỉnh con trong quá trình duyệt DFS.
Do vậy, mỗi truy vấn (𝑢, 𝑣) ta sẽ tăng đỉnh giá trị tại đỉnh 𝑢, 𝑣 thêm lượng
𝑥; giảm giá trị tại đỉnh 𝐿𝐶𝐴(𝑢, 𝑣) và cha của nó một lượng 𝑥.
Khi đó lúc duyệt DFS để cập nhật thông tin từ dưới lên ta sẽ giải quyết được
các truy vấn (dạng quy hoạch động trên cây).
Độ phức tạp thuật toán:
- Tạo bảng thưa để Binary lifting: 𝑂(𝑁. log(𝑁)).
- Xử lí các truy vấn 𝑂(𝑄. log(𝑁))
- Cập nhật giá trị tăng thêm tại các đỉnh 𝑂(𝑁).
Chương trình tham khảo:
#include<bits/stdc++.h>
#define maxn 100005

Trang 40
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

#define int long long


using namespace std;
int n,q,a[maxn],visited[maxn],b[maxn],up[maxn][20],depth[maxn];
vector<int> adj[maxn];
void dfs(int u, int p) {
depth[u]=depth[p]+1;
up[u][0]=p;
for(int i=1; i<=17; i++)
up[u][i]=up[up[u][i-1]][i-1];
for(int v:adj[u]) {
if(v==up[u][0])
continue;
dfs(v,u);
}
}
void dfs2(int u, int p) {
for(int v:adj[u]) {
if(v==p)
continue;
dfs2(v,u);
a[u]=a[u]+a[v];
}
}
int lca(int u, int v) {
if(depth[u]<depth[v])
swap(u,v);
for(int i=16; i>=0; i--)
if(depth[up[u][i]]>=depth[v])
u=up[u][i];
if(u==v)
return u;
for(int i=16; i>=0; i--)
if(up[u][i]!=up[v][i])
u=up[u][i],v=up[v][i];
return up[u][0];
}
main() {
ios_base::sync_with_stdio(0);
// freopen("[Link]","r",stdin);
cin>>n;
for(int i=1; i<=n; i++) {
cin>>b[i];
}
for(int i=1; i<n; i++) {
int u,v;
cin>>u>>v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1,0);
cin>>q;
while(q--) {
int u,v,x;
cin>>u>>v>>x;
a[u]=a[u]+x, a[v]=a[v]+x;
int l=lca(u,v);
a[l]-=x, a[up[l][0]]-=x;
}

Trang 41
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

dfs2(1,0);
for(int i=1; i<=n; i++)
cout<<a[i]+b[i]<<" ";
}
5.11.3. Test kèm theo
[Link]
BZTtbp?usp=sharing
Bài này có thể kết hợp thêm yêu cầu trả lời tính tổng trên đường đi 𝑢 → 𝑣,
bài toán khi đó không khó hơn mà chỉ là việc code dài hơn, nhưng kết hợp với
việc tổng hợp thông tin trên đường đi từ cha xuống con trước khi trả lời truy vấn
tính tổng.
5.12. Bài 12: Cập nhật thông tin trên cây 2
5.12.1. Đề bài: Update tree2
Nâng cấp bài UPDTREE, tương tự bài trên, ta có cây N đỉnh, Q truy vấn, mỗi
truy vấn cho bởi bộ 4 số (𝐴, 𝐵, 𝐶, 𝐷) tăng trọng số của các cạnh trên đường đi từ
A đến B lên 1 đơn vị nhưng không tăng trọng số của các cạnh mà nằm trên trường
đi từ C đến D.
Sau đó là P truy vấn tính tổng trọng số các cạnh trên đường đi từ E đến F.
Dữ liệu vào: Đọc vào từ tệp [Link]
Dòng đầu là số N, Q, P (1 ≤ 𝑁, 𝑄, 𝑃 ≤ 105 ).
N-1 dòng tiếp theo ghi các cặp (𝑢, 𝑣) thể hiện cạnh của cây.
Q dòng tiếp theo ghi 4 số 𝐴, 𝐵, 𝐶, 𝐷 với 1 ≤ 𝐴, 𝐵, 𝐶, 𝐷 ≤ 𝑁 thể hiện truy vấn
tăng trọng số cạnh
P dòng tiếp theo ghi 2 số 𝐸, 𝐹 (1 ≤ 𝐸, 𝐹 ≤ 𝑁) thể hiện truy vấn tính tổng.
Kết quả ra: Ghi ra tệp [Link]
Ghi ra một dòng gồm P số tương ứng P truy vấn tính tổng.
Ví dụ:
[Link] [Link] Giải thích
522 24 Sau 2 truy vấn
12 cập nhật, các
24 cạnh có trọng số
25 là:
13 (1,2)=1;(1,3)=1;
1423 (2;4)=2; (2,5)=0
3425
45
43
5.12.2. Phân tích đề bài và đề xuất thuật toán
Xét bài toán tương tự trên mảng 1 chiều truy vấn cập nhật cho bởi bộ 4 số
(𝐴, 𝐵, 𝐶, 𝐷). Tăng mỗi vị trí trong đoạn [𝐴, 𝐵] lên 1 nhưng không tăng trong đoạn
[𝐶, 𝐷].
Trang 42
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

- Nếu 2 đoạn giao nhau thì mỗi truy vấn ta thực hiện ta tăng tại A, giảm tại
𝑚𝑎𝑥(𝐴, 𝐶), tăng tại 𝑚𝑖𝑛(𝐵, 𝐷) và giảm tại 𝐵.
Ví dụ: 𝐴 < 𝐶 < 𝐵 < 𝐷

Min{B,D}+

A+ Max{A,C}- B-

- Nếu B<C hoặc D<A thì 2 đoạn không giao nhau, thực hiện tương tự bài
toán ví dụ của bài trước. Tăng tại vị trí A, giảm tại vị trí B+1. Hay khi đó thì
𝑚𝑎𝑥{𝐴, 𝐶} > 𝑚𝑖𝑛{𝐵, 𝐷}
Kết hợp với cách làm của bài UPDTREE, ta có thể làm như sau, tăng đoạn
[𝐴, 𝐵] ban đầu và xem có những cạnh nào giao với [𝐶, 𝐷] thì ta trừ đi. Để cập nhật
trọng số các cạnh, ta vẫn lưu giá trị vào đỉnh, đỉnh thể hiện thông tin cần cập nhật
lên cha của nó khi DFS.
Với việc tăng các cạnh từ A đến B ta thực hiện:
𝑣𝑎𝑙𝑢𝑒[𝐴] + +
{𝑣𝑎𝑙𝑢𝑒[𝐵] + +
𝑣𝑎𝑙𝑢𝑒[𝑙𝑐𝑎(𝐴, 𝐵)]−= 2
Gọi 𝑎𝑛𝑐1 = 𝑙𝑐𝑎(𝐴, 𝐵), 𝑎𝑛𝑐2 = 𝑙𝑐𝑎(𝐶, 𝐷). Ta cần loại bỏ thông tin tăng
trọng số cạnh trên cạnh giao nhau của 4 cặp đoạn sau:
Cặp 1: [A,anc1] với [C,anc2]
Cặp 2: [A,anc1] với [D,anc2]
Cặp 3: [B,anc1] với [C,anc2]
Cặp 4: [B,anc1] với [D,anc2]
Xử lí 4 cặp là tương tự nhau. Ví dụ ta sẽ xử lí cặp 1 như sau:
- Nếu anc2 không nằm trên đường đi từ 𝐴 → 𝑎𝑛𝑐1 → 𝑟𝑜𝑜𝑡 thì 2 đoạn đó
không giao nhau. Dễ thấy, vì nếu ngược lại thì sẽ xuất hiện chu trình.
- Gọi 𝑥 = 𝑙𝑐𝑎(𝐴, 𝐶) khi đó bài toán sẽ là tìm giao [𝐴, 𝑎𝑛𝑐1] với [𝑥, 𝑎𝑛𝑐2]. Khi
này việc xử lí giống hệt bài toán trên mảng 1 chiều đã xét ở trên.
- Độ phức tạp chung của thuật toán là: 𝑂(𝑁. log(𝑁) + 𝑄. log(𝑁))
Chương trinh tham khảo:
#include<bits/stdc++.h>
typedef unsigned long long ll;
using namespace std;
vector<int> adj[100005],tree[100005], depth, euler;
bool visited[100005];
int first[100005],last[100005];
ll value[100005];
Trang 43
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

int N, Q, P;
void dfs(int v, int level) {
visited[v] = true;
euler.push_back(v),depth.push_back(level);
first[v] = (int)[Link]()-1;
for(int u : adj[v]) {
if(!visited[u]) {
tree[v].push_back(u);
dfs(u, level + 1);
euler.push_back(v),depth.push_back(level);
}
}
last[v] = (int)[Link]()-1;
}
vector<int> log_table;
vector<vector<pair<int, int>>> sT(18, vector<pair<int, int>>());
void build_sparse() {
log_table.resize([Link]()+1);
for(int i = 2; i < log_table.size(); i++)
log_table[i] = log_table[i/2] + 1;
for(int i = 0; i < [Link](); ++i)
sT[0].push_back({depth[i], i});
for(int i=1; i<18; i++) {
sT[i].resize([Link]());
for(int j = 0; (j + (1<<i) <= sT[i].size()); ++j) {
sT[i][j] = min(sT[i-1][j], sT[i-1][j+(1<<(i-1))]);
}
}
}
int lca(int v, int u) {
if(v == u)
return v;
int l = min(first[v], first[u]);
int r = max(first[v], first[u]);
int row = log_table[r-l];
return euler[min(sT[row][l], sT[row][r-(1<<row)]).second];
}
bool is_ancestor(int u, int v) {
if(u == v)
return true;
return (first[u] < first[v] && last[v] < last[u]);
}
pair<int, int> intersection(int a, int anc1, int c, int anc2) {
if(a != anc1 && c != anc2 && is_ancestor(anc2, a)) {
int ac = lca(a, c);
if(is_ancestor(anc1, anc2))
swap(anc1, anc2);
if(is_ancestor(anc1, ac))
return {ac, anc1};
}
return {0, 0};
}
void update(int A, int B, int dx) {
if(A > B)
swap(A, B);
value[A]+=dx,value[B]+=dx,value[lca(A, B)]-=2*dx;

Trang 44
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

}
void update_edges() {
int A, B, C, D;
for(int i=0; i<Q; i++) {
cin>>A>>B>>C>>D;
update(A, B, 1);
/* handle intersection */
int anc1 = lca(A, B);
int anc2 = lca(C, D);
auto path1 = intersection(A, anc1, C, anc2);
auto path2 = intersection(A, anc1, D, anc2);
auto path3 = intersection(B, anc1, C, anc2);
auto path4 = intersection(B, anc1, D, anc2);
update([Link], [Link], -1);
update([Link], [Link], -1);
update([Link], [Link], -1);
update([Link], [Link], -1);
}
}
ll diff(int v) {
for(int u : tree[v])
value[v] += diff(u);
return value[v];
}
void build_partial(int v, ll prev) {
value[v] += prev;
for(int u : tree[v]) {
build_partial(u, value[v]);
}
}
void query_edges() {
int E, F;
for(int i=0; i<P; i++) {
cin>>E>>F;
ll sum = 0;
sum+=value[E],sum+=value[F],sum -=2*value[lca(E,F)];
printf("%llu\n", sum);
}
}
int main() {
ios_base::sync_with_stdio(0);
// freopen("[Link]","r",stdin);
cin>>N>>Q>>P;
for(int i=1; i<N; i++) {
int u, v;
cin>>u>>v;
adj[u].push_back(v),adj[v].push_back(u);
}
dfs(1, 0);
build_sparse();
update_edges();
diff(1);
build_partial(1, 0);
query_edges();
}

Trang 45
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.12.3. Test kèm theo


[Link]
BZTtbp?usp=sharing
5.13. Bài 13: Dạo chơi trên cây
5.13.1. Đề bài Walking
Cho một đồ thị dạng cây với N đỉnh và N-1 cạnh. Hằng ngày có một chú khỉ
đi từ một đỉnh 𝑎 đến đỉnh 𝑏. Chú thấy rằng có một số đỉnh đã được thăm vào các
ngày trước đó. Chú băn khoăn là trên đường đi ngày hôm nay từ 𝑎 → 𝑏 có bao
nhiêu đường đi của các ngày trước đó giao với ngày hôm nay, hai đường giao
nhau là 2 đường có ít nhất một đỉnh chung.
Dữ liệu vào: Đọc vào từ tệp [Link]
Dòng đầu tiên ghi số N, Q (1 ≤ 𝑁, 𝑄 ≤ 105 ) là số đỉnh và số ngày dạo chơi
N-1 dòng tiếp theo ghi cặp số (𝑢, 𝑣) thể hiện cạnh trên cây
Q dòng tiếp theo ghi cặp số (𝑎, 𝑏) thể hiện lộ trình dạo chơi của các ngày.
Kết quả ra: Ghi ra tệp [Link]
Q dòng, trong đó dòng thứ 𝑖 là số ngày có khác nhau đã từng đi qua một
đỉnh trên lộ trình của ngày 𝑖.
Ví dụ:
[Link] [Link] Giải thích
54 0 Ngày 1: Chưa có con đường
12 1 nào trước đó giao với đường
13 2 45.
34 2 Ngày 2: Giao với ngày 1 tại
35 đỉnh 4 3
45 Ngày 3: Giao với ngày 1,2
42 Ngày 4: Giao với ngày 2,3
13
12
5.13.2. Phân tích đề bài và đề xuất thuật toán
Để giải quyết bài toán, ta quan tâm đến 2 vấn đề sau:
Vấn đề 1: Làm sao để biết (𝑢, 𝑣) có giao nhau với (𝑢′ , 𝑣 ′ ) của ngày trước đó?
Cách đơn giản nhất đó là kiểm tra tất cả các điểm trên (𝑢, 𝑣) có điểm nào
thuộc đường đi 𝑢′ → 𝑣′ không. Cách này quá chậm, ta có cách khác như sau:
Gọi 𝑤 = 𝑙𝑐𝑎(𝑢, 𝑣), 𝑤’ = 𝑙𝑐𝑎(𝑢’, 𝑣’).
- Nếu 𝑤’ là tổ tiên của 𝑤, thì (𝑢, 𝑣) giao với (𝑢’, 𝑣’) khi 𝑢’ hoặc 𝑣’ nằm trong
cây con gốc 𝑤.
- Nếu w’ nằm trong cây con của w, thì đương nhiên u’, v’ cũng nằm trong
cây con của w. Do đó, nếu (u,v) giao với (u’,v’) thì bắt buộc w’ phải nằm trên đường
𝑢 → 𝑣.
Trang 46
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Tuy nhiên, ta có tất cả cỡ 𝑄2 cặp, mỗi cặp lại xử lí trong 𝑙𝑜𝑔(𝑁), vậy độ phức
tạp thuật toán sẽ là: 𝑂(𝑄2 . log(𝑁)).
Vấn đề 2: Làm sao để đếm nhanh được các cặp giao với (𝑢, 𝑣)?
Nhận xét thấy rằng, vấn đề có thể giải quyết với cấu trúc dữ liệu có khả
năng đếm (ví dụ: BIT, Segment tree).
Ở đây ta sẽ dùng 2 cây BIT cho 2 trường hợp w’ là tổ tiên của w hoặc w là
tổ tiên của w’.
*) Cây BIT1 sẽ giải quyết tình huống w’ là tổ tiên của w. Mỗi khi thêm đường
đi 𝑢 → 𝑣 ′ ta sẽ cập nhật thông tin giống bài UPDTREE2: Tăng thêm 1 tại u’, v’ và

giảm 2 tại lca(u’,v’).


Khi xét (u,v) thì kết quả được tăng thêm tổng trọng số các đỉnh thuộc cây
con của w.
*) Cây BIT2 sẽ giải quyết tình huống w’ là con cháu của w. Khi đó bắt buộc
w’ phải nằm trên đường đi 𝑢 → 𝑣, hay nói cách khác cây BIT2 sẽ đếm số đỉnh mà
là LCA của các (𝑢, 𝑣) đi đã xuất hiện từ 𝑟𝑜𝑜𝑡 → 𝑎, 𝑟𝑜𝑜𝑡 → 𝑏 rồi trừ đi 𝑟𝑜𝑜𝑡 →
𝑙𝑐𝑎(𝑎, 𝑏), 𝑟𝑜𝑜𝑡 → 𝑝𝑎𝑟𝑒𝑛𝑡[𝑙𝑐𝑎(𝑎, 𝑏)].
Để sử dụng cây BIT quản lý thông tin, ta phải quản lý các đỉnh theo thứ tự
duyệt đến và duyệt xong khi thực hiện DFS. Khi này LCA ta cũng quản lý theo thứ
tự duyệt DFS luôn.
Độ phức tạp thuật toán: 𝑂(𝑁. log(𝑁)) + 𝑄. log(𝑁))
Chương trình tham khảo:
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
int n,q,cnt,BIT1[N],BIT2[N],depth[N], up[N][19], out[N], in[N];
vector<int> adj[N];
void dfs(int x, int p) {
in[x] = ++cnt;
up[cnt][0] = in[p];
depth[cnt] = depth[in[p]]+1;;
for(int i=1; i<=17; i++)
up[cnt][i]=up[up[cnt][i-1]][i-1];
for(int y : adj[x])
if(y != p)
dfs(y,x);
out[in[x]] = cnt;
}
int lca(int u, int v) {
if(depth[u] < depth[v])
swap(u, v);
for(int i = 17; i >= 0; i--)
if(depth[up[u][i]] >= depth[v]) {
u = up[u][i];
}
if(u == v)
return u;
for(int i = 17; i >= 0; i--)
Trang 47
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

if(up[u][i] != up[v][i]) {
u = up[u][i], v = up[v][i];
}
return up[u][0];
}
void update(int BIT[], int x, int val) {
while(x<=n) {
BIT[x] += val;
x += (x & (-x));
}
}
int get(int BIT[], int id) {
int ret = 0;
while(id>0) {
ret += BIT[id];
id &= (id-1);
}
return ret;
}
int get(int BIT[], int l, int r) {
return get(BIT,r) - get(BIT,l-1);
}
int main() {
scanf("%d%d",&n,&q);
for(int i =1; i<n; i++) {
int x,y;
scanf("%d%d",&x,&y);
adj[x].push_back(y);
adj[y].push_back(x);
}
dfs(1,0);
while(q--) {
int a,b;
scanf("%d%d",&a,&b);
a = in[a], b = in[b];
int l = lca(a,b);
int ans = get(BIT1,l,out[l]);
update(BIT1, a, 1);
update(BIT1, b, 1);
update(BIT1, l, -2);
ans+=get(BIT2,a)+get(BIT2,b);
ans-=(get(BIT2,l)+(l==1?0:get(BIT2,up[l][0])));
update(BIT2,l,1); ///cap nhat tang tai lca
update(BIT2,out[l]+1,-1); //giam thong tin tren nhanh khac
printf("%d\n",ans);
}
}
5.13.3. Test kèm theo
[Link]
BZTtbp?usp=sharing
5.14. Bài 14: Cây đổi gốc
Bài sau đây, tôi trình bày dạng bài tìm LCA trong trường hợp cây đổi gốc,
hay còn gọi lày dynamic LCA.
Trang 48
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.14.1. Đề bài: ROOTLESS


Cho một đồ thị cây có N đỉnh N-1 cạnh và Q truy vấn. Mỗi truy vấn được
cho bởi bộ 3 số (𝑢, 𝑣, 𝑟) yêu cầu hãy tìm 𝐿𝐶𝐴(𝑢, 𝑣) khi chọn đỉnh 𝑟 làm gốc.
Dữ liệu vào: Đọc từ tệp [Link]
Dòng 1: Số nguyên N, Q (1 ≤ 𝑁, 𝑄 ≤ 105 ) là số đỉnh của cây, số truy vấn.
N-1 dòng tiếp theo ghi các cặp (𝑢, 𝑣) thể hiện cạnh của cây.
Q dòng tiếp theo, mỗi dòng ghi 3 số 𝑢, 𝑣, 𝑟 (1 ≤ 𝑢, 𝑣, 𝑟 ≤ 𝑁).
Kết quả ra: Ghi ra tệp [Link]
Đưa ra Q số là câu trả lời ứng với mỗi câu hỏi.
Ví dụ:
[Link] [Link] Giải thích
4 1 Khi chọn 1 làm gốc
12 2 thì lca(4,2)=1
23 Khi chọn 2 làm gốc
14 lca(4,2)=2
2
142
242

5.14.2. Phân tích đề bài và đề xuất thuật toán


Trong trường hợp không có truy vấn đổi gốc thì nó chỉ là bài toán cơ bản.
Tuy nhiên khi đổi gốc nếu tính lại Sparse table thì độ phức tạp thuật toán là
𝑂(𝑁. 𝑙𝑜𝑔𝑁. 𝑄). Còn nếu duyệt DFS đơn thuần, không dùng Sparse table thì độ
phức tạp thuật toán là 𝑂(𝑁. 𝑄).
Nhận xét:
Với truy vấn (u,v,r) thì lca(u,v) chỉ có thể là một trong 6 đỉnh : r, u, v,
lca(u,v), lca(r,u), lca(v,r) trong đó lca(x,y) là tổ tiên chung gần nhất của 2 đỉnh x,y
khi vẫn chọn 1 làm gốc.
Gọi 𝑥 = 𝑙𝑐𝑎(𝑢, 𝑣) khi chọn 𝑟 là gốc thì tổng khoảng cách 𝑥 → 𝑢, 𝑥 → 𝑣, 𝑥 →
𝑟 sẽ là nhỏ nhất.
Dựa theo nhận xét trên thì ta chỉ cần điều chỉnh lại chương trình ban đầu
một chút thì vẫn giải quyết các truy vấn trong 𝑂(𝑙𝑜𝑔(𝑁)).
Độ phức tạp thuật toán vẫn là 𝑂(𝑁. log(𝑁) + 𝑄. log(𝑁))
Chương trình tham khảo:
#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
vector <int> adj[maxn];
int up[maxn][20], depth[maxn], n, q;
void dfs(int u) {
for(int i = 1; i < 20; i++)

Trang 49
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

up[u][i] = up[up[u][i - 1]][i - 1];


for(int v:adj[u]) {
if(!depth[v]) {
up[v][0] = u;
depth[v] = depth[u] + 1;
dfs(v);
}
}
}
int lca(int u, int v) {
if(depth[u] < depth[v])
swap(u, v);
for(int i = 17; i >= 0; i--)
if(depth[up[u][i]] >= depth[v]) {
u = up[u][i];
}
if(u == v)
return u;
for(int i = 17; i >= 0; i--)
if(up[u][i] != up[v][i]) {
u = up[u][i];
v = up[v][i];
}
return up[u][0];
}
int dist(int u, int v) {
int x = lca(u, v);
int res = depth[u] + depth[v] - 2 * depth[x];
return res;
}
int main() {
cin >> n;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
depth[1] = 1;
dfs(1);
cin >> q;
pair<int, int> p[6];
while(q--) {
int u, v, r;
cin >> u >> v>>r;;
p[0].second = u;
p[1].second = v;
p[2].second = r;
p[3].second = lca(r, u);
p[4].second = lca(r, v);
p[5].second = lca(u, v);
for(int i = 0; i < 6; i++) {
int x = p[i].second;
p[i].first = dist(x, r) + dist(x, u) + dist(x, v);
}
sort(p, p + 6);

Trang 50
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

cout << p[0].second << endl;


}
}
5.14.3. Test kèm theo
[Link]
BZTtbp?usp=sharing
5.15. Bài 15: Cây đổi gốc 2
5.15.1. Đề bài: Rootless2
Đây là phiên bản nâng cấp của bài ROOTLESS, đề bài như sau:
Cho một đồ thị cây có N đỉnh N-1 cạnh và Q truy vấn. Gốc cây ban đầu là
đỉnh 1. Truy vấn có 3 loại sau:
- Loại 1: cho bởi dạng (1, 𝑢) chọn đỉnh 𝑢 làm gốc của cây.
- Loại 2: Cho bởi dạng (2, 𝑢, 𝑣, 𝑥) cộng tất cả các đỉnh trong cây con nhỏ nhất
mà chứa 𝑢, 𝑣 lên 𝑥. Lưu ý là cây con với đỉnh gốc đang có, chứ chưa chắc là đỉnh
gốc 1.
- Loại 3: Cho bởi dạng (3, 𝑢) yêu cầu tính tổng giá trị tại các đỉnh trong cây
con đỉnh 𝑢.
Dữ liệu vào: Đọc vào từ tệp [Link]
Dòng đầu tiên số N, Q (1 ≤ 𝑁, 𝑄 ≤ 105 ) là số đỉnh và số truy vấn.
Dòng tiếp theo ghi 𝑁 số 𝑎1 , 𝑎2 , … 𝑎𝑁 (với |𝑎𝑖 | ≤ 105 ) là giá trị ban đầu tại
các đỉnh.
N-1 dòng tiếp theo ghi cặp số (𝑢, 𝑣) thể hiện cạnh của cây.
Q dòng tiếp theo mô tả các truy vấn trong 3 loại đã nêu ở trên.
Kết quả ra: Ghi ra tệp [Link]
Ghi lần lượt các kết quả tương ứng với truy vấn 3 xuất hiện.
Ví dụ:
[Link] [Link] Giải thích
46 18 Truy vấn 1: (3,1) tính tổng các đỉnh
4356 21 trong cây con đỉnh 1. KQ: 4+3+5+6=18
12 Truy vấn 2. Đưa đỉnh 3 thành gốc.
23 Truy vấn 3: (2,2,4,3). Tăng các đỉnh
34 trong cây con chứa đỉnh 2,4 lên 3. Khi đó
31 trọng số các đỉnh lần lượt là: 7,6,8,9
13 Truy vấn 4: Chọn 1 làm gốc
2243 Truy vấn 5: Giảm các đỉnh cây con chứa
11 2,4 đi 3. Khi đó trọng số là: 7,3,5,6
2 2 4 -3 Truy vấn 6: Tổng trọng số cây con đỉnh
31 1 khi này là: 7+3+5+6=21

Trang 51
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

5.15.2. Phân tích đề bài và đề xuất thuật toán


Với truy vấn loại 1 thì giá trị tại các đỉnh không bị thay đổi.
Với truy vấn loại 2 thì giá trị các đỉnh trong cây con có gốc là 𝐿𝐶𝐴(𝑢, 𝑣)
được tăng một lượng là 𝑥.
Với truy vấn loại 3 thì ta tính tổng các đỉnh con của gốc 𝑢.
Để giải quyết bài này, ta sẽ trải cây thành mảng, để thực hiện truy vấn loại
2, với cập nhật cây con đỉnh 𝑢 có kích thước 𝑠 thì ta cập nhật đoạn từ 𝑢 đến 𝑢 +
𝑠 − 1. Điều này có thể dễ dàng thực hiện bằng cấu trúc Segment tree có Lazy
propagation.
Mọi thứ sẽ trở nên phức tạp khi gốc 𝑟 thay đổi, vậy làm sao để giảm độ phức
tạp khi đó, chắc chắn ta sẽ không thể cập nhật lại thông tin toàn bộ cây. Đương
nhiên ta vẫn sẽ giữa nguyên kết quả của việc tính toán ban đầu với gốc 1.
Ta cần phải giải quyết 2 vấn đề sau:
Vấn đề 1: Làm thế nào để xác định được lca(u,v) khi gốc là 𝑟?
Nếu cả 𝑢, 𝑣 vẫn thuộc cây con gốc 𝑟 (trong cây gốc 1 ban đầu) thì lca(u,v)
không thay đổi so với ban đầu.
Nếu chỉ có một trong hai 𝑢 hoặc 𝑣 thuộc cây con gốc 𝑟 thì lca(u,v) khi gốc là
𝑟 sẽ chính là 𝑟.
Nếu cả 𝑢, 𝑣 đều không thộc cây con gốc 𝑟, thì khi đó ta đi tìm 𝑝 = 𝐿𝐶𝐴(𝑢, 𝑟)
và 𝑞 = 𝐿𝐶𝐴(𝑣, 𝑟) trong cây ban đầu, đỉnh nào có độ sâu lớn hơn thì sẽ chính là
𝐿𝐶𝐴(𝑢, 𝑣) khi chọn gốc là 𝑟.
Kết luận: lca(u,v) khi chọn gốc 𝑟 là đỉnh sâu nhất trong các đỉnh sau:
𝐿𝐶𝐴(𝑢, 𝑣), 𝐿𝐶𝐴(𝑢, 𝑟), 𝐿𝐶𝐴(𝑣, 𝑟). Với cách tìm lca(u,v) như thế này ta cũng có thể
điều chỉnh lại thuật toán bài ROOTLESS.
Vấn đề 2: Cập nhật lại thông tin như thế nào khi đã tìm được 𝑤 = 𝐿𝐶𝐴(𝑢, 𝑣)
khi chọn 𝑟 làm gốc?
- Nếu 𝑤 = 𝑟 thì cập nhật toàn bộ cây.
- Nếu 𝑤 là con của 𝑟 thì chỉ cập nhật cây con gốc 𝑤.
- Nếu 𝑤 là tổ tiên của 𝑟 thì cập nhật toàn bộ cây, sau đó trừ đi nhánh con
của 𝑤 mà chứa 𝑟.
Nên nhớ, bài này cần trải cây thành mảng và sử dụng kĩ thuật Lazy
propagation trên cấu trúc dữ liệu Segment tree.
Việc tính tổng trong cây con gốc 𝒘 khi chọn gốc của cây là 𝒓 xử lí tương
tự như thao tác cập nhật.
Độ phức tạp thuật toán:
- Truy vấn đổi gốc: 𝑂(1)
- Truy vấn cập nhật: 𝑂(log(𝑁))
- Truy vấn tính tổng: 𝑂(log(𝑁))
Trang 52
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

Chương trình tham khảo:


#include<bits/stdc++.h>
#define N 200000
using namespace std;
int g[N],a[N*2][2],v[N],deep[N],fa[N][20],l[N],r[N],to[N];
long long f[N*3],sig[N*3];
int n,q;
void ins(int x,int y) {
static int sum=1;
a[++sum][0]=y,a[sum][1]=g[x],g[x]=sum;
}
void dfs(int x) {
static int sum=0;
to[l[x]=++sum]=x;
deep[x]++;
for(int i=0; fa[fa[x][i]][i]; i++)
fa[x][i+1]=fa[fa[x][i]][i];
for(int i=g[x]; i; i=a[i][1])
if(a[i][0]!=fa[x][0]) {
deep[a[i][0]]=deep[x];
fa[a[i][0]][0]=x;
dfs(a[i][0]);
}
r[x]=sum;
}
void build(int l,int r,int s) {
if(l==r) {
f[s]=v[to[l]];
return;
}
build(l,(l+r)/2,s+s);
build((l+r)/2+1,r,s+s+1);
f[s]=f[s+s]+f[s+s+1];
}
void down(int l,int r,int s) {
if(sig[s]) {
f[s]+=(r-l+1)*sig[s];
if(l!=r)
sig[s+s]+=sig[s],sig[s+s+1]+=sig[s];
sig[s]=0;
}
}
void ins(int l,int r,int s,int ll,int rr,int x) {
down(l,r,s);
if(r<ll||rr<l)
return;
if(ll<=l&&r<=rr) {
sig[s]=x;
down(l,r,s);
return;
}
ins(l,(l+r)/2,s+s,ll,rr,x),ins((l+r)/2+1,r,s+s+1,ll,rr,x);
f[s]=f[s+s]+f[s+s+1];
}
long long get(int l,int r,int s,int ll,int rr) {
down(l,r,s);
Trang 53
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

if(r<ll||rr<l)
return 0;
if(ll<=l&&r<=rr)
return f[s];
return get(l,(l+r)/2,s+s,ll,rr)+get((l+r)/2+1,r,s+s+1,ll,rr);
}
int getlca(int x,int y) {
int i=19;
if(deep[x]<deep[y])
swap(x,y);
while(deep[x]!=deep[y]) {
while(deep[fa[x][i]]<deep[y])
i--;
x=fa[x][i];
}
i=19;
while(x!=y) {
while(fa[x][i]==fa[y][i]&&i)
i--;
x=fa[x][i],y=fa[y][i];
}
return x;
}
int up(int x,int y) {
int i=19;
while(deep[x]!=y) {
while(deep[fa[x][i]]<y)
i--;
x=fa[x][i];
}
return x;
}
int main() {
scanf("%d %d",&n,&q);
for(int i=1; i<=n; i++)
scanf("%d",&v[i]);
for(int i=1; i<n; i++) {
int x,y;
scanf("%d %d",&x,&y);
ins(x,y);
ins(y,x);
}
dfs(1);
build(1,n,1);
int root=1;
while(q--) {
int sig,u,v,x;
scanf("%d %d",&sig,&v);
if(sig==1) {
root=v;
} else
if(sig==2) {
scanf("%d %d",&u,&x);
int LCA1=getlca(u,root);
int LCA2=getlca(v,root);
int LCA3=getlca(u,v);

Trang 54
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

if(deep[LCA1]<deep[LCA2])
swap(u,v),swap(LCA1,LCA2);
if(LCA1==root&&deep[LCA3]<=deep[root])
ins(1,n,1,1,n,x);
else
if(LCA1==LCA2&&LCA1!=LCA3)
ins(1,n,1,l[LCA3],r[LCA3],x);
else {
int LCA4=up(root,deep[LCA1]+1);
ins(1,n,1,1,n,x);
ins(1,n,1,l[LCA4],r[LCA4],-x);
}
} else {
int LCA1=getlca(v,root);
if(root==v)
printf("%I64d\n",get(1,n,1,1,n));
else
if(LCA1!=v)
printf("%I64d\n",get(1,n,1,l[v],r[v]));
else {
int LCA2=up(root,deep[v]+1);
printf("%I64d\n",get(1,n,1,1,n)-
get(1,n,1,l[LCA2],r[LCA2]));
}
}
}
}
5.15.3. Test kèm theo
[Link]
BZTtbp?usp=sharing

6. Một số bài tập tự luyện


[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
and-queries-on-the-graph

Trang 55
Chuyên đề Hội thảo khoa học các trường THPT Chuyên khu vực Duyên Hải và ĐBBB 2020

7. Kết luận
Bài toán LCA cũng là dạng bài toán xuất hiện trên đồ thị dạng cây, hoặc
các đồ thị có thể quy về dạng đồ thị dạng cây.
Đồ thị dạng cây cũng là đồ thị đặc biệt, ta có thể dễ dàng biến cây thành
mảng bằng Euler tour. Do đó, ta cũng có lớp bài toán trên cây áp dụng với các
cấu trúc dữ liệu, thuật toán như đối với mảng.
Bài toán LCA có thể kết hợp với các dạng bài về cấu trúc dữ liệu khác
(Disjoint set union, Segment tree, Binary index tree,…), các dạng bài toán khác
về cây (quy hoạch động, cây khung nhỏ nhất, cầu và khớp,…).
Dạng bài về LCA tôi thường áp dụng dạy cho học sinh đầu lớp 11. Do kinh
nghiệm chưa nhiều, nên cách nhìn nhận vấn đề còn chưa toàn diện và chưa
bao quát được hết các bài toán có sử dụng LCA. Cách trình bày, cách thể hiện
thuật toán có chỗ chưa thể hiện sự sáng tạo, vậy nên tôi rất mong nhận được
chia sẻ, đóng góp của bạn bè đông nghiệp để có cái nhìn đa chiều và hoàn thiện
hơn về dạng bài toán LCA.
Tôi xin chân thành cảm ơn!
8. Tài liệu tham khảo
[1]. Hồ Sĩ Đàm (2016), Tài liệu giáo khoa chuyên Tin học quyển 1, Nhà xuất
bản giáo dục Việt Nam.
[2]. [Link]
[3]. [Link]
[4]. [Link]
[5]. [Link]

Trang 56

You might also like