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 →

Số Có 9 Ước Số (Bản Khó)

Số Có 9 Ước Số (Bản Khó)
Nguồn: CLUE OJ Độ khó: Khá Số học Đếm ước Sàng Eratosthenes Tìm kiếm nhị phân

Đề bài

SỐ CÓ 9 ƯỚC SỐ (Bản Khó)

Time limit: 1s

Cho số nguyên dương $N$. Hãy đếm các số nguyên dương không lớn hơn $N$ và có đúng 9 ước số.

Input: Chỉ có 1 dòng ghi số $N$ $(1 \le N \le 10^{14})$.

Output: Ghi ra số lượng các số có 9 ước số.

Ví dụ – input
888
Ví dụ – output
8

Lời mở đầu

Đây là "bản khó" với $N$ lên tới $10^{14}$, nên đừng nghĩ tới chuyện duyệt từng số từ 1 đến $N$ rồi đếm ước nha các bạn – riêng việc duyệt qua $10^{14}$ số đã mất cả ngày rồi 😅. Để giải được bài này (ít nhất là theo hiểu biết của mình) chỉ có một con đường: phải nắm thật chắc công thức tính số lượng ước từ phân tích thừa số nguyên tố, rồi từ đó suy ngược ra dạng của những số có đúng 9 ước.

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:

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

Mình giải thích ví dụ mẫu để bạn nào chưa hiểu đề sẽ hiểu rõ. Với $N = 888$, có đúng 8 số không vượt quá 888 mà có đúng 9 ước:

SốPhân tíchCác ước
36$2^2 \cdot 3^2$1, 2, 3, 4, 6, 9, 12, 18, 36
100$2^2 \cdot 5^2$1, 2, 4, 5, 10, 20, 25, 50, 100
196$2^2 \cdot 7^2$1, 2, 4, 7, 14, 28, 49, 98, 196
225$3^2 \cdot 5^2$1, 3, 5, 9, 15, 25, 45, 75, 225
256$2^8$1, 2, 4, 8, 16, 32, 64, 128, 256
441$3^2 \cdot 7^2$1, 3, 7, 9, 21, 49, 63, 147, 441
484$2^2 \cdot 11^2$1, 2, 4, 11, 22, 44, 121, 242, 484
676$2^2 \cdot 13^2$1, 2, 4, 13, 26, 52, 169, 338, 676

Số tiếp theo có 9 ước là $1089 = 3^2 \cdot 11^2 \gt 888$ nên đáp án là 8.

Nền tảng: công thức đếm số ước

Nếu số $X$ phân tích ra thừa số nguyên tố có dạng $X = p_1^{e_1} \cdot p_2^{e_2} \cdots p_k^{e_k}$ (với $p_1, p_2, \ldots$ là các số nguyên tố, $e_1, e_2, \ldots$ là số mũ) thì số lượng ước phân biệt của $X$ là:

Công thức số ước
$d(X) = (e_1 + 1) \cdot (e_2 + 1) \cdots (e_k + 1)$
tức là lấy các số mũ cộng 1 rồi nhân tích lại. Ví dụ $100 = 2^2 \cdot 5^2$ có $(2 + 1)(2 + 1) = 9$ ước.

Lý do: mỗi ước của $X$ có dạng $p_1^{f_1} \cdots p_k^{f_k}$ với $0 \le f_i \le e_i$, và số mũ $f_i$ có $e_i + 1$ lựa chọn độc lập ⇒ nhân lại.

Vậy số có đúng 9 ước thì tích các $(e_i + 1)$ phải bằng 9. Mà mỗi $e_i \ge 1$ nên mỗi thừa số $(e_i + 1) \ge 2$, và $9$ chỉ tách được thành tích các thừa số $\ge 2$ theo 2 kiểu: $9$ hoặc $3 \times 3$. (Có bạn sẽ nói $1 \times 9$, $1 \times 1 \times 9$ cũng bằng 9 – đúng, nhưng thừa số 1 ứng với số mũ 0, tức là $p^0 = 1$, nghĩa là số nguyên tố đó thực ra không có mặt nên không cần xét.) Từ đó:

Dạng 1: $9 = 8 + 1$
$X = p^8$ với $p$ nguyên tố.
Ví dụ: $256 = 2^8$.
Dạng 2: $9 = (2 + 1)(2 + 1)$
$X = p^2 \cdot q^2$ với $p \lt q$ là hai số nguyên tố phân biệt.
Ví dụ: $36 = 2^2 \cdot 3^2$.

