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 →

Smallest Missing Genetic Value in Each Subtree (LeetCode 2003)

Smallest Missing Genetic Value in Each Subtree (LeetCode 2003)
Nguồn: LeetCode 2003 Độ khó: Khó Cây DFS Gộp tập nhỏ vào lớn Tìm kiếm nhị phân

Đề bài

LeetCode 2003 – Smallest Missing Genetic Value in Each Subtree (Giá trị gen nhỏ nhất bị thiếu trong mỗi cây con) – độ khó Hard.

Có một cây gia phả có gốc là nút $0$, gồm $n$ nút được đánh số từ $0$ đến $n - 1$. Cho mảng số nguyên parents (đánh số từ 0), trong đó parents[i] là cha của nút $i$. Vì nút $0$ là gốc nên parents[0] == -1.

Có $10^5$ giá trị gen, mỗi giá trị được biểu diễn bằng một số nguyên trong đoạn $[1, 10^5]$ (tính cả hai đầu). Cho mảng số nguyên nums (đánh số từ 0), trong đó nums[i] là giá trị gen của nút $i$, các giá trị này đôi một khác nhau.

Yêu cầu: trả về mảng ans độ dài $n$, trong đó ans[i] là giá trị gen nhỏ nhất bị thiếu trong cây con có gốc tại nút $i$.

Cây con có gốc tại nút $x$ gồm nút $x$ và tất cả các nút con cháu của nó.

Ràng buộc:

  • n == parents.length == nums.length
  • $2 \le n \le 10^5$
  • $0 \le$ parents[i] $\le n - 1$ với $i \ne 0$
  • parents[0] == -1
  • parents biểu diễn một cây hợp lệ.
  • $1 \le$ nums[i] $\le 10^5$
  • Các nums[i] đôi một khác nhau.

Ví dụ 1:

Cây của ví dụ 1
Ví dụ 1: số trong vòng tròn là chỉ số nút, số trong ngoặc vuông là giá trị gen.
Input: parents = [-1,0,0,2], nums = [1,2,3,4]
Output: [5,1,1,1]

Giải thích: đáp án cho từng cây con như sau:

  • 0: cây con gồm các nút [0,1,2,3] với các gen [1,2,3,4]. Giá trị nhỏ nhất bị thiếu là 5.
  • 1: cây con chỉ gồm nút 1 với gen 2. Giá trị nhỏ nhất bị thiếu là 1.
  • 2: cây con gồm các nút [2,3] với các gen [3,4]. Giá trị nhỏ nhất bị thiếu là 1.
  • 3: cây con chỉ gồm nút 3 với gen 4. Giá trị nhỏ nhất bị thiếu là 1.

Ví dụ 2:

Cây của ví dụ 2
Ví dụ 2.
Input: parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3]
Output: [7,1,1,4,2,1]

Giải thích:

  • 0: cây con gồm các nút [0,1,2,3,4,5] với các gen [5,4,6,2,1,3]. Giá trị nhỏ nhất bị thiếu là 7.
  • 1: cây con gồm các nút [1,2] với các gen [4,6]. Giá trị nhỏ nhất bị thiếu là 1.
  • 2: cây con chỉ gồm nút 2 với gen 6. Giá trị nhỏ nhất bị thiếu là 1.
  • 3: cây con gồm các nút [3,4,5] với các gen [2,1,3]. Giá trị nhỏ nhất bị thiếu là 4.
  • 4: cây con chỉ gồm nút 4 với gen 1. Giá trị nhỏ nhất bị thiếu là 2.
  • 5: cây con chỉ gồm nút 5 với gen 3. Giá trị nhỏ nhất bị thiếu là 1.

Ví dụ 3:

Input: parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8]
Output: [1,1,1,1,1,1,1]

Giải thích: giá trị 1 bị thiếu trong tất cả các cây con.

2003. Smallest Missing Genetic Value in Each Subtree – Hard

There is a family tree rooted at 0 consisting of n nodes numbered 0 to n - 1. You are given a 0-indexed integer array parents, where parents[i] is the parent for node i. Since node 0 is the root, parents[0] == -1.

