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
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Bài 44: Kỹ thuật trên cây trong C++ và Python: Euler tour kết hợp Fenwick, gộp tập nhỏ vào lớn (small-to-large) và DSU on tree

Bài 44: Kỹ thuật trên cây trong C++ và Python: Euler tour kết hợp Fenwick, gộp tập nhỏ vào lớn (small-to-large) và DSU on tree
Series: Thuật toán chuyên sâu Độ khó: Khá Bài 44 / Thuật toán chuyên sâu Euler tour Small-to-large Cây
// Thuật toán chuyên sâu · Bài 44
Euler tour và gộp tập nhỏ vào lớn: trả lời mọi câu hỏi về cây con
Duỗi cây thành một mảng để cây con thành một đoạn liên tiếp, rồi giao cho Fenwick. Còn khi mỗi cây con cần cả một tập hợp, hãy luôn đổ tập nhỏ vào tập lớn: một mẹo hai dòng mà biến $O(n^2)$ thành $O(n \log n)$.

Lời mở đầu

Ở bài 33 em đã tính kích thước cây con, quy hoạch động trên cây bằng một lần DFS. Ở bài 34 em gặp mảng tin/tout để kiểm tra tổ tiên trong $O(1)$. Nhưng đề thi HSG còn thích hai kiểu câu hỏi khó hơn: (1) cây thay đổi liên tục (sửa giá trị một đỉnh, cộng thêm cho cả một phòng ban…) xen kẽ với câu hỏi “tổng của cả cây con là bao nhiêu?”; (2) mỗi cây con phải trả lời một câu hỏi về cả tập hợp các giá trị bên trong: có bao nhiêu màu khác nhau, màu nào xuất hiện nhiều nhất, có bao nhiêu cặp đỉnh cách nhau đúng $K$…

Hôm nay ta học hai kỹ thuật “đinh” cho hai kiểu đó. Euler tour duỗi cây thành một mảng, cây con nào cũng thành một đoạn liên tiếp, và thế là Fenwick, Segment Tree (bài 31), lazy (bài 37) dùng được ngay trên cây. Gộp tập nhỏ vào lớn (small-to-large, còn gọi là DSU on tree) là phiên bản trên cây của ý tưởng “hợp theo kích thước” của DSU (bài 30): luôn đổ tập nhỏ hơn vào tập lớn hơn, nhờ đó mỗi phần tử chỉ bị chép $O(\log n)$ lần.

Trong bài này bạn sẽ học: Euler tour: đánh số tin, tout, mảng ord để biến mỗi cây con thành đoạn liên tiếp; dùng Fenwick trên Euler tour để sửa một đỉnh và hỏi tổng cây con, cộng cho cả cây con, hỏi tổng trên đường gốc – đỉnh; bài toán đếm số màu khác nhau trong mỗi cây con và vì sao gộp ngây thơ tốn $O(n^2)$; kỹ thuật gộp tập nhỏ vào lớn với chứng minh $O(n \log^2 n)$ và mẹo swap trong $O(1)$; biến thể sack (DSU on tree với con nặng) chạy $O(n \log n)$ và chuyện đệ quy sâu. Có 2 khung chạy tay, 3 mô phỏng từng bước và 8 bài tập kèm gợi ý.

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

Phần 1Euler tour: biến cây con thành đoạn liên tiếp

Nhắc lại từ bài 34: DFS từ gốc với một bộ đếm timer. Khi vào đỉnh $u$, tăng bộ đếm và ghi tin[u] = timer; khi rời $u$ (đã đi hết cây con), ghi tout[u] = timer. Hôm nay ta thêm một mảng nữa: ord[t] = đỉnh được thăm thứ $t$, tức là ord[tin[u]] = u. Mảng ord chính là cái cây đã được duỗi thẳng ra.

Điều quan trọng nhất: DFS vào $u$ rồi đi hết cây con của $u$ mới rời $u$, nên trong khoảng thời gian đó nó chỉ thăm các đỉnh của cây con $u$, và thăm tất cả chúng. Vì vậy:

Cây con của $u$ = các đỉnh ord[tin[u]], ord[tin[u] + 1], …, ord[tout[u]]  ·  kích thước = tout[u] − tin[u] + 1
1[1, 10]2[2, 6]3[7, 10]4[3, 3]5[4, 6]6[8, 8]7[9, 10]8[5, 5]9[6, 6]10[10, 10]

Hình 1. Cây ví dụ dùng xuyên suốt bài (gốc 1, DFS thăm con theo số tăng dần). Dưới mỗi đỉnh là đoạn $[\text{tin}, \text{tout}]$. Cây con của 2 (vàng và xanh lá) là đoạn [2, 6].

t12345678910
ord[t]12458936710
a[ord[t]]5371282469

Bảng 1. Mảng ord của cây Hình 1 và giá trị a của từng đỉnh xếp theo ord. Các ô tô màu là cây con của 2: {2, 4, 5, 8, 9}, nằm liền nhau từ ô 2 tới ô 6. Tổng cây con của 2 bằng tổng đoạn này: 21.

Em bấm Tiếp để xem bộ đếm chạy và mỗi khi DFS rời một đỉnh, cả cây con của đỉnh đó sáng lên thành một đoạn liền trên mảng ord. Thử đổi dãy cha để thấy tính chất này đúng với mọi cây:

MÔ PHỎNGEuler tour: DFS đánh số tin, tout và cây con thành đoạnNhập dãy cha của các đỉnh 2, 3, …, n (n ≤ 16). Mỗi lần rời một đỉnh, đoạn [tin, tout] của nó sáng lên.
vừa vàođang trên ngăn xếp DFScây con vừa xong = đoạn liên tiếpđã rời
Khuôn mẫu Euler tour
void dfs(int u, int p){
    tin[u] = ++timer_; ord_[timer_] = u;     // vào u
    for(int v : adj[u]) if(v != p) dfs(v, u);
    tout[u] = timer_;                        // rời u
}
// cây con u  <=>  đoạn [tin[u], tout[u]] trên mảng ord_
// v thuộc cây con u  <=>  tin[u] <= tin[v] <= tout[u]
Mọi bài “truy vấn trên cây con” đều bắt đầu bằng 4 dòng này. Phần còn lại là chọn cấu trúc dữ liệu cho bài toán trên đoạn.
Ba kiểu “Euler tour” hay bị nhầm
(1) Chỉ ghi lúc vào: $n$ phần tử, dùng cho truy vấn cây con (bài hôm nay). (2) Ghi cả lúc vào và lúc ra: $2n$ phần tử, mỗi đỉnh xuất hiện hai lần, đôi khi dùng cho truy vấn đường đi. (3) Ghi mỗi khi đi qua một đỉnh (cả lúc quay về từ con): $2n - 1$ phần tử, dùng cho LCA bằng RMQ ở bài 34. Đọc lời giải người khác, em để ý xem họ dùng kiểu nào.

Phần 2Fenwick trên Euler tour: sửa đỉnh, hỏi tổng cây con

Bài toán. Cây $n$ đỉnh gốc 1, đỉnh $u$ có giá trị $a_u$. Có $q$ truy vấn ($n, q \le 2 \cdot 10^5$): 1 u x gán $a_u = x$; 2 u in tổng giá trị các đỉnh trong cây con của $u$. Duyệt cây con mỗi lần tốn $O(n)$, tổng $O(nq)$, quá chậm.

Cách làm. Đặt giá trị của đỉnh $u$ vào ô tin[u] của một mảng $b$: $b[\text{tin}[u]] = a_u$. Khi đó:

  • sửa giá trị đỉnh $u$ = sửa một ô $b[\text{tin}[u]]$;
  • tổng cây con của $u$ = tổng đoạn $b[\text{tin}[u] .. \text{tout}[u]]$ = $\text{sum}(\text{tout}[u]) - \text{sum}(\text{tin}[u] - 1)$.