Bài toán quy về đếm:

  • Trường hợp 1: số các số nguyên tố $p$ thoả $p^8 \le N$.
  • Trường hợp 2: số các cặp số nguyên tố $p \lt q$ thoả $p^2 \cdot q^2 \le N$ $\iff$ $p \cdot q \le \sqrt{N}$.

Hai dạng không bao giờ trùng nhau (do phân tích thừa số nguyên tố là duy nhất), nên đáp án là tổng số lượng của cả hai trường hợp. Đến đây là đã nhìn ra bản chất của bài rồi hen 😉. Ta yêu cầu $p \lt q$ vì khi phân tích, các thừa số nguyên tố được viết theo thứ tự tăng dần – nhờ vậy mỗi số chỉ được đếm đúng 1 lần ($2^2 \cdot 3^2$ và $3^2 \cdot 2^2$ là cùng một số 36).

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

Cách 1Vét từng trường hợp với hàm isPrime – chạy được ~90%

Ta cần một hàm kiểm tra số nguyên tố isPrime(x) chạy trong $O(\sqrt{x})$ (thử chia cho các số từ 2 đến $\sqrt{x}$). Hàm này dùng chung cho cả hai trường hợp.

Trường hợp 1: cho $i$ chạy từ 2 (số nguyên tố nhỏ nhất) trở lên, chừng nào $i^8 \le N$ thì nếu $i$ nguyên tố ta tăng biến đếm. Khi $i^8 \gt N$ thì dừng luôn, vì $i$ càng lớn thì $i^8$ càng lớn. Lũy thừa 8 tăng cực nhanh: với $N = 10^{14}$ vòng lặp chỉ chạy tới 56 thôi ($57^8 \approx 1{,}1 \cdot 10^{14} \gt 10^{14}$), và mỗi lần isPrime chỉ thử vài số chia (với 57 chỉ thử tới 7) ⇒ trường hợp này gần như không tốn gì.

Trường hợp 2 mới là phần nặng. Đặt $X = \lfloor\sqrt{N}\rfloor$, ta cần đếm cặp nguyên tố $a \lt b$ với $a \cdot b \le X$:

  • Vì $a \lt b$ và $a \cdot b \le X$ nên $a^2 \lt X$, tức là $a$ chỉ cần xét tới $\sqrt{X}$. (Nếu $a \gt \sqrt{X}$ thì để $a \cdot b \le X$ buộc $b \lt \sqrt{X} \lt a$, trái với $a \lt b$.)
  • Với mỗi $a$ nguyên tố, $b$ phải thoả $a \lt b \le X / a$. Ta cho $b$ chạy từ $X / a$ giảm dần tới khi $b \le a$ thì dừng, gặp $b$ nguyên tố thì tăng biến đếm.
Tóm tắt
Đáp án = #{$p$ nguyên tố : $p^8 \le N$} + #{$(a, b)$ nguyên tố : $a \lt b$, $a \cdot b \le \lfloor\sqrt{N}\rfloor$}.

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;

/* Hàm kiểm tra số nguyên tố với độ phức tạp O(sqrt(X)) với X là số cần kiểm tra */
bool isPrime(long long x){
    for(long long i = 2; i * i <= x; ++i){
        if(x % i == 0){
            return false;
        }
    }
    return true;
}

int main(){
    long long n;
    cin >> n;
    long long cnt = 0;
    /* Trường hợp 1: Đếm tất cả các số nguyên tố thoả mãn luỹ thừa 8 mà <= n */
    for(long long i = 2; (long long)pow(i, 8) <= n; ++i){
        if(isPrime(i)){
            cnt++;
        }
    }
    /* Trường hợp 2: Đếm tất cả các số nguyên tố a, b thoả mãn a < b và a^2 * b^2 <= n */
    long long x = (long long)sqrt(n);
    long long sqrt_x = (long long)sqrt(x);
    for(long long a = 2; a <= sqrt_x; ++a){
        if(isPrime(a)){
            for(long long b = x / a; b > a; --b){
                if(isPrime(b)){
                    cnt++;
                }
            }
        }
    }
    cout << cnt;
    return 0;
}
import math

# Hàm kiểm tra số nguyên tố với độ phức tạp O(sqrt(X))
def is_prime(x):
    i = 2
    while i * i <= x:
        if x % i == 0:
            return False
        i += 1
    return True

n = int(input())
cnt = 0
# Trường hợp 1: đếm các số nguyên tố i thoả i^8 <= n
i = 2
while i ** 8 <= n:
    if is_prime(i):
        cnt += 1
    i += 1