There are $10^5$ genetic values, each represented by an integer in the inclusive range $[1, 10^5]$. You are given a 0-indexed integer array nums, where nums[i] is a distinct genetic value for node i.

Return an array ans of length n where ans[i] is the smallest genetic value that is missing from the subtree rooted at node i.

The subtree rooted at a node x contains node x and all of its descendant nodes.

Example 1:

Example 1 tree
Input: parents = [-1,0,0,2], nums = [1,2,3,4]
Output: [5,1,1,1]
Explanation: The answer for each subtree is calculated as follows:
- 0: The subtree contains nodes [0,1,2,3] with values [1,2,3,4]. 5 is the smallest missing value.
- 1: The subtree contains only node 1 with value 2. 1 is the smallest missing value.
- 2: The subtree contains nodes [2,3] with values [3,4]. 1 is the smallest missing value.
- 3: The subtree contains only node 3 with value 4. 1 is the smallest missing value.

Example 2:

Example 2 tree
Input: parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3]
Output: [7,1,1,4,2,1]
Explanation: The answer for each subtree is calculated as follows:
- 0: The subtree contains nodes [0,1,2,3,4,5] with values [5,4,6,2,1,3]. 7 is the smallest missing value.
- 1: The subtree contains nodes [1,2] with values [4,6]. 1 is the smallest missing value.
- 2: The subtree contains only node 2 with value 6. 1 is the smallest missing value.
- 3: The subtree contains nodes [3,4,5] with values [2,1,3]. 4 is the smallest missing value.
- 4: The subtree contains only node 4 with value 1. 2 is the smallest missing value.
- 5: The subtree contains only node 5 with value 3. 1 is the smallest missing value.

Example 3:

Input: parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8]
Output: [1,1,1,1,1,1,1]
Explanation: The value 1 is missing from all the subtrees.

Constraints:

  • n == parents.length == nums.length
  • $2 \le n \le 10^5$
  • $0 \le$ parents[i] $\le n - 1$ for i != 0
  • parents[0] == -1
  • parents represents a valid tree.
  • $1 \le$ nums[i] $\le 10^5$
  • Each nums[i] is distinct.

Lời mở đầu

Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.

Nói trước với các bạn là bài này mình không đảm bảo cách làm của mình là tối ưu nhất. Mấu chốt để cách làm của mình AC được là nhờ vào một cấu trúc dữ liệu có sẵn của GNU C++ là ordered_set (thư viện pb_ds), chứ ban đầu mình bị TLE với một số test. Tuy nhiên mình có xem sơ qua code của một số người AC thì lại không cần dùng đến cấu trúc dữ liệu này như mình. Nên mình nói rõ để các bạn xem tham khảo được gì thì tham khảo hen. Ở cuối bài mình có trình bày thêm cách làm $O(n)$ mà nhiều bạn AC đã dùng để các bạn so sánh.

Về dữ liệu vào / ra của code trong bài
Trên LeetCode các bạn chỉ cần nộp phần class Solution. Để các bạn chạy thử được trên máy (hoặc trang Chạy code online), mọi code trong bài đều có thêm hàm main đọc từ bàn phím theo định dạng: dòng 1 là $n$, dòng 2 là $n$ số của parents, dòng 3 là $n$ số của nums; in ra $n$ số của mảng ans cách nhau dấu cách. Ví dụ 1 sẽ nhập là:
4
-1 0 0 2
1 2 3 4

Mình giải thích lại đề cho bạn nào chưa hiểu: mỗi nút của cây mang một số (gen), các số đôi một khác nhau. Với mỗi nút $i$, gom tất cả các số nằm trong cây con gốc $i$ lại thành một tập hợp, rồi tìm số nguyên dương nhỏ nhất không có trong tập đó (người ta hay gọi là MEX của tập). Ví dụ 2: cây con gốc 3 gồm các nút 3, 4, 5 mang các số $\{2, 1, 3\}$ ⇒ có đủ 1, 2, 3, thiếu 4 ⇒ ans[3] = 4. Cây con gốc 1 gồm $\{4, 6\}$ thiếu ngay số 1 ⇒ ans[1] = 1.

