Bài toán: "Tổ tiên chung gần nhất"-LCA
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
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
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;…
- 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.
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
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
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 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
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:
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.
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
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
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
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
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
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
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ừ 12 độ
212 7 dài 2. Đường đi từ 3
432 đến 2 độ dài 7.
143
12
32
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
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
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
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
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
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
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
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à
′
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
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
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
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
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
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