# Trường hợp 2: đếm các cặp nguyên tố a < b thoả a^2 * b^2 <= n
x = math.isqrt(n)
sqrt_x = math.isqrt(x)
for a in range(2, sqrt_x + 1):
    if is_prime(a):
        b = x // a
        while b > a:
            if is_prime(b):
                cnt += 1
            b -= 1
print(cnt)

Bản Python dùng math.isqrt để lấy căn nguyên chính xác. Các bạn chạy tay với $N = 888$ để thấy từng số được đếm (bên phần Dữ liệu có danh sách các số đã đếm được):

C++▶ Chạy tay từng dòng codeCách 1: đếm p^8 ≤ n, rồi đếm cặp nguyên tố a < b với a·b ≤ √n, kiểm tra nguyên tố từng số.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4/* Hàm kiểm tra số nguyên tố với độ phức tạp O(sqrt(X)) với X là số cần kiểm tra */
5bool isPrime(long long x){
6 for(long long i = 2; i * i <= x; ++i){
7 if(x % i == 0){
8 return false;
9 }
10 }
11 return true;
12}
13
14int main(){
15 long long n;
16 cin >> n;
17 long long cnt = 0;
18 /* Trường hợp 1: Đếm tất cả các số nguyên tố thoả mãn luỹ thừa 8 mà <= n */
19 for(long long i = 2; (long long)pow(i, 8) <= n; ++i){
20 if(isPrime(i)){
21 cnt++;
22 }
23 }
24 /* Trường hợp 2: Đếm tất cả các số nguyên tố a, b thoả mãn a < b và a^2 * b^2 <= n */
25 long long x = (long long)sqrt(n);
26 long long sqrt_x = (long long)sqrt(x);
27 for(long long a = 2; a <= sqrt_x; ++a){
28 if(isPrime(a)){
29 for(long long b = x / a; b > a; --b){
30 if(isPrime(b)){
31 cnt++;
32 }
33 }
34 }
35 }
36 cout << cnt;
37 return 0;
38}

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

  • Không gian: $O(1)$.
  • Thời gian: Trường hợp 1 là $O(N^{1/8})$ (vòng for i) $\times$ $O(N^{1/16})$ (kiểm tra nguyên tố của $i$) – không đáng kể. Trường hợp 2 là $O(N^{1/4})$ (vòng for a) $\times$ [$O(N^{1/8})$ (kiểm tra $a$) + $O(N^{1/2})$ (vòng for b) $\times$ $O(N^{1/4})$ (kiểm tra $b$)] ⇒ tính thô theo trường hợp xấu nhất thì cỡ $O(N)$.
Không gian$O(1)$
Thời giantính thô ~$O(N)$

Thực tế không tệ đến vậy: con số trên chỉ là chặn trên lý thuyết. Vòng for b chỉ chạy khi $a$ nguyên tố, độ dài của nó là $X/a$ giảm rất nhanh khi $a$ tăng, và isPrime(b) với $b$ là hợp số thường dừng sớm (đa số chia hết cho 2, 3, 5...). Vì vậy code này nộp được 90/100, chỉ 10 test cuối bị TLE.

Vẫn TLE với N lớn
Mình đo thử: với $N = 10^{14}$ code C++ ở trên tốn khoảng 15 giây CPU – vượt xa 1 giây. Nguyên nhân: isPrime bị gọi liên tục qua 2 vòng lặp lồng nhau, kiểm tra đi kiểm tra lại tính nguyên tố của cùng những số $b$ (với $a = 2$, $a = 3$, $a = 5$... các khoảng $b$ chồng lên nhau), và còn tốn công kiểm tra cả những $b$ không phải số nguyên tố.

Cách 2Sàng Eratosthenes + tìm kiếm nhị phân – O(√N log log √N)

Như mình nói, bài này chỉ có một hướng (xử lý 2 trường hợp), nên ta không cần nghĩ ra cách khác mà chỉ cần nhìn ra những chỗ chưa tốt của Cách 1 rồi khắc phục:

  1. Bỏ việc gọi isPrime lặp đi lặp lại. Ngay từ đầu ta tìm hết các số nguyên tố trong phạm vi cần thiết rồi đưa vào một mảng $a$. Chấp nhận tốn thêm bộ nhớ, nhưng bù lại chỉ duyệt trên số nguyên tố, và việc "xác định nguyên tố" chỉ làm đúng một lần. Phạm vi cần thiết là $\le \sqrt{N}$, vì $a^2 b^2 \le N \iff a \cdot b \le \sqrt{N}$. Và ta không kiểm tra từng số bằng isPrime nữa mà dùng sàng Eratosthenes cho tối ưu.
  2. Đếm bằng tìm kiếm nhị phân thay vì duyệt tuần tự. Mảng số nguyên tố sinh ra từ sàng luôn tăng dần, nên đếm "có bao nhiêu số nguyên tố $\le$ giá trị nào đó" chỉ cần tìm vị trí phần tử đầu tiên lớn hơn giá trị đó, dùng upper_bound có sẵn trong STL.