Link nộp bài

Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.

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:

Cách 1Gộp tập nhỏ vào tập lớn + ordered_set – O(n log² n)

Ý tưởng: mỗi nút giữ tập hợp các gen trong cây con của nó

Đại khái ý tưởng bài này là với mỗi nút $u$, dùng một tập hợp (set) S[u] để lưu trữ tập hợp các gen trên cây con gốc $u$. Ban đầu S[u] chỉ có đúng gen của chính nút $u$. Khi DFS tới nút $u$, xử lý xong các cây con $v$ của nó, ta sẽ đổ các phần tử từ S[v] vào S[u]. Đổ hết các con xong thì S[u] chính là tập gen của cây con gốc $u$, tiến hành tìm phần tử nhỏ nhất không xuất hiện trong tập hợp là xong.

Đổ thẳng v vào u thì bị TLE
Khổ một cái là nếu cứ đổ các phần tử từ cây $v$ vào cây $u$ như vậy thì sẽ bị TimeOut. Ví dụ cây là một đường thẳng $0 - 1 - 2 - \ldots - (n-1)$: nút $n-2$ đổ 1 phần tử, nút $n-3$ đổ 2 phần tử, ..., nút $0$ đổ $n - 1$ phần tử ⇒ tổng cộng khoảng $\dfrac{n^2}{2} = 5 \cdot 10^9$ lần chèn vào set với $n = 10^5$.

Đổ khéo hơn: luôn đổ tập nhỏ vào tập lớn

Thì thực ra ta vẫn đổ, nhưng phải đổ khéo hơn: nếu kích thước tập hợp S[u] nhỏ hơn kích thước S[v] thì ta hoán đổi 2 tập hợp lại (S[u].swap(S[v]) chỉ đổi "ruột" 2 cây, mất $O(1)$), rồi mới đổ tập ít phần tử hơn vào tập nhiều phần tử hơn. Làm vậy sẽ không bị TLE, vì tổng số thao tác đổ sẽ không vượt quá $N \log N$.

Vì sao tổng số lần đổ chỉ cỡ N log N?
Theo dõi một gen $x$ bất kỳ. Mỗi lần $x$ bị "bê" sang tập khác thì $x$ đang nằm ở tập nhỏ hơn, nên tập mới chứa $x$ có kích thước ít nhất gấp đôi tập cũ. Kích thước tập không vượt quá $N$, nên $x$ bị bê nhiều nhất $\log_2 N$ lần. Có $N$ gen ⇒ tổng số lần chèn $\le N \log_2 N \approx 1{,}7 \cdot 10^6$ với $N = 10^5$. Kỹ thuật này hay được gọi là small to large (gộp nhỏ vào lớn).

Tìm số nhỏ nhất bị thiếu bằng ordered_set + chặt nhị phân

Sau khi đổ xong, ta cần tìm MEX của S[u]. Duyệt từ 1 lên thì lại chậm, nên mình dùng chặt nhị phân kết hợp với sức mạnh của ordered_set. Cấu trúc dữ liệu này là một set (cũng tự sắp xếp tăng dần, cũng insert, lower_bound như set thường) nhưng có hỗ trợ thêm việc tìm vị trí của phần tử trong set: order_of_key(x) trả về số phần tử nhỏ hơn $x$, trong $O(\log n)$.

Code của mình xử lý 3 trường hợp:

  1. Phần tử nhỏ nhất của S[u] khác 1 ⇒ thiếu ngay số 1 ⇒ ans[u] = 1.
  2. Phần tử lớn nhất của S[u] bằng đúng kích thước tập ⇒ các gen khác nhau, đều $\ge 1$, có $k$ số mà lớn nhất là $k$ ⇒ tập chính là $\{1, 2, \ldots, k\}$ ⇒ ans[u] = k + 1.
  3. Còn lại: chặt nhị phân trên đoạn $[1, size + 1]$. Với mỗi mid, lấy value là số nhỏ nhất trong tập mà $\ge$ mid (lower_bound). Nếu thứ tự của value trong tập (order_of_key(value) + 1) đúng bằng value nghĩa là đủ hết các số $1, 2, \ldots, value$, kết quả còn lớn hơn nữa ⇒ cần tìm trên đoạn lớn hơn. Còn nếu thứ tự mà nhỏ hơn value thì trước value đã có số bị thiếu, mà các số từ mid tới value - 1 lại không có trong tập ⇒ kết quả $\le$ mid: lưu kết quả là mid, tìm trên đoạn nhỏ hơn.