Đúng bài toán “sửa một phần tử, hỏi tổng đoạn” của bài 31. Một cây Fenwick giải mỗi thao tác trong $O(\log n)$. Ví dụ với Bảng 1: cây con của 2 là đoạn [2, 6], tổng $3 + 7 + 1 + 2 + 8 = 21$. Gán $a_5 = 10$ (tăng 9) thì chỉ ô $b[\text{tin}[5]] = b[4]$ đổi, và tổng cây con của 2 thành 30, của gốc thành 56.

Xem code C++ / Python: euler tour + fenwick: gán giá trị đỉnh, tổng cây con (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, tin[MAXN], tout[MAXN], timer_ = 0;
long long a[MAXN], bit[MAXN];
vector<int> adj[MAXN];

void add(int i, long long v){ for(; i <= n; i += i & -i) bit[i] += v; }
long long sum(int i){ long long s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> par(n + 1, 0), it(n + 1, 0), st = {1};   // DFS không đệ quy
    tin[1] = ++timer_;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; tin[v] = ++timer_;               // vào v
            st.push_back(v);
        } else {
            tout[u] = timer_;                            // rời u
            st.pop_back();
        }
    }
    for(int u = 1; u <= n; u++) add(tin[u], a[u]);      // giá trị của u nằm ở ô tin[u]
    while(q--){
        int t, u; cin >> t >> u;
        if(t == 1){
            long long x; cin >> x;
            add(tin[u], x - a[u]); a[u] = x;             // gán a[u] = x
        } else {
            cout << sum(tout[u]) - sum(tin[u] - 1) << "\n";   // tổng đoạn [tin, tout]
        }
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [0] + [int(x) for x in data[2:2 + n]]
    pos = 2 + n
    adj = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    tin = [0] * (n + 1); tout = [0] * (n + 1); par = [0] * (n + 1); it = [0] * (n + 1)
    timer = 1; tin[1] = 1; st = [1]                   # DFS không đệ quy
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v == par[u]:
                continue
            par[v] = u; timer += 1; tin[v] = timer      # vào v
            st.append(v)
        else:
            tout[u] = timer                             # rời u
            st.pop()
    bit = [0] * (n + 1)

    def add(i, v):
        while i <= n:
            bit[i] += v; i += i & -i

    def pref(i):
        s = 0
        while i > 0:
            s += bit[i]; i -= i & -i
        return s

    for u in range(1, n + 1):
        add(tin[u], a[u])                               # giá trị của u nằm ở ô tin[u]
    out = []
    for _ in range(q):
        t, u = int(data[pos]), int(data[pos + 1]); pos += 2
        if t == 1:
            x = int(data[pos]); pos += 1
            add(tin[u], x - a[u]); a[u] = x             # gán a[u] = x
        else:
            out.append(pref(tout[u]) - pref(tin[u] - 1))
    print('\n'.join(map(str, out)))

main()

Khung chạy tay dưới đây dùng DFS đệ quy cho dễ đọc. Em bấm Tự chạy để thấy tin, tout được ghi, rồi mỗi truy vấn cây con chỉ còn là hai lần tính tổng tiền tố trên Fenwick:

C++▶ Chạy tay từng dòng codeDFS ghi tin/tout, đặt a[u] vào ô tin[u] của Fenwick, rồi tổng cây con = sum(tout) − sum(tin − 1) (cột vàng: đỉnh đang thăm / ô bit đang xét).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int n, q, timer_ = 0, tin[105], tout[105];
5long long a[105], bit[105];
6vector<int> adj[105];
7
8void dfs(int u, int p){
9 tin[u] = ++timer_;
10 for(int v : adj[u])
11 if(v != p) dfs(v, u);
12 tout[u] = timer_;
13}
14void add(int i, long long v){
15 for(; i <= n; i += i & -i) bit[i] += v;
16}
17long long sum(int i){
18 long long s = 0;
19 for(; i > 0; i -= i & -i) s += bit[i];
20 return s;
21}
22
23int main(){
24 cin >> n >> q;
25 for(int i = 1; i <= n; i++) cin >> a[i];
26 for(int i = 1; i < n; i++){
27 int u, v; cin >> u >> v;
28 adj[u].push_back(v); adj[v].push_back(u);
29 }
30 dfs(1, 0);
31 for(int u = 1; u <= n; u++) add(tin[u], a[u]);
32 while(q--){
33 int t, u; cin >> t >> u;
34 if(t == 1){ long long x; cin >> x; add(tin[u], x - a[u]); a[u] = x; }
35 else cout << sum(tout[u]) - sum(tin[u] - 1) << "\n";
36 }
37 return 0;
38}

Để nhìn rõ hơn vì sao chỉ cần hai lần tính tổng tiền tố, em dùng mô phỏng sau: đỉnh $u$ cần hỏi tô vàng, cả cây con tô xanh lá, các ô bit được cộng lần lượt sáng lên. Thử đổi $u$ thành 3, 5 hoặc 1:

MÔ PHỎNGTổng cây con bằng Fenwick trên Euler tourNhập dãy cha, giá trị các đỉnh và đỉnh u cần hỏi. Xem tổng cây con quy về hai lần tính tổng tiền tố.
đỉnh u / ô bit đang cộngđoạn [tin, tout]cây con / đã cộng

Ba biến thể hay gặp. Cùng một mảng Euler tour, chỉ đổi cách dùng Fenwick là trả lời được những truy vấn khác:

Cập nhậtTruy vấnLàm trên FenwickVì sao đúng
$a_u$ += x (một đỉnh)tổng cây con $u$add(tin[u], x); hỏi sum(tout) − sum(tin − 1)cây con = đoạn
cộng x cho cả cây con $u$giá trị đỉnh $v$add(tin[u], x), add(tout[u] + 1, −x); hỏi sum(tin[v])mảng hiệu: cộng đoạn, hỏi một điểm
$a_u$ += x (một đỉnh)tổng trên đường gốc → $v$add(tin[u], x), add(tout[u] + 1, −x); hỏi sum(tin[v])$u$ nằm trên đường gốc → $v$ ⇔ $v$ thuộc cây con $u$
cộng x cho cả cây contổng cây conSegment Tree lazy (bài 37) trên đoạn [tin, tout]cộng đoạn, hỏi tổng đoạn

Bảng 2. Các dạng truy vấn cây con / đường đi về gốc và cách quy về bài toán trên mảng.

Dòng thứ ba đáng để em suy nghĩ kỹ, vì nó “lật ngược” vai trò: đỉnh $u$ có mặt trên đường từ gốc tới $v$ khi và chỉ khi $u$ là tổ tiên của $v$, tức là $v$ nằm trong cây con của $u$, tức là $\text{tin}[v] \in [\text{tin}[u], \text{tout}[u]]$. Vậy “thêm $x$ vào đỉnh $u$” tương đương “cộng $x$ cho mọi ô thuộc đoạn $[\text{tin}[u], \text{tout}[u]]$”, còn “tổng trên đường gốc → $v$” là giá trị tại một điểm $\text{tin}[v]$. Kết hợp với LCA (bài 34), tổng trên đường đi $u$ – $v$ bất kỳ là $S(u) + S(v) - S(l) - S(\text{cha}(l))$ với $l = \text{LCA}(u, v)$ và $S(x)$ là tổng trên đường gốc → $x$.

Lỗi hay gặp
1. Fenwick đánh số theo tin, không theo số hiệu đỉnh: viết nhầm add(u, x) thay vì add(tin[u], x) là lỗi kinh điển, test nhỏ có khi vẫn đúng vì tin trùng số hiệu. 2. add(tout[u] + 1, −x) có thể chạm ô $n + 1$: hàm add với điều kiện i <= n tự bỏ qua, nhưng nếu em dùng mảng hiệu thường thì phải khai báo dư một ô. 3. Tổng có thể vượt $2^{31}$: dùng long long.

Phần 3Đếm số màu khác nhau trong mỗi cây con

Bài toán. Cây $n$ đỉnh gốc 1 ($n \le 2 \cdot 10^5$), đỉnh $u$ có màu $c_u$. Với mọi đỉnh $u$, đếm xem cây con của $u$ có bao nhiêu màu khác nhau. Với cây Hình 1 và màu $c = (1, 2, 1, 3, 2, 3, 1, 2, 1, 3)$ của các đỉnh 1..10, đáp án là $3, 3, 2, 1, 2, 1, 2, 1, 1, 1$.

Tổng thì cộng dồn được (tổng cây con = $a_u$ + tổng các cây con của con), còn “số màu khác nhau” thì không: cây con 2 có 3 màu, cây con 3 có 2 màu, nhưng gốc không có $1 + 3 + 2$ màu mà chỉ có 3, vì hai bên có thể trùng màu. Muốn ghép, ta phải biết cụ thể tập màu của từng cây con.

Cách 1: DFS riêng cho từng đỉnh. Với mỗi $u$, duyệt cả cây con, bỏ màu vào một set. Tổng công việc = tổng kích thước các cây con = tổng độ sâu, với cây đường thẳng là $1 + 2 + \cdots + n \approx n^2 / 2 = 2 \cdot 10^{10}$. Quá chậm.

Cách 2: gộp ngây thơ từ dưới lên. Duyệt từ lá lên gốc, S[u] = {c[u]} rồi chép hết S[con] vào S[u]. Nghe hợp lý, nhưng trên cây đường thẳng 1 – 2 – … – n, tập của đỉnh $n$ có 1 phần tử được chép lên $n - 1$, tập có 2 phần tử được chép lên $n - 2$… Tổng số lần chép vẫn là $\frac{n(n - 1)}{2}$, nhân thêm $\log n$ cho mỗi lần chèn vào set. Cái dở nằm ở chỗ: tập to của con cứ bị chép đi chép lại sang tập bé xíu của cha.

ngây thơ1chép 52chép 43chép 34chép 25chép 16tổng: 15nhỏ vào lớn1chép 12chép 13chép 14chép 15chép 16tổng: 5

Hình 2. Cây đường thẳng 6 đỉnh, gộp từ đỉnh 6 lên đỉnh 1. Gộp ngây thơ chép tập của con (5, 4, 3, 2, 1 phần tử) sang cha. Nếu thay vào đó ta giữ lại tập to của con và chỉ chép 1 màu của cha sang, mỗi bước chỉ tốn 1.

Còn một cách nữa: Euler tour + Fenwick offline
Nhờ phần 1, “số màu khác nhau trong cây con $u$” = “số giá trị khác nhau trên đoạn $[\text{tin}[u], \text{tout}[u]]$ của mảng $c[\text{ord}[t]]$”. Đây là bài kinh điển trên mảng: sắp các truy vấn theo đầu phải, duyệt mảng từ trái sang, mỗi màu chỉ “bật” ở vị trí xuất hiện gần nhất trên Fenwick, đáp án là tổng đoạn. Cũng $O(n \log n)$, nhưng chỉ làm được với những câu hỏi có cấu trúc đặc biệt như vậy. Gộp nhỏ vào lớn ở phần 4 thì tổng quát hơn nhiều: đếm màu, màu nhiều nhất, cặp đỉnh cách nhau $K$… đều cùng một khuôn.

Phần 4Gộp tập nhỏ vào lớn (small-to-large)

Sửa cách 2 đúng một dòng: khi gộp hai tập, luôn chép tập nhỏ hơn vào tập lớn hơn. Nếu tập của con lớn hơn tập của cha, ta đổi chỗ hai tập trước rồi mới chép. Điều kỳ diệu là việc đổi chỗ không tốn gì: swap(S[u], S[v]) giữa hai std::set chỉ đổi con trỏ bên trong, chạy $O(1)$ (trong Python thì chỉ đổi hai tham chiếu).

Chứng minh $O(n \log n)$ lần chép. Theo dõi một phần tử $x$ bất kỳ. Mỗi lần $x$ bị chép, nó đi từ tập nhỏ (cỡ $s$) sang tập lớn (cỡ $\ge s$), nên tập chứa $x$ sau đó có cỡ $\ge 2s$: ít nhất gấp đôi. Một tập không thể lớn hơn $n$, nên $x$ bị chép không quá $\log_2 n$ lần. Có $n$ phần tử, tổng số lần chép $\le n \log_2 n$. Mỗi lần chép là một lần chèn vào set tốn $O(\log n)$, vậy tổng cộng $O(n \log^2 n)$. Với unordered_set hoặc set của Python (bảng băm) thì còn $O(n \log n)$ trung bình.

Lập luận này giống hệt lập luận “hợp theo kích thước” của DSU ở bài 30. Em xem mô phỏng: mỗi đỉnh mang một phần tử riêng, số dưới mỗi đỉnh đếm số lần phần tử đó bị chép. Thử nhập cây đường thẳng 1 2 3 4 5 6 7 8 9 10 để so tổng số lần chép với gộp ngây thơ:

MÔ PHỎNGGộp tập nhỏ vào tập lớn: mỗi phần tử bị chép bao nhiêu lần?Nhập dãy cha (n ≤ 16). Thử cây hình đường thẳng “1 2 3 4 5 6 7 8 9 10” để thấy gộp ngây thơ tệ thế nào.
đỉnh u đang gộpcon v / tập nhỏphần tử bị chéptập đã gộp xong

Cài đặt không đệ quy. Để tránh đệ quy sâu, ta BFS lấy thứ tự các đỉnh (cha luôn đứng trước con), rồi duyệt ngược thứ tự đó: tới lượt $u$ thì mọi con của $u$ đã được gộp vào S[u], ta ghi đáp án ans[u] = |S[u]| rồi gộp S[u] lên cha theo quy tắc nhỏ vào lớn.

Xem code C++ / Python: đếm số màu khác nhau trong mỗi cây con bằng gộp nhỏ vào lớn (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, c[MAXN], par[MAXN], ans[MAXN];
vector<int> adj[MAXN];
set<int> S[MAXN];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> c[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};                       // BFS: cha đứng trước con
    vector<bool> seen(n + 1, false); seen[1] = true;
    for(int i = 0; i < (int)order.size(); i++)
        for(int v : adj[order[i]]) if(!seen[v]){
            seen[v] = true; par[v] = order[i]; order.push_back(v);
        }
    for(int u = 1; u <= n; u++) S[u].insert(c[u]);
    for(int i = n - 1; i >= 0; i--){               // con được xử lý trước cha
        int u = order[i];
        ans[u] = S[u].size();                      // mọi con của u đã gộp vào S[u]
        int p = par[u];
        if(p == 0) continue;
        if(S[u].size() > S[p].size()) swap(S[u], S[p]);   // O(1): chỉ đổi “ruột” hai set
        for(int x : S[u]) S[p].insert(x);          // đổ tập NHỎ vào tập LỚN
        S[u].clear();
    }
    for(int u = 1; u <= n; u++) cout << ans[u] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    c = [0] + [int(x) for x in data[1:1 + n]]
    adj = [[] for _ in range(n + 1)]
    pos = 1 + n
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    par = [0] * (n + 1); seen = [False] * (n + 1); seen[1] = True
    order = [1]                                   # BFS: cha đứng trước con
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; order.append(v)
    S = [None] + [{c[u]} for u in range(1, n + 1)]
    ans = [0] * (n + 1)
    for u in reversed(order):                     # con được xử lý trước cha
        ans[u] = len(S[u])
        p = par[u]
        if p == 0:
            continue
        small, big = S[u], S[p]
        if len(small) > len(big):
            small, big = big, small               # chỉ đổi tham chiếu, O(1)
        big.update(small)                         # đổ tập NHỎ vào tập LỚN
        S[p] = big; S[u] = None
    print(*ans[1:])

main()

Bản đệ quy thì gọn và dễ hiểu hơn, em xem từng dòng trong khung chạy tay. Để ý dòng swap: có lúc tập của con to hơn và hai tập được đổi chỗ, sau đó chỉ vài màu được chép:

C++▶ Chạy tay từng dòng codedfs(u): S[u] = {c[u]}, gộp từng cây con theo quy tắc nhỏ vào lớn, ans[u] = |S[u]| (cột vàng: u, cột đỏ: con v đang gộp).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int n, c[105], ans[105];
5vector<int> adj[105];
6set<int> S[105];
7
8void dfs(int u, int p){
9 S[u].insert(c[u]);
10 for(int v : adj[u]){
11 if(v == p) continue;
12 dfs(v, u);
13 if(S[v].size() > S[u].size()) swap(S[u], S[v]);
14 for(int x : S[v]) S[u].insert(x);
15 S[v].clear();
16 }
17 ans[u] = S[u].size();
18}
19
20int main(){
21 cin >> n;
22 for(int i = 1; i <= n; i++) cin >> c[i];
23 for(int i = 1; i < n; i++){
24 int u, v; cin >> u >> v;
25 adj[u].push_back(v); adj[v].push_back(u);
26 }
27 dfs(1, 0);
28 for(int i = 1; i <= n; i++) cout << ans[i] << " ";
29 return 0;
30}
Khuôn mẫu gộp nhỏ vào lớn
// S[u] đã chứa thông tin của u, S[v] là thông tin cả cây con v
if(S[v].size() > S[u].size()) swap(S[u], S[v]);   // O(1)
for(auto x : S[v]){
    // (nếu cần) dùng x để cập nhật đáp án với S[u] TRƯỚC khi chèn
    S[u].insert(x);
}
S[v].clear();
Thay set bằng map<màu, số lần> là đếm được màu xuất hiện nhiều nhất (bài tập 5); thay bằng map<độ sâu, số đỉnh> là đếm được cặp đỉnh cách nhau $K$ (bài tập 8).
Ba cái bẫy
1. Chép thay vì swap: viết S[u] = S[v] là sao chép cả tập, tốn $O(|S[v]|)$, mất hết tác dụng. Phải dùng swap (hoặc mảng con trỏ / chỉ số). 2. Ghi đáp án quá muộn: sau khi gộp lên cha, S[v] bị xoá hoặc bị đổi chỗ, nên đáp án của $v$ phải ghi trước khi gộp. Vì thế bài toán thường phải giải offline (đọc hết rồi mới trả lời). 3. So sánh sai thứ: phải so kích thước tập, không phải kích thước cây con hay số hiệu đỉnh. (So kích thước cây con cũng đúng độ phức tạp, nhưng so kích thước tập thì đơn giản và an toàn hơn.)

Phần 5Sack (DSU on tree) với con nặng: O(n log n)

Small-to-large dùng set/map nên có thêm một thừa số $\log$ và tốn bộ nhớ cấp phát. Khi màu nhỏ ($c_u \le n$), ta có thể dùng một mảng đếm toàn cục cnt[màu] cùng biến distinct (số màu đang có cnt > 0), và tổ chức thứ tự duyệt thật khéo. Đó là kỹ thuật sack (có nơi gọi là DSU on tree):

  • Gọi con nặng big[u] là con có cây con lớn nhất; các con còn lại là con nhẹ.
  • dfs(u, keep): (1) gọi đệ quy mọi con nhẹ với keep = false: tính xong đáp án của chúng rồi xoá sạch khỏi cnt; (2) gọi con nặng với keep = true: tính xong và giữ nguyên dữ liệu trong cnt; (3) thêm $u$ và toàn bộ các cây con nhẹ vào cnt, duyệt nhanh nhờ Euler tour ord[tin[v]..tout[v]]; (4) ghi ans[u] = distinct; (5) nếu keep = false thì xoá cả cây con $u$.

Vì sao $O(n \log n)$? Một đỉnh $x$ bị thêm lại (bước 3) mỗi khi nó nằm trong một cây con nhẹ của một tổ tiên. Đi từ $x$ lên gốc, mỗi lần đi qua một cạnh nhẹ (từ con nhẹ lên cha) thì kích thước cây con ít nhất gấp đôi (vì con nặng còn to hơn con nhẹ). Vậy có không quá $\log_2 n$ cạnh nhẹ trên đường lên gốc, và $x$ bị thêm/xoá $O(\log n)$ lần. Mỗi lần thêm/xoá là $O(1)$ trên mảng, tổng $O(n \log n)$. Lại là lập luận “gấp đôi” quen thuộc!

Đỉnh u12345678910
kích thước cây con10541312111
con nặng big[u]257–8–10–––

Bảng 3. Con nặng của từng đỉnh trong cây Hình 1 (hai con bằng nhau thì chọn con thăm trước). Ví dụ ở gốc 1: cây con 2 có 5 đỉnh, cây con 3 có 4 đỉnh nên con nặng là 2; khi tính gốc, chỉ phải thêm lại 4 đỉnh của cây con nhẹ.

Xem code C++ / Python: sack (dsu on tree) đếm số màu khác nhau (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, c[MAXN], sz[MAXN], big[MAXN], tin[MAXN], tout[MAXN], ord_[MAXN], timer_ = 0;
int cnt[MAXN], distinct_ = 0, ans[MAXN];
vector<int> adj[MAXN];

void prep(int u, int p){                    // sz, con nặng, tin/tout, ord
    sz[u] = 1; tin[u] = ++timer_; ord_[timer_] = u; big[u] = 0;
    for(int v : adj[u]) if(v != p){
        prep(v, u);
        sz[u] += sz[v];
        if(big[u] == 0 || sz[v] > sz[big[u]]) big[u] = v;
    }
    tout[u] = timer_;
}
void addV(int v){ if(cnt[c[v]]++ == 0) distinct_++; }
void delV(int v){ if(--cnt[c[v]] == 0) distinct_--; }

void dfs(int u, int p, bool keep){
    for(int v : adj[u]) if(v != p && v != big[u]) dfs(v, u, false);   // con nhẹ: làm xong rồi xoá
    if(big[u]) dfs(big[u], u, true);                                  // con nặng: giữ lại cnt
    addV(u);
    for(int v : adj[u]) if(v != p && v != big[u])
        for(int t = tin[v]; t <= tout[v]; t++) addV(ord_[t]);       // thêm lại các cây con nhẹ
    ans[u] = distinct_;
    if(!keep)
        for(int t = tin[u]; t <= tout[u]; t++) delV(ord_[t]);       // xoá sạch cây con u
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> c[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    prep(1, 0);
    dfs(1, 0, true);
    for(int u = 1; u <= n; u++) cout << ans[u] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    c = [0] + [int(x) for x in data[1:1 + n]]
    adj = [[] for _ in range(n + 1)]
    pos = 1 + n
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    # DFS không đệ quy: tin, tout, ord (thứ tự vào), cha
    par = [0] * (n + 1); tin = [0] * (n + 1); tout = [0] * (n + 1); it = [0] * (n + 1)
    order = [0, 1]; tin[1] = 1; st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; tin[v] = len(order); order.append(v); st.append(v)
        else:
            tout[u] = len(order) - 1; st.pop()
    sz = [1] * (n + 1); big = [0] * (n + 1)
    for t in range(n, 1, -1):                 # duyệt ngược thứ tự vào: con trước cha
        v = order[t]; p = par[v]
        sz[p] += sz[v]
        if big[p] == 0 or sz[v] > sz[big[p]]:
            big[p] = v
    cpos = [c[v] for v in order]              # cpos[t] = màu của đỉnh ord[t]
    cnt = [0] * (n + 1); ans = [0] * (n + 1); distinct = 0
    stack = [(1, True, False)]                # (đỉnh, keep, đã xử lý các con chưa)
    while stack:
        u, keep, done = stack.pop()
        if not done:
            stack.append((u, keep, True))
            if big[u]:
                stack.append((big[u], True, False))     # con nặng làm SAU các con nhẹ
            for v in adj[u]:
                if v != par[u] and v != big[u]:
                    stack.append((v, False, False))
            continue
        cols = [c[u]]
        for v in adj[u]:
            if v != par[u] and v != big[u]:
                cols += cpos[tin[v]:tout[v] + 1]     # cả cây con nhẹ v là một đoạn
        for col in cols:
            if cnt[col] == 0:
                distinct += 1
            cnt[col] += 1
        ans[u] = distinct
        if not keep:
            for col in cpos[tin[u]:tout[u] + 1]:
                cnt[col] -= 1
                if cnt[col] == 0:
                    distinct -= 1
    print(*ans[1:])

main()
Đệ quy sâu
Bản C++ ở trên viết đệ quy cho gần với ý tưởng. Với cây đường thẳng $2 \cdot 10^5$ đỉnh, độ sâu đệ quy cũng $2 \cdot 10^5$: trên Codeforces và đa số máy chấm Linux có ngăn xếp lớn thì chạy được, nhưng máy chấm Windows (ngăn xếp mặc định 1 MB) có thể tràn. Bản Python bắt buộc khử đệ quy: dùng một ngăn xếp các bộ (u, keep, đã_xử_lý_con), đẩy con nặng vào trước các con nhẹ để con nhẹ được lấy ra và làm xong trước. Khi không chắc về máy chấm, em làm giống bản Python, hoặc dùng cách gộp nhỏ vào lớn theo thứ tự BFS ngược như phần 4.
Cách làmThời gianBộ nhớƯu điểmDùng khi
DFS từng đỉnh$O(n^2)$$O(n)$dễ viết$n \le 5000$, để đối chiếu
Euler tour + Fenwick offline$O(n \log n)$$O(n)$không đệ quy, nhanhđếm giá trị khác nhau, tổng/đếm có điều kiện
Gộp nhỏ vào lớn (set/map)$O(n \log^2 n)$$O(n)$tổng quát nhất, giá trị tuỳ ýmàu lớn, thông tin phức tạp, đếm cặp
Sack (con nặng + cnt)$O(n \log n)$$O(n)$nhanh, mảng thườnggiá trị nhỏ ($\le n$) hoặc nén được

Bảng 4. So sánh các cách trả lời câu hỏi “tập hợp” trên mọi cây con.

Khi đi thi: gặp “với mỗi đỉnh, hãy tính … trên cây con của nó” mà thông tin không cộng dồn được, em nghĩ ngay tới gộp nhỏ vào lớn: code ngắn, ít lỗi, $n = 2 \cdot 10^5$ chạy thoải mái. Chỉ chuyển sang sack khi giới hạn thời gian thật chặt, hoặc khi thao tác “xoá” dễ hơn “gộp hai cấu trúc”.

Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Cây.

Bài tậpLuyện Euler tour và gộp tập nhỏ vào lớn

Bài 1Tổng lương phòng banDễ

Công ty có $n$ nhân viên tạo thành cây gốc 1 (giám đốc), nhân viên $i$ có lương $a_i$ ($n, q \le 2 \cdot 10^5$, $|a_i| \le 10^9$). Có $q$ thao tác: 1 u x: lương của $u$ tăng thêm $x$ ($|x| \le 10^9$); 2 u: in tổng lương của $u$ và mọi cấp dưới (trực tiếp hoặc gián tiếp) của $u$.

Input: dòng đầu $n, q$; dòng hai $a_1..a_n$; $n - 1$ dòng tiếp là các cạnh; $q$ dòng thao tác.

Input
10 5
5 3 2 7 1 4 6 2 8 9
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
2 2
2 3
1 5 9
2 2
2 1
Output
21
21
30
56

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đúng code phần 2, chỉ khác là “tăng thêm” nên gọi thẳng add(tin[u], x), không cần trừ giá trị cũ. Ở ví dụ, cây con của 2 gồm {2, 4, 5, 8, 9} có tổng 21; tăng lương nhân viên 5 thêm 9 thì tổng thành 30. Bản Python dựng Fenwick trong $O(n)$: cộng mỗi ô vào ô “cha” i + (i & −i) của nó.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, tin[MAXN], tout[MAXN];
long long bit[MAXN];
vector<int> adj[MAXN];

void add(int i, long long v){ for(; i <= n; i += i & -i) bit[i] += v; }
long long sum(int i){ long long s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    vector<long long> a(n + 1);
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> par(n + 1, 0), it(n + 1, 0), st = {1};
    int timer_ = 0; tin[1] = ++timer_;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; tin[v] = ++timer_; st.push_back(v);
        } else { tout[u] = timer_; st.pop_back(); }
    }
    for(int u = 1; u <= n; u++) add(tin[u], a[u]);
    while(q--){
        int t, u; cin >> t >> u;
        if(t == 1){ long long x; cin >> x; add(tin[u], x); }       // tăng a[u] thêm x
        else cout << sum(tout[u]) - sum(tin[u] - 1) << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [0] + [int(x) for x in data[2:2 + n]]
    pos = 2 + n
    adj = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    tin = [0] * (n + 1); tout = [0] * (n + 1); par = [0] * (n + 1); it = [0] * (n + 1)
    timer = 1; tin[1] = 1; st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; timer += 1; tin[v] = timer; st.append(v)
        else:
            tout[u] = timer; st.pop()
    bit = [0] * (n + 1)
    for u in range(1, n + 1):
        bit[tin[u]] += a[u]
    for i in range(1, n + 1):                 # dựng Fenwick trong O(n)
        j = i + (i & -i)
        if j <= n:
            bit[j] += bit[i]

    def pref(i):
        s = 0
        while i > 0:
            s += bit[i]; i -= i & -i
        return s

    out = []
    for _ in range(q):
        t, u = int(data[pos]), int(data[pos + 1]); pos += 2
        if t == 1:
            x = int(data[pos]); pos += 1
            i = tin[u]
            while i <= n:
                bit[i] += x; i += i & -i
        else:
            out.append(pref(tout[u]) - pref(tin[u] - 1))
    print('\n'.join(map(str, out)))

main()
Bài 2Thưởng cả nhánhDễ

Cây $n$ đỉnh gốc 1, đỉnh $i$ có giá trị ban đầu $a_i$ ($n, q \le 2 \cdot 10^5$). Thao tác 1 u x: cộng $x$ vào giá trị của mọi đỉnh trong cây con $u$; 2 u: in giá trị hiện tại của đỉnh $u$. ($|a_i|, |x| \le 10^9$.)

Input: như bài 1.

Input
10 5
5 3 2 7 1 4 6 2 8 9
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
1 2 10
1 5 -3
2 8
2 4
2 3
Output
9
17
2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Cộng $x$ cho cả cây con = cộng $x$ cho cả đoạn $[\text{tin}[u], \text{tout}[u]]$. Dùng mảng hiệu trên Fenwick (dòng 2 của Bảng 2): add(tin[u], x), add(tout[u] + 1, −x); giá trị của $v$ = $a_v$ + sum(tin[v]). Ví dụ: đỉnh 8 nằm trong cây con 2 và cây con 5 nên nhận $10 - 3$, giá trị $2 + 7 = 9$; đỉnh 4 chỉ thuộc cây con 2: $7 + 10 = 17$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, tin[MAXN], tout[MAXN];
long long bit[MAXN];
vector<int> adj[MAXN];

void add(int i, long long v){ for(; i <= n; i += i & -i) bit[i] += v; }
long long sum(int i){ long long s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    vector<long long> a(n + 1);
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> par(n + 1, 0), it(n + 1, 0), st = {1};
    int timer_ = 0; tin[1] = ++timer_;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; tin[v] = ++timer_; st.push_back(v);
        } else { tout[u] = timer_; st.pop_back(); }
    }
    while(q--){
        int t, u; cin >> t >> u;
        if(t == 1){
            long long x; cin >> x;
            add(tin[u], x); add(tout[u] + 1, -x);      // cộng x cho cả đoạn [tin, tout]
        } else cout << a[u] + sum(tin[u]) << "\n";      // giá trị = ban đầu + tổng mảng hiệu
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [0] + [int(x) for x in data[2:2 + n]]
    pos = 2 + n
    adj = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    tin = [0] * (n + 1); tout = [0] * (n + 1); par = [0] * (n + 1); it = [0] * (n + 1)
    timer = 1; tin[1] = 1; st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; timer += 1; tin[v] = timer; st.append(v)
        else:
            tout[u] = timer; st.pop()
    bit = [0] * (n + 2)

    def add(i, v):
        while i <= n:
            bit[i] += v; i += i & -i

    out = []
    for _ in range(q):
        t, u = int(data[pos]), int(data[pos + 1]); pos += 2
        if t == 1:
            x = int(data[pos]); pos += 1
            add(tin[u], x); add(tout[u] + 1, -x)    # cộng x cho cả đoạn [tin, tout]
        else:
            i, s = tin[u], 0
            while i > 0:
                s += bit[i]; i -= i & -i
            out.append(a[u] + s)
    print('\n'.join(map(str, out)))

main()
Bài 3Tổng trên đường về gốcTrung bình

Cây $n$ đỉnh gốc 1, đỉnh $i$ có giá trị $a_i$ ($n, q \le 2 \cdot 10^5$, $|a_i|, |x| \le 10^9$). Thao tác 1 u x: tăng $a_u$ thêm $x$; 2 u: in tổng giá trị các đỉnh trên đường đi từ gốc 1 tới $u$ (tính cả hai đầu).

Input: như bài 1.

Input
10 4
5 3 2 7 1 4 6 2 8 9
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
2 8
2 10
1 2 100
2 9
Output
11
22
117

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Dòng 3 của Bảng 2: giá trị của $u$ “ảnh hưởng” tới mọi đỉnh trong cây con của $u$ (vì $u$ nằm trên đường về gốc của chúng). Nên mỗi lần thêm $x$ vào $u$ ta cộng $x$ cho đoạn $[\text{tin}[u], \text{tout}[u]]$, và đáp án cho $v$ là giá trị tại điểm $\text{tin}[v]$ = sum(tin[v]). Giá trị ban đầu cũng nạp bằng đúng thao tác này. Ví dụ đường 1 → 2 → 5 → 8 có tổng $5 + 3 + 1 + 2 = 11$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, tin[MAXN], tout[MAXN];
long long bit[MAXN];
vector<int> adj[MAXN];

void add(int i, long long v){ for(; i <= n; i += i & -i) bit[i] += v; }
long long sum(int i){ long long s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }
void addVertex(int u, long long x){ add(tin[u], x); add(tout[u] + 1, -x); }   // u ảnh hưởng cả cây con

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    vector<long long> a(n + 1);
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> par(n + 1, 0), it(n + 1, 0), st = {1};
    int timer_ = 0; tin[1] = ++timer_;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; tin[v] = ++timer_; st.push_back(v);
        } else { tout[u] = timer_; st.pop_back(); }
    }
    for(int u = 1; u <= n; u++) addVertex(u, a[u]);
    while(q--){
        int t, u; cin >> t >> u;
        if(t == 1){ long long x; cin >> x; addVertex(u, x); }
        else cout << sum(tin[u]) << "\n";          // tổng các tổ tiên (kể cả u) của u
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = [0] + [int(x) for x in data[2:2 + n]]
    pos = 2 + n
    adj = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    tin = [0] * (n + 1); tout = [0] * (n + 1); par = [0] * (n + 1); it = [0] * (n + 1)
    timer = 1; tin[1] = 1; st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; timer += 1; tin[v] = timer; st.append(v)
        else:
            tout[u] = timer; st.pop()
    bit = [0] * (n + 2)

    def add(i, v):
        while i <= n:
            bit[i] += v; i += i & -i

    for u in range(1, n + 1):                 # u ảnh hưởng cả cây con của nó
        add(tin[u], a[u]); add(tout[u] + 1, -a[u])
    out = []
    for _ in range(q):
        t, u = int(data[pos]), int(data[pos + 1]); pos += 2
        if t == 1:
            x = int(data[pos]); pos += 1
            add(tin[u], x); add(tout[u] + 1, -x)
        else:
            i, s = tin[u], 0
            while i > 0:
                s += bit[i]; i -= i & -i
            out.append(s)                     # tổng các tổ tiên (kể cả u) của u
    print('\n'.join(map(str, out)))

main()
Bài 4Bộ sưu tập màuTrung bình

Cây $n$ đỉnh gốc 1 ($n \le 2 \cdot 10^5$), đỉnh $u$ có màu $c_u$ ($1 \le c_u \le 10^9$). Với mỗi đỉnh $u$, in số màu khác nhau trong cây con của $u$.

Input: dòng đầu $n$; dòng hai $c_1..c_n$; $n - 1$ dòng tiếp là các cạnh. Output: $n$ số trên một dòng.

Input
10
1 2 1 3 2 3 1 2 1 3
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
Output
3 3 2 1 2 1 2 1 1 1

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đúng code phần 4. Màu tới $10^9$ nên không dùng mảng đếm được, nhưng set thì không quan tâm giá trị lớn hay nhỏ. Ví dụ: cây con 2 có màu {2, 3, 2, 2, 1} → 3 màu; cây con 3 có màu {1, 3, 1, 3} → 2 màu.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, c[MAXN], par[MAXN], ans[MAXN];
vector<int> adj[MAXN];
set<int> S[MAXN];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> c[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};                       // BFS: cha đứng trước con
    vector<bool> seen(n + 1, false); seen[1] = true;
    for(int i = 0; i < (int)order.size(); i++)
        for(int v : adj[order[i]]) if(!seen[v]){
            seen[v] = true; par[v] = order[i]; order.push_back(v);
        }
    for(int u = 1; u <= n; u++) S[u].insert(c[u]);
    for(int i = n - 1; i >= 0; i--){               // con được xử lý trước cha
        int u = order[i];
        ans[u] = S[u].size();                      // mọi con của u đã gộp vào S[u]
        int p = par[u];
        if(p == 0) continue;
        if(S[u].size() > S[p].size()) swap(S[u], S[p]);   // O(1): chỉ đổi “ruột” hai set
        for(int x : S[u]) S[p].insert(x);          // đổ tập NHỎ vào tập LỚN
        S[u].clear();
    }
    for(int u = 1; u <= n; u++) cout << ans[u] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    c = [0] + [int(x) for x in data[1:1 + n]]
    adj = [[] for _ in range(n + 1)]
    pos = 1 + n
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    par = [0] * (n + 1); seen = [False] * (n + 1); seen[1] = True
    order = [1]                                   # BFS: cha đứng trước con
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; order.append(v)
    S = [None] + [{c[u]} for u in range(1, n + 1)]
    ans = [0] * (n + 1)
    for u in reversed(order):                     # con được xử lý trước cha
        ans[u] = len(S[u])
        p = par[u]
        if p == 0:
            continue
        small, big = S[u], S[p]
        if len(small) > len(big):
            small, big = big, small               # chỉ đổi tham chiếu, O(1)
        big.update(small)                         # đổ tập NHỎ vào tập LỚN
        S[p] = big; S[u] = None
    print(*ans[1:])

main()
Bài 5Màu chiếm ưu thếKhá

Cây $n$ đỉnh gốc 1 ($n \le 10^5$), đỉnh $u$ có màu $c_u$ ($1 \le c_u \le n$). Trong cây con của $u$, một màu gọi là chiếm ưu thế nếu không màu nào xuất hiện nhiều lần hơn nó (có thể có nhiều màu cùng chiếm ưu thế). Với mỗi $u$, in tổng các màu chiếm ưu thế trong cây con của $u$.

Input: như bài 4. Output: $n$ số trên một dòng.

Input
5
1 2 3 2 3
1 2
1 3
2 4
3 5
Output
5 2 3 2 3

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Sack ở phần 5, thay biến distinct bằng hai biến mx (số lần xuất hiện lớn nhất) và sumMx (tổng các màu đạt mx). Khi thêm một đỉnh màu $c$: tăng cnt[c] thành $k$; nếu $k > mx$ thì mx = k, sumMx = c; nếu $k = mx$ thì sumMx += c. Không cần hỗ trợ xoá từng phần tử: khi keep = false ta xoá sạch, đặt lại cnt về 0 và mx = sumMx = 0. Ví dụ ở gốc: màu 2 và 3 đều xuất hiện 2 lần, tổng $2 + 3 = 5$. Tổng có thể vượt int.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
int n, c[MAXN], sz[MAXN], big[MAXN], tin[MAXN], tout[MAXN], ord_[MAXN], timer_ = 0, cnt[MAXN];
int mx = 0;                  // số lần xuất hiện lớn nhất hiện tại
long long sumMx = 0, ans[MAXN];   // tổng các màu đạt mx
vector<int> adj[MAXN];

void prep(int u, int p){
    sz[u] = 1; tin[u] = ++timer_; ord_[timer_] = u;
    for(int v : adj[u]) if(v != p){
        prep(v, u); sz[u] += sz[v];
        if(big[u] == 0 || sz[v] > sz[big[u]]) big[u] = v;
    }
    tout[u] = timer_;
}
void addV(int v){
    int k = ++cnt[c[v]];
    if(k > mx){ mx = k; sumMx = c[v]; }
    else if(k == mx) sumMx += c[v];
}
void dfs(int u, int p, bool keep){
    for(int v : adj[u]) if(v != p && v != big[u]) dfs(v, u, false);
    if(big[u]) dfs(big[u], u, true);
    addV(u);
    for(int v : adj[u]) if(v != p && v != big[u])
        for(int t = tin[v]; t <= tout[v]; t++) addV(ord_[t]);
    ans[u] = sumMx;
    if(!keep){                                      // xoá sạch: đặt lại cnt, mx, sumMx
        for(int t = tin[u]; t <= tout[u]; t++) cnt[c[ord_[t]]] = 0;
        mx = 0; sumMx = 0;
    }
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> c[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    prep(1, 0);
    dfs(1, 0, true);
    for(int u = 1; u <= n; u++) cout << ans[u] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    c = [0] + [int(x) for x in data[1:1 + n]]
    adj = [[] for _ in range(n + 1)]
    pos = 1 + n
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    par = [0] * (n + 1); tin = [0] * (n + 1); tout = [0] * (n + 1); it = [0] * (n + 1)
    order = [0, 1]; tin[1] = 1; st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; tin[v] = len(order); order.append(v); st.append(v)
        else:
            tout[u] = len(order) - 1; st.pop()
    sz = [1] * (n + 1); big = [0] * (n + 1)
    for t in range(n, 1, -1):
        v = order[t]; p = par[v]
        sz[p] += sz[v]
        if big[p] == 0 or sz[v] > sz[big[p]]:
            big[p] = v
    cpos = [c[v] for v in order]
    cnt = [0] * (n + 1); ans = [0] * (n + 1)
    mx = 0; smx = 0
    stack = [(1, True, False)]
    while stack:
        u, keep, done = stack.pop()
        if not done:
            stack.append((u, keep, True))
            if big[u]:
                stack.append((big[u], True, False))
            for v in adj[u]:
                if v != par[u] and v != big[u]:
                    stack.append((v, False, False))
            continue
        cols = [c[u]]
        for v in adj[u]:
            if v != par[u] and v != big[u]:
                cols += cpos[tin[v]:tout[v] + 1]
        for col in cols:
            cnt[col] += 1
            k = cnt[col]
            if k > mx:
                mx = k; smx = col
            elif k == mx:
                smx += col
        ans[u] = smx
        if not keep:
            for col in cpos[tin[u]:tout[u] + 1]:
                cnt[col] = 0
            mx = 0; smx = 0
    print(*ans[1:])

main()
Bài 6Đếm đỉnh ở tầng thứ kKhá

Cây $n$ đỉnh gốc 1 ($n, q \le 2 \cdot 10^5$). Mỗi truy vấn cho $u, k$ ($0 \le k \le n$): đếm số đỉnh trong cây con của $u$ nằm thấp hơn $u$ đúng $k$ tầng (độ sâu bằng $\text{dep}[u] + k$).

Input: dòng đầu $n, q$; $n - 1$ dòng tiếp là các cạnh; $q$ dòng sau mỗi dòng $u, k$.

Input
10 5
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
1 2
2 2
3 1
1 0
5 3
Output
4
2
2
1
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Các đỉnh có cùng độ sâu $d$, xếp theo tin tăng dần, tạo thành một danh sách byDep[d] (DFS thêm vào theo thứ tự vào nên tự tăng). Đỉnh nào trong danh sách có $\text{tin} \in [\text{tin}[u], \text{tout}[u]]$ thì thuộc cây con của $u$, đếm bằng hai lần tìm kiếm nhị phân. Mỗi truy vấn $O(\log n)$. Ví dụ: tầng 2 của cây gồm 4, 5, 6, 7, đều thuộc cây con gốc nên (1, 2) → 4; cây con 2 ở tầng 2 có 8, 9 nên (2, 2) → 2.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, tin[MAXN], tout[MAXN], dep[MAXN];
vector<int> adj[MAXN], byDep[MAXN];      // byDep[d]: tin của các đỉnh độ sâu d, tăng dần

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    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);
    }
    vector<int> par(n + 1, 0), it(n + 1, 0), st = {1};
    int timer_ = 0; tin[1] = ++timer_; byDep[0].push_back(1);
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; dep[v] = dep[u] + 1; tin[v] = ++timer_;
            byDep[dep[v]].push_back(tin[v]);       // thêm theo thứ tự vào nên tự tăng dần
            st.push_back(v);
        } else { tout[u] = timer_; st.pop_back(); }
    }
    while(q--){
        int u, k; cin >> u >> k;
        int d = dep[u] + k;
        if(d >= n){ cout << 0 << "\n"; continue; }
        vector<int> &w = byDep[d];
        cout << upper_bound(w.begin(), w.end(), tout[u]) - lower_bound(w.begin(), w.end(), tin[u]) << "\n";
    }
    return 0;
}
import sys
from bisect import bisect_left, bisect_right

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    pos = 2
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    tin = [0] * (n + 1); tout = [0] * (n + 1); par = [0] * (n + 1); it = [0] * (n + 1); dep = [0] * (n + 1)
    by_dep = [[] for _ in range(n)]
    timer = 1; tin[1] = 1; by_dep[0].append(1); st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; dep[v] = dep[u] + 1; timer += 1; tin[v] = timer
                by_dep[dep[v]].append(timer)        # tự tăng dần
                st.append(v)
        else:
            tout[u] = timer; st.pop()
    out = []
    for _ in range(q):
        u, k = int(data[pos]), int(data[pos + 1]); pos += 2
        d = dep[u] + k
        if d >= n:
            out.append(0); continue
        w = by_dep[d]
        out.append(bisect_right(w, tout[u]) - bisect_left(w, tin[u]))
    print('\n'.join(map(str, out)))

main()
Bài 7Cấp dưới lương thấp hơnKhá

Công ty $n$ nhân viên dạng cây gốc 1 ($n \le 2 \cdot 10^5$), lương $a_i$ ($1 \le a_i \le 10^9$). Với mỗi nhân viên $u$, đếm số cấp dưới $v$ (trực tiếp hoặc gián tiếp, $v \ne u$) có lương nhỏ hơn hẳn $a_u$.

Input: dòng đầu $n$; dòng hai $a_1..a_n$; $n - 1$ dòng tiếp là các cạnh. Output: $n$ số trên một dòng.

Input
10
5 3 2 7 1 4 6 2 8 9
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
Output
5 2 0 0 0 0 0 0 0 0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Làm offline trên Euler tour: sắp các đỉnh theo lương tăng dần, lần lượt “bật” ô tin[v] trên Fenwick. Khi tới lượt $u$, các ô đã bật chính là các đỉnh lương nhỏ hơn, nên đáp án = số ô đã bật trong đoạn $[\text{tin}[u], \text{tout}[u]]$. Cẩn thận với lương bằng nhau: xử lý theo nhóm, hỏi hết cả nhóm rồi mới bật cả nhóm. Ví dụ: cây con 2 có lương {3, 7, 1, 2, 8}, trong đó 1 và 2 nhỏ hơn 3 nên đáp án của 2 là 2 (đỉnh 8 lương 2 là cấp dưới gián tiếp qua 5, vẫn được tính). Gốc có lương 5, các cấp dưới lương 3, 2, 1, 4, 2 nhỏ hơn nên đáp án là 5.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, tin[MAXN], tout[MAXN], bit[MAXN];
long long a[MAXN];
vector<int> adj[MAXN];

void add(int i){ for(; i <= n; i += i & -i) bit[i]++; }
int sum(int i){ int s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> par(n + 1, 0), it(n + 1, 0), st = {1};
    int timer_ = 0; tin[1] = ++timer_;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; tin[v] = ++timer_; st.push_back(v);
        } else { tout[u] = timer_; st.pop_back(); }
    }
    vector<int> id(n);
    iota(id.begin(), id.end(), 1);
    sort(id.begin(), id.end(), [](int x, int y){ return a[x] < a[y]; });
    vector<int> ans(n + 1);
    for(int i = 0; i < n; ){
        int j = i;
        while(j < n && a[id[j]] == a[id[i]]) j++;            // nhóm giá trị bằng nhau
        for(int k = i; k < j; k++){                          // hỏi trước: chỉ đếm số NHỎ HƠN
            int u = id[k];
            ans[u] = sum(tout[u]) - sum(tin[u] - 1);
        }
        for(int k = i; k < j; k++) add(tin[id[k]]);         // rồi mới bật cả nhóm
        i = j;
    }
    for(int u = 1; u <= n; u++) cout << ans[u] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = [0] + [int(x) for x in data[1:1 + n]]
    adj = [[] for _ in range(n + 1)]
    pos = 1 + n
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    tin = [0] * (n + 1); tout = [0] * (n + 1); par = [0] * (n + 1); it = [0] * (n + 1)
    timer = 1; tin[1] = 1; st = [1]
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v != par[u]:
                par[v] = u; timer += 1; tin[v] = timer; st.append(v)
        else:
            tout[u] = timer; st.pop()
    bit = [0] * (n + 1)

    def pref(i):
        s = 0
        while i > 0:
            s += bit[i]; i -= i & -i
        return s

    ids = sorted(range(1, n + 1), key=lambda x: a[x])
    ans = [0] * (n + 1)
    i = 0
    while i < n:
        j = i
        while j < n and a[ids[j]] == a[ids[i]]:
            j += 1
        for k in range(i, j):                 # hỏi trước: chỉ đếm số NHỎ HƠN
            u = ids[k]
            ans[u] = pref(tout[u]) - pref(tin[u] - 1)
        for k in range(i, j):                 # rồi mới bật cả nhóm
            x = tin[ids[k]]
            while x <= n:
                bit[x] += 1; x += x & -x
        i = j
    print(*ans[1:])