Đếm bằng upper_bound
  • Trường hợp 1: $x = \lfloor N^{1/8} \rfloor$. upper_bound(a, x) trả về vị trí idx của số nguyên tố đầu tiên $\gt x$. Vì mảng đánh chỉ số từ 0 nên có đúng idx số nguyên tố $\le x$ ⇒ cnt = idx.
  • Trường hợp 2: với mỗi $a[i]$ còn thoả $a[i]^4 \le N$ (tức $a[i] \le N^{1/4}$), đặt $x = \lfloor\sqrt{N}\rfloor / a[i]$, tìm j = upper_bound trong đoạn bắt đầu từ $i$. Các $b$ hợp lệ là $a[i+1], \ldots, a[j-1]$ ⇒ cộng thêm $j - i - 1$.

Minh hoạ: sàng xong rồi đếm bằng upper_bound

Em nhập $N$ rồi bấm Tiếp. Ô vàng là $a[i]$ đang xét, các ô xanh là những $b$ được đếm (từ $a[i+1]$ tới ngay trước vị trí upper_bound), ô có viền là vị trí upper_bound trả về:

Ví dụ nhanh:

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;

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    long long n;
    cin >> n;
    /* Bước sàng nguyên tố */
    long long sz = (long long)sqrt(n);
    vector<bool> checkPrime(sz + 1, true);
    checkPrime[0] = false;
    checkPrime[1] = false;
    vector<long long> a;
    for(long long i = 2; i <= sz; ++i){
        if(checkPrime[i] == true){
            a.push_back(i);
            for(long long j = i * i; j <= sz; j += i){
                checkPrime[j] = false;
            }
        }
    }
    /* Xử lý trường hợp 1 */
    long long x = (long long)pow(n, 1.0 / 8);
    vector<long long>::iterator upper;
    upper = upper_bound(a.begin(), a.end(), x);
    long long cnt = upper - a.begin();
    /* Xử lý trường hợp 2 */
    for(long long i = 0; i < a.size(); i++){
        if(pow(a[i], 4) > n){
            break;
        }
        x = sz / a[i];
        upper = upper_bound(a.begin() + i, a.end(), x);
        cnt += upper - (a.begin() + i) - 1;
    }
    cout << cnt;
    return 0;
}
import math
from bisect import bisect_right

n = int(input())
# Bước sàng nguyên tố đến sz = sqrt(n)
sz = math.isqrt(n)
check_prime = bytearray([1]) * (sz + 1)
check_prime[0] = 0
if sz >= 1:
    check_prime[1] = 0
for i in range(2, math.isqrt(sz) + 1):
    if check_prime[i]:
        check_prime[i * i::i] = bytes(len(range(i * i, sz + 1, i)))
a = [i for i in range(2, sz + 1) if check_prime[i]]

# Xử lý trường hợp 1: số nguyên tố p với p^8 <= n
x = math.isqrt(math.isqrt(math.isqrt(n)))   # căn bậc 8 của n (lấy phần nguyên)
cnt = bisect_right(a, x)

# Xử lý trường hợp 2: cặp a[i] < b với a[i] * b <= sz
for i in range(len(a)):
    if a[i] ** 4 > n:
        break
    x = sz // a[i]
    j = bisect_right(a, x, i)
    cnt += j - i - 1
print(cnt)
Một chỉnh sửa nhỏ so với code gốc của mình
Bản gốc khai báo mảng sàng là bool checkPrime[sz + 1]; – đây là mảng có độ dài biến (VLA), không thuộc C++ chuẩn và nằm trên stack. Với $N = 10^{14}$ thì $sz = 10^7$, mảng chiếm 10 MB stack. Hệ thống chấm cho stack lớn thì không sao, nhưng trên máy có stack mặc định 8 MB (như máy mình thử) chương trình bị Segmentation fault ngay. Nên mình đổi sang vector<bool> checkPrime(sz + 1, true); (cấp phát trên heap, còn gọn hơn vì khởi tạo true luôn), logic giữ nguyên.
Về các phép pow / sqrt với số thực
(long long)pow(n, 1.0 / 8) và (long long)sqrt(n) tính bằng số thực nên về lý thuyết có thể lệch 1 ở sát biên. Mình đã kiểm tra trực tiếp: với mọi số nguyên tố $p \le 59$ và $n \in \{p^8 - 1, p^8\}$, cũng như mọi $n \in \{k^2 - 1, k^2\}$ với $k \le 10^7$, kết quả đều đúng trong phạm vi $N \le 10^{14}$. Bản Python thì mình dùng math.isqrt (căn nguyên chính xác; căn bậc 8 = isqrt lồng 3 lần) cho chắc ăn.
C++▶ Chạy tay từng dòng codeCách 2: sàng nguyên tố đến √n, rồi đếm cả 2 trường hợp bằng upper_bound.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(0);
6 cin.tie(0);
7 cout.tie(0);
8 long long n;
9 cin >> n;
10 /* Bước sàng nguyên tố */
11 long long sz = (long long)sqrt(n);
12 vector<bool> checkPrime(sz + 1, true);
13 checkPrime[0] = false;
14 checkPrime[1] = false;
15 vector<long long> a;
16 for(long long i = 2; i <= sz; ++i){
17 if(checkPrime[i] == true){
18 a.push_back(i);
19 for(long long j = i * i; j <= sz; j += i){
20 checkPrime[j] = false;
21 }
22 }
23 }
24 /* Xử lý trường hợp 1 */
25 long long x = (long long)pow(n, 1.0 / 8);
26 vector<long long>::iterator upper;
27 upper = upper_bound(a.begin(), a.end(), x);
28 long long cnt = upper - a.begin();
29 /* Xử lý trường hợp 2 */
30 for(long long i = 0; i < a.size(); i++){
31 if(pow(a[i], 4) > n){
32 break;
33 }
34 x = sz / a[i];
35 upper = upper_bound(a.begin() + i, a.end(), x);
36 cnt += upper - (a.begin() + i) - 1;
37 }
38 cout << cnt;
39 return 0;
40}

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

  • Không gian: $O(M + K)$ với $M = \sqrt{N}$ là kích thước mảng checkPrime, $K$ là số lượng số nguyên tố trong mảng $a$ ($K \lt M$). Tính tổng quát cứ xem như $O(2\sqrt{N}) = O(\sqrt{N})$.
  • Thời gian: Bước sàng Eratosthenes đến $\sqrt{N}$ tốn $O(\sqrt{N} \log\log\sqrt{N})$ – nếu tính rộng tay là $O(\sqrt{N} \log \sqrt{N})$ thì vẫn là chặn trên đúng. Trường hợp 1 không đáng kể. Trường hợp 2 vòng lặp chạy chỉ tới các số nguyên tố $\le N^{1/4}$, mỗi lần tốn một lần tìm kiếm nhị phân $O(\log \sqrt{N})$ – nhỏ hơn hẳn bước sàng.

Theo quy tắc cộng của Big-O thì lấy bước lớn nhất là bước sàng.

Không gian$O(\sqrt{N})$
Thời gian$O(\sqrt{N} \log\log\sqrt{N})$

Vậy cách này chấp nhận tốn thêm bộ nhớ (với $N = 10^{14}$ là mảng sàng $10^7$ phần tử) nhưng thời gian giảm từ cỡ $O(N)$ xuống chỉ còn quanh $O(\sqrt{N})$. Kể cả $N = 10^{14}$ cũng không sợ TLE nữa. Các bạn đã thấy cái hay của thuật toán chưa 🥰

Ghi chú của mình: code C++ và Python của cả 2 cách đã được chạy với test đề (ra đúng 8) và so với code vét cạn (đếm ước trực tiếp mọi số đến $2 \cdot 10^6$) trên hơn 2000 giá trị $N$ (mọi $N \le 1200$ và 800 giá trị ngẫu nhiên đến $2 \cdot 10^6$; bản Python kiểm trên 500 giá trị trong số đó) – khớp hết. Với $N$ lớn ($10^{12}$, $3{,}14 \cdot 10^{12}$, $10^{14} - 1$, $10^{14}$) hai cách C++ và bản Python Cách 2 cho cùng kết quả (với $N = 10^{14}$ đáp án là 1903894). Về thời gian với $N = 10^{14}$: Cách 2 bản C++ tốn khoảng 0,08 giây CPU, bản Python khoảng 0,8 giây; còn Cách 1 tốn khoảng 15 giây CPU ⇒ TLE đúng như phân tích.
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