Ví dụ chặt nhị phân
$S = \{1, 2, 3, 5, 9\}$, size = 5, max = 9 ≠ 5 ⇒ vào trường hợp 3, lo = 1, hi = 6.
mid = 3: value = 3, thứ tự = 3 = value ⇒ đủ 1..3 ⇒ lo = 4.
mid = 5: value = 5, thứ tự = 4 < 5 ⇒ thiếu ⇒ res = 5, hi = 4.
mid = 4: value = lower_bound(4) = 5, thứ tự = 4 < 5 ⇒ res = 4, hi = 3. Dừng ⇒ đáp án 4.

Em nhập cây (mảng parents và nums) rồi bấm Tiếp để xem DFS đi lên từ lá, đổ tập nhỏ vào tập lớn và tính đáp án cho từng nút. Dòng chữ nhỏ dưới mỗi nút là tập S[u] hiện tại:

Ví dụ nhanh:

Code dưới đây của mình đã AC với tư tưởng đã trình bày ở trên, anh em có thể tham khảo nhé:

#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using namespace std;
using namespace __gnu_pbds;

#define ordered_set tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update>

ordered_set S[100005];
vector<int> adj[100005];
vector<int> ans;

void dfs(int u, int father) {
    for (int i = 0; i < (int) adj[u].size(); i++) {
        int v = adj[u][i];
        if (v == father) {
            continue;
        }
        dfs(v, u);
        if ((int) S[u].size() < (int) S[v].size()) {
            S[u].swap(S[v]);
        }
        for (ordered_set::iterator it = S[v].begin(); it != S[v].end(); it++) {
            S[u].insert(*it);
        }
        S[v].clear();
    }
    if (*S[u].begin() != 1) {
        ans[u] = 1;
        return;
    }
    ordered_set::iterator it = S[u].end();
    it--;
    if (*it == (int) S[u].size()) {
        ans[u] = *it + 1;
        return;
    }
    int lo = 1;
    int hi = (int) S[u].size() + 1;
    int res = -1;
    while (lo <= hi) {
        int mid = (lo + hi) >> 1;
        ordered_set::iterator it = S[u].lower_bound(mid);
        int value = *it;
        if ((int) S[u].order_of_key(value) + 1 == value) {
            lo = mid + 1;
        }
        else {
            res = mid;
            hi = mid - 1;
        }
    }
    ans[u] = res;
}

class Solution {
public:
    vector<int> smallestMissingValueSubtree(vector<int>& parents, vector<int>& nums) {
        int n = (int) nums.size();
        ans.clear();
        ans.resize(n);
        for (int i = 0; i < n; i++) {
            adj[i].clear();
            S[i].clear();
        }
        for (int i = 1; i < n; i++) {
            adj[i].push_back(parents[i]);
            adj[parents[i]].push_back(i);
        }
        for (int i = 0; i < n; i++) {
            S[i].insert(nums[i]);
        }
        dfs(0, -1);
        return ans;
    }
};

int main() {
    int n;
    cin >> n;
    vector<int> parents(n), nums(n);
    for (int i = 0; i < n; i++) cin >> parents[i];
    for (int i = 0; i < n; i++) cin >> nums[i];
    Solution sol;
    vector<int> res = sol.smallestMissingValueSubtree(parents, nums);
    for (int i = 0; i < n; i++) cout << res[i] << " ";
    return 0;
}
import sys
from typing import List

class Solution:
    def smallestMissingValueSubtree(self, parents: List[int], nums: List[int]) -> List[int]:
        n = len(nums)
        children = [[] for _ in range(n)]
        for i in range(1, n):
            children[parents[i]].append(i)
        # thứ tự DFS từ gốc 0 (không đệ quy để tránh tràn stack khi cây là đường thẳng)
        order = [0]
        for u in order:
            order.extend(children[u])
        S = [{nums[i]} for i in range(n)]
        ans = [1] * n
        for u in reversed(order):          # con luôn được xử lý trước cha
            mex = 1
            for v in children[u]:
                if len(S[u]) < len(S[v]):  # đổ tập nhỏ vào tập lớn
                    S[u], S[v] = S[v], S[u]
                S[u].update(S[v])
                S[v] = None
                mex = max(mex, ans[v])     # tập của u chứa tập của v nên mex(u) >= mex(v)
            while mex in S[u]:
                mex += 1
            ans[u] = mex
        return ans

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    parents = [int(x) for x in data[1:1 + n]]
    nums = [int(x) for x in data[1 + n:1 + 2 * n]]
    res = Solution().smallestMissingValueSubtree(parents, nums)
    print(" ".join(map(str, res)))

main()

Giải thích một số chỗ trong code:

  1. Hai dòng #include <ext/pb_ds/...> và #define ordered_set ... là để dùng ordered_set của GNU, chỉ có trên trình biên dịch g++ (LeetCode, Codeforces... đều dùng được).
  2. Các mảng S, adj, ans khai báo toàn cục, mà LeetCode gọi hàm nhiều lần cho nhiều test trên cùng một chương trình, nên đầu hàm smallestMissingValueSubtree phải clear lại hết.
  3. S[u].swap(S[v]) đổi nội dung 2 tập trong $O(1)$, sau đó S[v] luôn là tập nhỏ hơn, ta đổ nó vào S[u] rồi clear để giải phóng bộ nhớ.
  4. Bản Python: Python không có sẵn ordered_set, nên mình dùng set thường, vẫn đổ tập nhỏ vào tập lớn. Riêng bước tìm MEX dùng một nhận xét: tập của $u$ chứa tập của mọi con $v$ nên ans[u] $\ge$ ans[v]; ta bắt đầu từ $\max$ ans[v] rồi tăng dần đến khi gặp số không có trong tập. Số lần tăng chỉ đáng kể trên các tổ tiên của nút mang gen 1 (các nút khác đáp án luôn là 1), nên tổng lại vẫn chỉ $O(n)$. Ngoài ra bản Python duyệt cây bằng vòng lặp thay cho đệ quy để không bị giới hạn độ sâu đệ quy.
Cẩn thận độ sâu đệ quy
Hàm dfs đệ quy sâu đúng bằng chiều cao cây, tệ nhất là $10^5$ tầng (cây đường thẳng). Trên LeetCode code này AC bình thường, nhưng mình chạy thử trên máy Linux với stack mặc định 8 MB thì test đường thẳng $n = 10^5$ bị lỗi tràn stack (thoát giữa chừng, không in gì); nâng stack lên (ulimit -s unlimited) thì chạy đúng. Bạn nào dùng code này ở chỗ khác thì lưu ý nhé.
C++▶ Chạy tay từng dòng codeDFS, đổ tập nhỏ vào tập lớn, tìm MEX bằng ordered_set + chặt nhị phân.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#include <ext/pb_ds/assoc_container.hpp>
3#include <ext/pb_ds/tree_policy.hpp>
4using namespace std;
5using namespace __gnu_pbds;
6
7#define ordered_set tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update>
8
9ordered_set S[100005];
10vector<int> adj[100005];
11vector<int> ans;
12
13void dfs(int u, int father) {
14 for (int i = 0; i < (int) adj[u].size(); i++) {
15 int v = adj[u][i];
16 if (v == father) {
17 continue;
18 }
19 dfs(v, u);
20 if ((int) S[u].size() < (int) S[v].size()) {
21 S[u].swap(S[v]);
22 }
23 for (ordered_set::iterator it = S[v].begin(); it != S[v].end(); it++) {
24 S[u].insert(*it);
25 }
26 S[v].clear();
27 }
28 if (*S[u].begin() != 1) {
29 ans[u] = 1;
30 return;
31 }
32 ordered_set::iterator it = S[u].end();
33 it--;
34 if (*it == (int) S[u].size()) {
35 ans[u] = *it + 1;
36 return;
37 }
38 int lo = 1;
39 int hi = (int) S[u].size() + 1;
40 int res = -1;
41 while (lo <= hi) {
42 int mid = (lo + hi) >> 1;
43 ordered_set::iterator it = S[u].lower_bound(mid);
44 int value = *it;
45 if ((int) S[u].order_of_key(value) + 1 == value) {
46 lo = mid + 1;
47 }
48 else {
49 res = mid;
50 hi = mid - 1;
51 }
52 }
53 ans[u] = res;
54}
55
56class Solution {
57public:
58 vector<int> smallestMissingValueSubtree(vector<int>& parents, vector<int>& nums) {
59 int n = (int) nums.size();
60 ans.clear();
61 ans.resize(n);
62 for (int i = 0; i < n; i++) {
63 adj[i].clear();
64 S[i].clear();
65 }
66 for (int i = 1; i < n; i++) {
67 adj[i].push_back(parents[i]);
68 adj[parents[i]].push_back(i);
69 }
70 for (int i = 0; i < n; i++) {
71 S[i].insert(nums[i]);
72 }
73 dfs(0, -1);
74 return ans;
75 }
76};
77
78int main() {
79 int n;
80 cin >> n;
81 vector<int> parents(n), nums(n);
82 for (int i = 0; i < n; i++) cin >> parents[i];
83 for (int i = 0; i < n; i++) cin >> nums[i];
84 Solution sol;
85 vector<int> res = sol.smallestMissingValueSubtree(parents, nums);
86 for (int i = 0; i < n; i++) cout << res[i] << " ";
87 return 0;
88}

Đánh giá độ phức tạp của cách làm này:

  • Không gian: $O(n)$: mỗi gen chỉ nằm trong đúng một tập tại mỗi thời điểm, cộng thêm danh sách kề và ngăn xếp đệ quy.
  • Thời gian: tổng số lần chèn khi đổ tập là $O(n \log n)$, mỗi lần chèn vào ordered_set mất $O(\log n)$ ⇒ $O(n \log^2 n)$. Bước chặt nhị phân ở mỗi nút có $O(\log n)$ vòng, mỗi vòng gọi lower_bound, order_of_key mất $O(\log n)$ ⇒ cũng $O(n \log^2 n)$ cho cả cây.
Không gian$O(n)$
Thời gian$O(n \log^2 n)$

Cách 2Chỉ đi trên đường từ nút mang gen 1 lên gốc – O(n)

Như mình nói ở đầu bài, code của nhiều người AC không cần tới ordered_set. Mấu chốt là một nhận xét rất đơn giản:

Nhận xét
Cây con nào không chứa nút mang gen 1 thì thiếu số 1 ⇒ đáp án là 1. Gen 1 xuất hiện ở nhiều nhất một nút (gọi là nút $x$), và các cây con chứa $x$ chính là cây con của $x$ và của các tổ tiên của $x$. Nên chỉ có các nút trên đường đi từ $x$ lên gốc $0$ là cần tính, mọi nút khác đáp án bằng 1.

Đi từ $x$ lên gốc thì cây con sau luôn chứa cây con trước. Vậy ta dùng một mảng đánh dấu has[] các gen đã gặp và mảng visited[] các nút đã duyệt: tới nút $u$ trên đường đi, ta chỉ duyệt thêm những nút chưa thăm trong cây con của $u$ (phần cây con trước đó đã thăm rồi thì không duyệt lại), đánh dấu gen của chúng. Đáp án cũng không giảm khi đi lên, nên biến mex chỉ việc tăng dần từ giá trị cũ.

Nút trên đường đi (ví dụ 2)Nút duyệt thêmGen đã cómex
4 (gen 1)4{1}2
33, 5{1, 2, 3}4
00, 1, 2{1, 2, 3, 4, 5, 6}7

Các nút 1, 2, 5 không nằm trên đường đi nên đáp án là 1 ⇒ ans = [7, 1, 1, 4, 2, 1], đúng như đề.

Source code để các bạn tham khảo nếu làm theo hướng này:

#include <bits/stdc++.h>
using namespace std;

class Solution {
public:
    vector<int> smallestMissingValueSubtree(vector<int>& parents, vector<int>& nums) {
        int n = (int) nums.size();
        vector<int> ans(n, 1);
        int node = -1;
        for (int i = 0; i < n; i++) {
            if (nums[i] == 1) node = i;
        }
        if (node == -1) return ans;             // không có gen 1: mọi cây con đều thiếu 1
        vector<vector<int>> children(n);
        for (int i = 1; i < n; i++) children[parents[i]].push_back(i);
        vector<bool> visited(n, false);         // nút đã được duyệt
        vector<bool> has(100002, false);        // gen đã xuất hiện
        int mex = 1;
        while (node != -1) {                    // đi ngược lên từ nút có gen 1
            vector<int> st;
            st.push_back(node);
            while (!st.empty()) {               // duyệt các nút chưa thăm trong cây con
                int u = st.back();
                st.pop_back();
                if (visited[u]) continue;
                visited[u] = true;
                has[nums[u]] = true;
                for (int v : children[u]) {
                    if (!visited[v]) st.push_back(v);
                }
            }
            while (has[mex]) mex++;
            ans[node] = mex;
            node = parents[node];
        }
        return ans;
    }
};

int main() {
    int n;
    cin >> n;
    vector<int> parents(n), nums(n);
    for (int i = 0; i < n; i++) cin >> parents[i];
    for (int i = 0; i < n; i++) cin >> nums[i];
    Solution sol;
    vector<int> res = sol.smallestMissingValueSubtree(parents, nums);
    for (int i = 0; i < n; i++) cout << res[i] << " ";
    return 0;
}
import sys
from typing import List

class Solution:
    def smallestMissingValueSubtree(self, parents: List[int], nums: List[int]) -> List[int]:
        n = len(nums)
        ans = [1] * n
        if 1 not in nums:                 # không có gen 1: mọi cây con đều thiếu 1
            return ans
        node = nums.index(1)
        children = [[] for _ in range(n)]
        for i in range(1, n):
            children[parents[i]].append(i)
        visited = [False] * n             # nút đã được duyệt
        has = [False] * (n + 2)           # gen đã xuất hiện (chỉ cần tới n + 1)
        mex = 1
        while node != -1:                 # đi ngược lên từ nút có gen 1
            st = [node]
            while st:                     # duyệt các nút chưa thăm trong cây con
                u = st.pop()
                if visited[u]:
                    continue
                visited[u] = True
                if nums[u] <= n:
                    has[nums[u]] = True
                for v in children[u]:
                    if not visited[v]:
                        st.append(v)
            while has[mex]:
                mex += 1
            ans[node] = mex
            node = parents[node]
        return ans

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    parents = [int(x) for x in data[1:1 + n]]
    nums = [int(x) for x in data[1 + n:1 + 2 * n]]
    res = Solution().smallestMissingValueSubtree(parents, nums)
    print(" ".join(map(str, res)))

main()

Đánh giá độ phức tạp: mỗi nút được đánh dấu visited và đẩy vào ngăn xếp tối đa vài lần (mỗi cạnh một lần), biến mex chỉ tăng tối đa tới $n + 1$ ⇒ thời gian $O(n)$ (cộng thêm mảng has cỡ $10^5$), bộ nhớ $O(n)$. Cách này cũng duyệt bằng ngăn xếp tự quản lý nên không lo tràn stack.

Không gian$O(n)$
Thời gian$O(n)$
Ghi chú của mình: code C++ và Python của cả 2 cách (đọc dữ liệu từ bàn phím như mô tả ở đầu bài) đã được kiểm tra với 3 ví dụ của đề và 500 cây ngẫu nhiên ($n \le 12$), so với code vét cạn – khớp hết. Với $n = 10^5$ (cây đường thẳng, cây hình sao, cây ngẫu nhiên): cả 4 chương trình chạy dưới 0.6 giây trên máy mình; riêng cách 1 bản C++ với cây đường thẳng cần nâng giới hạn stack như mình đã lưu ý ở trên.
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 😇
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