Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Tích ước chung lớn nhất trên cây (Tin học trẻ toàn quốc 2025 – Vòng loại đợt 3 – Bảng C1, C2)

Tích ước chung lớn nhất trên cây
Nguồn: Tin học trẻ toàn quốc 2025 Độ khó: Khó Cây, LCA Số học

Đề bài

Cho một cây gồm $N$ đỉnh đánh số từ 1 đến $N$; trên đỉnh thứ $i$ có ghi số nguyên dương $A_i$.

Có $Q$ truy vấn, truy vấn thứ $k$ cho bởi ba số $u_k, v_k, x_k$. Hãy tính

$$S_k = \Big( \prod_{t} \gcd(A_t, x_k) \Big) \bmod (10^9 + 7),$$

trong đó tích lấy trên mọi đỉnh $t$ nằm trên đường đi đơn từ $u_k$ đến $v_k$ (kể cả hai đầu mút), $\gcd$ là ước chung lớn nhất.

Dữ liệu vào:

  • Dòng đầu: hai số nguyên dương $N, Q$ ($N, Q \le 10^5$).
  • Dòng thứ hai: $N$ số nguyên dương $A_1, A_2, \ldots, A_N$ ($A_i \le 10^7$).
  • $N - 1$ dòng tiếp theo, mỗi dòng hai số $u, v$ – một cạnh của cây.
  • $Q$ dòng cuối, dòng thứ $k$ chứa $u_k, v_k, x_k$ ($1 \le u_k, v_k \le N$; $x_k \le 10^7$).

Kết quả: $Q$ dòng, dòng thứ $k$ ghi $S_k$.

Subtask:

  • Subtask 1 (30% số điểm): $N, Q \le 1000$.
  • Subtask 2 (30% số điểm): mọi $x_k$ đều là lũy thừa của 2 (số mũ nguyên không âm).
  • Subtask 3 (40% số điểm): không có ràng buộc gì thêm.

Giới hạn thời gian: 1 giây. Dữ liệu nhập từ bàn phím và in ra màn hình.

Ví dụ – input
4 3
1 2 3 4
1 2
2 3
3 4
1 4 1
4 4 2
1 4 2
Ví dụ – output
1
2
4

Giải thích: cây là đường thẳng $1 - 2 - 3 - 4$.

  • Truy vấn 1: $x = 1$ nên mọi $\gcd$ đều bằng 1, $S_1 = 1$.
  • Truy vấn 2: đường đi chỉ gồm đỉnh 4, $S_2 = \gcd(4, 2) = 2$.
  • Truy vấn 3: đường đi $1 \to 2 \to 3 \to 4$, $S_3 = \gcd(1,2) \cdot \gcd(2,2) \cdot \gcd(3,2) \cdot \gcd(4,2)$ $= 1 \cdot 2 \cdot 1 \cdot 2$ $= 4$.

Nguồn: Hội thi Tin học trẻ toàn quốc 2025 – Vòng loại quốc gia đợt 3 – Bảng C1, C2 (bài TREEGCD). Đề được mình viết lại cho dễ đọc; phần giải thích ví dụ là mình tự thêm.

Lời mở đầu

Đây là bài khó nhất của đợt 3, kết hợp hai mảng kiến thức: số học (phân tích thừa số nguyên tố, sàng) và cây (LCA, DFS). Cái khó là $\gcd$ không "cộng dồn" được như tổng – không thể làm tiền tố trực tiếp. Mẹo là tách $\gcd$ thành từng lũy thừa nguyên tố, khi đó mỗi phần trở thành bài đếm số đỉnh trên đường đi – thứ mà LCA làm rất giỏi. Subtask 2 ($x = 2^e$) chính là bước đệm để nghĩ ra mẹo này.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Bộ testBộ test có gì?

Bộ test của mình gồm 20 test chia đúng tỉ lệ subtask: test 1–6 (subtask 1), 7–12 (subtask 2), 13–20 (subtask 3). Test 1 là ví dụ của đề. Lời giải chính thức của mình (cùng ý tưởng cách 3, cài đặt tối ưu hơn với LCA Tarjan) đã được nộp thử trên hệ thống chấm chính thức tinhoctre.vn và đạt 100/100 điểm.

Test$N$$Q$Độ sâu cây
(gốc 1)
Dạng cây$A$ lớn nhất$x$ lớn nhất$x$ đều là $2^e$?
1433nhỏ42có
2150nhỏ218.168.160không
31.0001.00017ngẫu nhiên / nhị phân9.997.0209.979.200không
41.0001.000691đường thẳng9.996.0009.975.040không
51.0001.0002hình sao9.992.0619.990.441không
61.0001.000674đường thẳng8.388.6088.388.608có
7100.000100.00034ngẫu nhiên / nhị phân9.437.1848.388.608có
8100.000100.00061.484đường thẳng9.437.1848.388.608có
9100.000100.00040.346chổi / sâu8.388.6088.388.608có
10100.000100.00073.291đường thẳng8.388.6088.388.608có
11100.000100.00028ngẫu nhiên / nhị phân9.999.7638.388.608có
12100.000100.00039.739chổi / sâu9.999.8608.388.608có
13100.000100.00042ngẫu nhiên / nhị phân9.999.9109.999.987không
14100.000100.00032ngẫu nhiên / nhị phân9.998.1009.998.560không
15100.000100.00078.788đường thẳng9.998.5609.998.560không
16100.000100.00049.996chổi / sâu9.999.9859.999.784không
17100.000100.00058.142đường thẳng9.699.6909.699.690không
18100.000100.0002hình sao9.998.5609.999.603không
19100.000100.00039.286chổi / sâu9.999.9339.998.100không
20100.000100.00032ngẫu nhiên / nhị phân9.998.1009.998.100không
  • Cây có đủ dạng: đường thẳng dài (độ sâu tới hàng chục nghìn – "sát thủ" của cách đi từng bước), hình sao, chổi, cây ngẫu nhiên nông.
  • Test 10 và 17 dùng $A$, $x$ cố định có số mũ lớn ($2^{23}$; $2 \cdot 3 \cdot 5 \cdots 19$) trên đường thẳng dài.

Kết quả chạy thật của các cách theo từng subtask:

SubtaskĐiều kiệnTestCách 1
C++
Cách 1
Python
Cách 2
C++
Cách 2
Python
Cách 3
C++
Subtask 1 (30%)$N, Q \le 1000$1–66/66/62/62/66/6
Subtask 2 (30%)mọi $x_k$ là lũy thừa của 27–122/60/66/60/66/6
Subtask 3 (40%)không ràng buộc thêm13–204/81/80/80/88/8

Ý tưởngTách gcd thành các lũy thừa nguyên tố

Nhận xét 1. Nếu $x = \prod p^{e_p}$ thì $\gcd(A, x)$ $= \prod p^{\min(e_p(A),\, e_p)}$, với $e_p(A)$ là số mũ của $p$ trong $A$. Nhân trên cả đường đi:

$$S = \prod_{p \mid x} p^{\,\sum_t \min(e_p(A_t),\, e_p)}.$$

Nhận xét 2 – mẹo đếm. $\min(e_p(A), e)$ bằng số các $k \in [1, e]$ mà $p^k \mid A$. Ví dụ $A = 12 = 2^2 \cdot 3$, $e = 3$: $2^1 \mid 12$, $2^2 \mid 12$, $2^3 \nmid 12$ → đếm được 2 = $\min(2, 3)$. Đổi thứ tự tổng:

Công thức
Số mũ của $p$ trong $S$ $= \displaystyle\sum_{k=1}^{e_p} \text{cnt}_{p^k}(u, v)$, với $\text{cnt}_{q}(u, v)$ = số đỉnh trên đường đi $u$–$v$ có $A_t$ chia hết cho $q$.

Nhận xét 3 – đếm trên đường đi. Gọi $f_q(w)$ là số đỉnh trên đường từ gốc tới $w$ có $q \mid A$. Với $l = \text{LCA}(u, v)$ và $\text{par}(l)$ là cha của $l$:

$$\text{cnt}_q(u, v) = f_q(u) + f_q(v) - f_q(l) - f_q(\text{par}(l)).$$

Vì $x \le 10^7$ nên $x$ có không quá 8 ước nguyên tố khác nhau và tổng các số mũ không quá 23 – mỗi truy vấn chỉ cần vài chục giá trị $q = p^k$.

Em sửa cây (danh sách $A$ và cha của các đỉnh 2..n), truy vấn rồi bấm Tiếp để xem cách đếm:

Ví dụ nhanh:

Cách 1Đi trên đường đi, nhân trực tiếp (subtask 1)

BFS từ gốc 1 để có cha và độ sâu. Với mỗi truy vấn, đỉnh nào sâu hơn thì nhảy lên cha, nhân $\gcd$ của đỉnh đó vào kết quả – tới khi hai đỉnh gặp nhau ở LCA thì nhân nốt đỉnh LCA.

C++▶ Chạy tay từng dòng codeCách 1: đỉnh sâu hơn nhảy lên cha, nhân gcd từng đỉnh trên đường đi.
Dữ liệu mẫu:
Không gian$O(N)$
Thời gian$O(Q \cdot N \log A)$ trường hợp xấu

Kết quả chạy thật: Bản C++ đúng 12/20 test, quá thời gian test 8–10, 12, 15–17, 19, chạy lâu nhất 4.006s. Bản Python đúng 7/20 test, quá thời gian test 7–17, 19–20, chạy lâu nhất 3.943s. Cách này đúng với mọi test nhưng mỗi truy vấn tốn $O(\text{độ dài đường đi})$: trên cây nông (sao, ngẫu nhiên) vẫn chạy kịp cả với $N = Q = 10^5$, còn trên cây dạng đường thẳng thì tới $10^5 \cdot 10^5$ phép – quá thời gian.

Cách 2Khi x là lũy thừa của 2: đếm tiền tố trên cây (subtask 2)

Chỉ có một số nguyên tố $p = 2$. Đặt $v_2(A)$ là số mũ của 2 trong $A$ ($\le 23$ vì $A \le 10^7 < 2^{24}$). Theo nhận xét 2, số mũ của 2 trong đáp án là $\sum_{k=1}^{e} \text{cnt}_k(u, v)$ với $\text{cnt}_k$ = số đỉnh trên đường đi có $v_2(A_t) \ge k$.

Ta tính sẵn pre[w][k] = số đỉnh trên đường gốc → $w$ có $v_2 \ge k$ (24 giá trị cho mỗi đỉnh, tính khi BFS: pre[w][k] = pre[cha][k] + (v2[w] >= k)), và tìm LCA bằng nhảy nhị phân (binary lifting). Mỗi truy vấn chỉ còn $O(\log N + 23)$.

C++▶ Chạy tay từng dòng codeCách 2 (x = 2^e): pre[w][k] đếm số đỉnh có v2 ≥ k trên đường gốc → w, ghép bằng LCA.
Dữ liệu mẫu:
Bảng nhảy nhị phân up[][] và hàm lca được tính gọn trong một bước để khung chạy tay không quá dài.
  1. up[w][j] là tổ tiên thứ $2^j$ của $w$; tính ngay khi BFS gặp $w$ vì cha của $w$ đã có đủ bảng.
  2. pre[0] toàn 0 đóng vai trò "cha của gốc", nên khi LCA là gốc thì pl = 0 vẫn đúng.
  3. Kết quả là $2^E$ với $E$ có thể tới $23 \cdot 10^5$ – dùng lũy thừa nhanh power theo modulo.
Không gian$O(N (\log N + 24))$
Thời gian$O((N + Q)(\log N + 24))$

Kết quả chạy thật: Bản C++ đúng 8/20 test, sai test 2–5, 13–20, chạy lâu nhất 0.348s. Bản Python (chậm vì vòng lặp Python, chỉ qua test nhỏ) đúng 2/20 test, sai test 2–5, quá thời gian test 7–20, chạy lâu nhất 2.997s. Cách 2 chỉ đúng khi $x$ là lũy thừa của 2 nên ăn trọn subtask 2 (và các test của subtask 1 mà $x$ là lũy thừa của 2). Nếu ghép thêm cách 1 cho $N, Q \le 1000$ thì được 60 điểm.

Cách 3Mọi lũy thừa nguyên tố + DFS offline (full điểm)

Tổng quát hoá cách 2 cho mọi $p$: cần $f_q(w)$ với mọi lũy thừa nguyên tố $q = p^k \le 10^7$ – không thể lưu bảng cho từng đỉnh. Ta làm offline:

  1. Sàng ước nguyên tố nhỏ nhất tới $10^7$ để phân tích nhanh $A_i$ và $x_k$.
  2. Với mỗi truy vấn, tìm $l = \text{LCA}(u, v)$ và gắn truy vấn vào 4 đỉnh: $u$ (dấu +), $v$ (+), $l$ (−), $\text{par}(l)$ (−).
  3. DFS từ gốc, duy trì mảng cnt[q]: vào đỉnh $w$ thì cnt[q]++ với mọi lũy thừa nguyên tố $q \mid A_w$, ra khỏi $w$ thì cnt[q]--. Như vậy khi đang đứng ở $w$, cnt[q] đúng bằng $f_q(w)$.
  4. Đứng ở đỉnh $w$, với mỗi truy vấn gắn vào $w$ (dấu $s$), cộng $s \cdot \text{cnt}[p^j]$ ($j = 1..e_p$) vào số mũ của $p$ trong đáp án truy vấn đó.
  5. Cuối cùng $S_k = \prod p^{\text{số mũ}} \bmod (10^9+7)$ bằng lũy thừa nhanh.
C++▶ Chạy tay từng dòng codeCách 3: gắn truy vấn vào u, v, lca, cha(lca); DFS duy trì cnt[p^k] trên đường gốc → đỉnh.
Dữ liệu mẫu:
Bỏ qua phần sàng 10^7 và các bước bên trong hàm lca/primePowers để khung gọn. Nhãn dưới đỉnh: "+q0" nghĩa là truy vấn 0 gắn vào đỉnh này với dấu +.
  1. DFS viết không đệ quy (dùng mảng it[u] là vị trí kề tiếp theo cần thăm) vì cây đường thẳng sâu tới $10^5$, đệ quy dễ tràn ngăn xếp.
  2. Các cặp $(p, e)$ của mọi truy vấn lưu liên tiếp trong hai mảng P, E (truy vấn $k$ chiếm đoạn st[k]..st[k+1]-1) thay vì vector lồng nhau – đỡ cấp phát bộ nhớ, chạy nhanh hơn rõ.
  3. Bộ nhớ: hai mảng $10^7$ số int (spf, cnt) khoảng 80 MB, trong giới hạn 1 GB của đề.
  4. Đọc dữ liệu bằng fread vì có tới khoảng $6 \cdot 10^5$ số.
Không gian$O(M + N + Q)$, $M = 10^7$
Thời gian$O(M \log\log M + (N + Q)\log M + Q \log N)$

Kết quả chạy thật: bản C++ đúng 20/20 test, chạy lâu nhất 0.700s – full điểm. Phần sàng tới $10^7$ chiếm khoảng 0,15–0,2 giây. Bản mình nộp lên tinhoctre.vn dùng sàng tuyến tính, LCA Tarjan offline và mảng phẳng (CSR) thay cho vector lồng nhau nên chỉ khoảng 0,3 giây trên máy mình. Cách 3 mình không viết bản Python: riêng việc sàng $10^7$ phần tử bằng Python đã mất vài giây.

Tổng kếtKết quả chạy thật trên bộ test

CáchNgôn ngữĐúngLâu nhấtGhi chú
1. Đi trên đường điC++12/204.006s30 điểm (theo subtask)
1. Đi trên đường điPython7/203.943s30 điểm (theo subtask)
2. x = 2^e, đếm tiền tốC++8/200.348s30 điểm (theo subtask)
2. x = 2^e, đếm tiền tốPython2/202.997s0 điểm (theo subtask)
3. Lũy thừa nguyên tố + DFS offlineC++20/200.700s100 điểm (theo subtask)
Ghi chú của mình: thời gian là thời gian CPU trên máy mình (máy dùng chung đang khá tải), giới hạn 1 giây; điểm tính theo subtask. Ba kỹ thuật đáng nhớ: (1) $\min(e_p(A), e)$ $= \#\{k \le e : p^k \mid A\}$ – biến min thành phép đếm; (2) đếm trên đường đi bằng $f(u) + f(v) - f(\text{lca}) - f(\text{par(lca)})$; (3) khi không lưu nổi $f_q$ cho mọi đỉnh, hãy gắn truy vấn vào đỉnh rồi DFS một lần, vào thì cộng ra thì trừ. Lời giải chính thức đã đạt 100/100 trên tinhoctre.vn.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