main()
Bài 8Cặp đỉnh cách nhau KKhó

Cho cây $n$ đỉnh ($n \le 2 \cdot 10^5$) và số nguyên $K$ ($1 \le K \le n$). Đếm số cặp đỉnh không thứ tự $\{x, y\}$ mà đường đi giữa chúng có đúng $K$ cạnh.

Input: dòng đầu $n, K$; $n - 1$ dòng tiếp là các cạnh.

Input
10 3
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
7 10
Output
10

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Gộp nhỏ vào lớn với map<độ sâu, số đỉnh>. Mỗi cặp $\{x, y\}$ được đếm đúng một lần tại $p = \text{LCA}(x, y)$, lúc hai phía chứa $x$ và $y$ được gộp lại. Khoảng cách $= \text{dep}[x] + \text{dep}[y] - 2\,\text{dep}[p]$, nên với mỗi độ sâu $d$ trong tập nhỏ, số đỉnh ghép được là số đỉnh có độ sâu $2\,\text{dep}[p] + K - d$ trong tập lớn. Đếm hết cặp trước rồi mới chèn tập nhỏ vào. Lưu độ sâu tuyệt đối (tính từ gốc) để không phải sửa lại khoá khi đi lên. Đáp án tới cỡ $n^2 / 2$: dùng long long. Ví dụ có 10 cặp cách nhau 3 cạnh, chẳng hạn (4, 3), (8, 1), (10, 1), (8, 4).
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, K, par[MAXN], dep[MAXN];
vector<int> adj[MAXN];
map<int, int> M[MAXN];          // M[u][d] = số đỉnh ở độ sâu d (tính từ gốc) trong cây con u

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> K;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};
    vector<bool> seen(n + 1, false); seen[1] = true;
    for(int i = 0; i < (int)order.size(); i++)
        for(int v : adj[order[i]]) if(!seen[v]){
            seen[v] = true; par[v] = order[i]; dep[v] = dep[order[i]] + 1; order.push_back(v);
        }
    for(int u = 1; u <= n; u++) M[u][dep[u]] = 1;
    long long ans = 0;
    for(int i = n - 1; i > 0; i--){
        int u = order[i], p = par[u];
        if(M[u].size() > M[p].size()) swap(M[u], M[p]);     // M[u] giờ là tập nhỏ
        for(auto [d, c] : M[u]){                             // đếm cặp có LCA = p trước khi gộp
            auto f = M[p].find(2 * dep[p] + K - d);
            if(f != M[p].end()) ans += 1LL * c * f->second;
        }
        for(auto [d, c] : M[u]) M[p][d] += c;
        M[u].clear();
    }
    cout << ans;
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, K = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    pos = 2
    for _ in range(n - 1):
        u, v = int(data[pos]), int(data[pos + 1]); pos += 2
        adj[u].append(v); adj[v].append(u)
    par = [0] * (n + 1); dep = [0] * (n + 1); seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1; order.append(v)
    M = [None] + [{dep[u]: 1} for u in range(1, n + 1)]
    ans = 0
    for u in reversed(order[1:]):
        p = par[u]
        small, big = M[u], M[p]
        if len(small) > len(big):
            small, big = big, small
        target = 2 * dep[p] + K
        for d, c in small.items():            # đếm cặp có LCA = p trước khi gộp
            ans += c * big.get(target - d, 0)
        for d, c in small.items():
            big[d] = big.get(d, 0) + c
        M[p] = big; M[u] = None
    print(ans)

main()
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 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét