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 →

Chuyên đề 2: Số thực, làm tròn và các hàm toán học trong C++ và Python: in số thực, sai số, sqrt, pow, chia lấy trần

Chuyên đề 2: Số thực, làm tròn và các hàm toán học trong C++ và Python: in số thực, sai số, sqrt, pow, chia lấy trần
Series: Chuyên đề nhập môn Độ khó: Dễ Chuyên đề 2 / Nhập môn lập trình Số thực Làm tròn
// Chuyên đề nhập môn · Chuyên đề 2
Số thực, làm tròn và các hàm toán học
Vì sao 7 / 2 lại ra 3, vì sao 0.1 + 0.2 không bằng 0.3, và làm sao in đúng “hai chữ số sau dấu phẩy”? Chuyên đề này gỡ từng cái bẫy số thực mà học sinh mới học hay vấp nhất.

Lời mở đầu

Ở bài 2 em đã biết các kiểu dữ liệu và cách nhập xuất, ở bài 12 em đã làm quen với số học: chia lấy dư, ước, bội, số nguyên tố. Nhưng khi làm bài thật, rất nhiều bạn bị mất điểm oan chỉ vì những chuyện “nhỏ”: đề bảo in hai chữ số thập phân mà mình in ra 3.1428571; tính trung bình cộng ra 7 thay vì 7.75; kiểm tra số chính phương bằng sqrt thì đúng với số nhỏ nhưng sai với số cỡ $10^{18}$.

Chuyên đề 2 gom tất cả những chuyện đó lại một chỗ. Mình sẽ đi thật chậm, mỗi ý đều có ví dụ cụ thể chạy được, có mô phỏng để em tự bấm và nhìn thấy máy tính lưu số thực “lệch” đi như thế nào. Học xong, em sẽ biết khi nào dùng số thực, in nó ra sao cho đúng, và quan trọng hơn: khi nào nên tránh hẳn số thực để dùng số nguyên cho chắc ăn.

Trong bài này bạn sẽ học: ba kiểu số thực float, double, long double và nên dùng kiểu nào; cách in đúng $k$ chữ số thập phân trong C++ và Python; lỗi kinh điển 7 / 2 = 3; bốn cách làm tròn floor, ceil, trunc, round với cả số âm, và cái bẫy round của Python; vì sao 0.1 + 0.2 ≠ 0.3 và cách so sánh bằng eps; mẹo chia lấy trần (a + b - 1) / b; tính căn bậc hai, căn bậc ba chính xác cho số tới $10^{18}$; vì sao không dùng pow cho số nguyên; khoảng cách, diện tích tam giác bằng số nguyên. Có 2 khung chạy tay, 3 mô phỏng 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 1Kiểu số thực và cách in số thực đúng yêu cầu

Số thực là số có phần thập phân như $3.14$, $-0.5$, $7.75$. Trong máy tính, số thực được lưu gần đúng với một số lượng chữ số có nghĩa giới hạn. C++ có ba kiểu số thực, khác nhau ở độ chính xác:

Kiểu (C++)Kích thướcSố chữ số có nghĩaNhận xét
float4 bytekhoảng 6–7quá ít, không nên dùng khi đi thi
double8 bytekhoảng 15–16dùng mặc định
long double10–16 bytekhoảng 18–19 (trên g++)khi cần chính xác hơn double
float của Python8 bytekhoảng 15–16chính là double của C++

Bảng 1. Các kiểu số thực. Python chỉ có một kiểu số thực float, nhưng nó tương đương double của C++ (không phải float của C++).

Ví dụ để thấy float “hụt hơi” cỡ nào: float f = 16777217; rồi in ra, em sẽ thấy 16777216. Chỉ mới 8 chữ số mà đã sai! Vì vậy quy tắc đơn giản: số thực thì dùng double, đừng bao giờ dùng float.

Lỗi kinh điển: 7 / 2 = 3

Trong C++, nếu cả hai số đều là số nguyên thì phép / là chia nguyên: kết quả bỏ đi phần lẻ. Vì vậy 7 / 2 cho 3, không phải 3.5. Điều nguy hiểm là dù em gán vào biến double thì vẫn sai, vì phép chia đã được làm xong bằng số nguyên trước khi gán:

Câu lệnh C++Kết quảGiải thích
double x = 7 / 2;37 và 2 đều nguyên → chia nguyên ra 3, rồi mới đổi 3 thành 3.0
double x = 7.0 / 2;3.57.0 là số thực → chia thực
double x = (double)a / b;3.5ép a sang double trước khi chia
double x = 1.0 * a / b;3.5nhân 1.0 cho a thành số thực trước
double x = (double)(a / b);3ép kiểu sau khi đã chia nguyên: quá muộn

Bảng 2. Với int a = 7, b = 2. Quy tắc: phải có ít nhất một số thực trước khi phép chia xảy ra.

Python thì đơn giản hơn: dấu / luôn là chia thực (7 / 2 ra 3.5), còn chia nguyên dùng dấu // (7 // 2 ra 3). Các bạn học Python chuyển sang C++ hay bị bẫy ở đúng chỗ này.

In số thực với đúng $k$ chữ số thập phân

Đề bài thường ghi “in kết quả với đúng 2 chữ số sau dấu chấm thập phân”. Nếu em chỉ viết cout << x thì C++ in tối đa 6 chữ số có nghĩa (không phải 6 chữ số thập phân!): 22.0 / 7 in ra 3.14286, còn 1234567.0 in ra 1.23457e+06. Cả hai đều sai định dạng. Có ba cách in đúng:

CáchCâu lệnhVới x = 22.0 / 7
C++ coutcout << fixed << setprecision(2) << x;3.14
C++ printfprintf("%.2f", x);3.14
Python f-stringprint(f"{x:.2f}")3.14
Python %print("%.2f" % x)3.14
Thiếu fixedcout << setprecision(2) << x;3.1

Bảng 3. Các cách in số thực với 2 chữ số thập phân. Dòng cuối là lỗi hay gặp: thiếu fixed thì setprecision(2) nghĩa là “2 chữ số có nghĩa”.

Vài điều cần nhớ:

  • fixed và setprecision chỉ cần viết một lần, chúng có tác dụng cho mọi lần cout số thực phía sau.
  • Cả hai cách đều tự làm tròn: 3.14285… in 2 chữ số thành 3.14, in 3 chữ số thành 3.143.
  • Khi in số thực bằng printf, dùng %f cho cả float lẫn double; khi đọc bằng scanf thì double phải dùng %lf. Cho gọn, em cứ dùng cin/cout.
Xem code C++ / Python: chia số nguyên và in số thực với k chữ số (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int a, b;
    cin >> a >> b;
    cout << a / b << "\n";                         // chia số nguyên: bỏ phần lẻ
    cout << fixed << setprecision(2);
    cout << (double)a / b << "\n";                 // ép a sang số thực trước khi chia
    printf("%.3f\n", 1.0 * a / b);                 // cách viết khác, in 3 chữ số
    return 0;
}
a, b = map(int, input().split())
print(a // b)                # chia lấy phần nguyên
print(f"{a / b:.2f}")        # dấu / của Python luôn cho số thực
print("%.3f" % (a / b))      # cách viết khác, in 3 chữ số
Khuôn mẫu in số thực
// C++: viết một lần ở đầu main
cout << fixed << setprecision(2);
cout << (double)tong / n << "\n";
# Python
print(f"{tong / n:.2f}")
Thay số 2 bằng số chữ số thập phân mà đề yêu cầu. Nếu đề không nói gì mà chấp nhận sai số (kiểu “sai khác không quá $10^{-6}$”), em in hẳn 9 hoặc 10 chữ số cho an toàn.
Trung bình cộng mà ra số nguyên?
Tổng s và số lượng n đều là int thì s / n là chia nguyên. Ví dụ điểm 7, 8, 10, 6 có tổng 31, 31 / 4 ra 7 chứ không phải 7.75. Luôn viết (double)s / n hoặc 1.0 * s / n.

Phần 2Làm tròn: floor, ceil, trunc, round

Có rất nhiều cách “làm tròn” một số thực thành số nguyên. Mỗi cách có tên riêng, và em phải phân biệt cho thật rõ:

  • floor(x) (sàn, kí hiệu $\lfloor x \rfloor$): số nguyên lớn nhất không vượt quá $x$. Trên trục số: đi sang trái.
  • ceil(x) (trần, kí hiệu $\lceil x \rceil$): số nguyên nhỏ nhất không nhỏ hơn $x$. Trên trục số: đi sang phải.
  • trunc(x): bỏ hẳn phần thập phân, tức là đi về phía số 0. Phép ép kiểu (int)x, (long long)x trong C++ và int(x) trong Python làm đúng như vậy.
  • round(x): số nguyên gần nhất. Khi phần lẻ đúng bằng 0.5 thì C++ chọn số xa số 0 hơn.

Với số dương, floor và trunc giống nhau, nên rất nhiều bạn nghĩ chúng là một. Nhưng với số âm thì khác hẳn:

xfloor(x)ceil(x)trunc(x)round(x) C++round(x) Python
2.323222
2.523232
2.723233
3.534344
−2.3−3−2−2−2−2
−2.5−3−2−2−3−2
−2.7−3−2−2−3−3

Bảng 4. Bốn hàm làm tròn với số dương và số âm. Ô vàng: floor của số âm đi xa số 0, khác với trunc. Ô đỏ: Python làm tròn 2.5 thành 2 và −2.5 thành −2.

Em hãy nhập thử nhiều số khác nhau vào mô phỏng dưới đây, đặc biệt là số âm và các số có đuôi .5, để thấy mỗi hàm “đi” về đâu trên trục số:

MÔ PHỎNGTrục số làm tròn: floor, ceil, trunc, roundNhập một số x (có thể âm) rồi bấm Tiếp. Mỗi hàm làm tròn là một mũi tên đi tới một số nguyên trên trục số.
floor: sang tráiceil: sang phảitrunc: về phía 0round: gần nhấtvị trí của x

Cái bẫy round của Python

Hàm round của Python không làm tròn “5 lên” như ta học ở trường. Khi số nằm đúng giữa hai số nguyên, Python chọn số chẵn: round(2.5) = 2, round(3.5) = 4, round(0.5) = 0. Cách này gọi là làm tròn kiểu ngân hàng (banker’s rounding), giúp sai số không bị dồn về một phía khi cộng nhiều số. Nhưng nếu đề bài yêu cầu làm tròn “nửa lên” thì em phải tự viết:

Xem code C++ / Python: floor, ceil, trunc và round “nửa lên” cho cả số âm (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    double x;
    cin >> x;
    cout << (long long)floor(x) << " "      // làm tròn xuống
         << (long long)ceil(x) << " "       // làm tròn lên
         << (long long)trunc(x) << " "      // bỏ phần thập phân (về phía 0)
         << (long long)round(x) << "\n";    // gần nhất, .5 thì xa số 0
    return 0;
}
import math

def round_half_up(x):            # làm tròn như C++: .5 thì xa số 0
    if x >= 0:
        return math.floor(x + 0.5)
    return -math.floor(-x + 0.5)

x = float(input())
print(math.floor(x), math.ceil(x), math.trunc(x), round_half_up(x))

Để ý trong C++ mình ép kết quả về long long trước khi in. Lý do: floor, ceil, round trong C++ trả về double, nếu in thẳng có khi ra -0 (số 0 âm, ví dụ trunc(-0.3)) hoặc 1e+06.

Làm tròn tới $k$ chữ số thập phân

Muốn làm tròn tới 1 chữ số thập phân, mẹo quen thuộc là nhân 10, làm tròn, rồi chia 10: round(x * 10) / 10. Nhưng hãy cẩn thận với những số “đúng giữa”. Ví dụ thầy cô làm tròn điểm 7.25 thành 7.3, nhưng printf("%.1f", 7.25) lại in ra 7.2! Vì sao? Số 7.25 lưu được chính xác trong máy, nó nằm đúng giữa 7.2 và 7.3, và cả printf của C++ lẫn f-string của Python đều chọn chữ số chẵn giống round của Python. Còn với 2.675 thì "%.2f" in 2.67 vì máy lưu 2.675 thành 2.67499999999999982…, hơi nhỏ hơn 2.675.

Làm tròn kiểu “nửa lên” cho chắc chắn
Khi đề yêu cầu làm tròn chính xác theo kiểu ở trường (≥ 5 thì lên), cách an toàn nhất là không dùng số thực: đọc điểm dưới dạng xâu, đổi ra số nguyên theo đơn vị nhỏ nhất (7.25 → 725 phần trăm), làm tròn bằng phép chia nguyên (725 + 5) / 10 = 73, rồi in 73 / 10 “.” 73 % 10 → 7.3. Bài tập 3 luyện đúng kỹ thuật này.

Phần 3Sai số số thực: vì sao 0.1 + 0.2 ≠ 0.3

Em chạy thử dòng này trong Python: print(0.1 + 0.2 == 0.3). Kết quả là False! Còn print(0.1 + 0.2) in ra 0.30000000000000004. Trong C++ cũng y hệt. Máy tính không hề tính sai, mà vì nó lưu số ở hệ nhị phân (hệ 2, em đã học ở chuyên đề 1).

Giống như $\frac{1}{3} = 0.3333\ldots$ viết ở hệ 10 thì dài vô hạn, số $0.1$ viết ở hệ 2 cũng dài vô hạn: $0.0001100110011\ldots_2$. Kiểu double chỉ giữ được khoảng 53 chữ số nhị phân, nên nó cắt bớt và lưu số gần 0.1 nhất mà nó biểu diễn được, đó là $0.1000000000000000055511\ldots$ Tương tự 0.2 và 0.3 cũng bị lưu lệch một chút. Cộng hai số lệch lại, kết quả lệch theo một kiểu khác với 0.3, nên phép so sánh == cho kết quả sai.

Số em viếtMáy thật sự lưu (in 20 chữ số)Lưu chính xác?
0.50.50000000000000000000có, vì $0.5 = 2^{-1}$
0.250.25000000000000000000có, vì $0.25 = 2^{-2}$
0.10.10000000000000000555không
0.30.29999999999999998890không
0.1 + 0.20.30000000000000004441không, và khác với 0.3
2.6752.67499999999999982236không

Bảng 5. In bằng printf("%.20f", x). Chỉ những số là tổng các lũy thừa của 2 (như 0.5, 0.25, 0.375) mới lưu chính xác.

Sai số của một phép tính rất nhỏ, chỉ cỡ $10^{-17}$. Nhưng khi em cộng đi cộng lại nhiều lần, các sai số nhỏ tích luỹ lại. Mô phỏng dưới đây cộng 0.1 mười lần và in giá trị thật mà máy đang lưu sau mỗi lần cộng (17 chữ số). Em sẽ thấy kết quả cuối cùng là 0.99999999999999989 chứ không phải 1:

MÔ PHỎNGSai số tích luỹ: cộng 0.1 nhiều lầnNhập số cộng d và số lần k (≤ 30). Mỗi bước hiện giá trị THẬT máy lưu (in 17 chữ số) để so với giá trị đúng.
lần cộng mới nhấtbằng số đúnglệch khỏi số đúng

So sánh số thực bằng eps

Vì số thực luôn có thể lệch một chút, ta không bao giờ so sánh bằng ==. Thay vào đó, ta coi hai số là bằng nhau nếu chúng đủ gần nhau: hiệu của chúng nhỏ hơn một số rất bé gọi là eps (epsilon), thường chọn $10^{-9}$.

khoảng [0.3 − eps, 0.3 + eps] với eps = 1e-90.3 máy lưu0.2999999999999999890.1 + 0.20.300000000000000044mỗi vạch ≈ 5.6e-17

Hình 1. Phóng to trục số quanh 0.3: hai giá trị máy lưu là hai điểm khác nhau (nên == sai), nhưng cùng nằm trong khoảng eps rất hẹp quanh 0.3 (nên coi là bằng nhau).

Muốn kiểm traViết saiViết đúng (với eps)
$a = b$a == bfabs(a - b) < EPS
$a \ne b$a != bfabs(a - b) >= EPS
$a < b$a < ba < b - EPS
$a \le b$a <= ba < b + EPS

Bảng 6. So sánh số thực với const double EPS = 1e-9;. Trong Python dùng abs thay cho fabs.

Xem code C++ / Python: so sánh số thực với eps (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const double EPS = 1e-9;

int main(){
    double a, b, c;
    cin >> a >> b >> c;
    if(fabs(a + b - c) < EPS) cout << "BANG NHAU\n";   // không viết a + b == c
    else if(a + b < c) cout << "NHO HON\n";
    else cout << "LON HON\n";
    return 0;
}
EPS = 1e-9
a, b, c = map(float, input().split())
if abs(a + b - c) < EPS:          # không viết a + b == c
    print("BANG NHAU")
elif a + b < c:
    print("NHO HON")
else:
    print("LON HON")

Cách tốt nhất: tránh hẳn số thực

eps không phải phép màu: chọn eps quá lớn thì hai số khác nhau bị coi là bằng, quá nhỏ thì hai số bằng nhau bị coi là khác. Vì thế, nếu bài toán có thể giải chỉ bằng số nguyên, hãy giải bằng số nguyên. Ba tình huống hay gặp:

  • So sánh hai phân số $\frac{a}{b}$ và $\frac{c}{d}$ (với $b, d > 0$): thay vì so sánh 1.0*a/b với 1.0*c/d, ta nhân chéo và so sánh hai số nguyên $a \cdot d$ và $c \cdot b$. Ví dụ $\frac{1}{3}$ và $\frac{333333333}{10^9}$: $1 \cdot 10^9 = 1000000000 > 333333333 \cdot 3 = 999999999$, nên $\frac{1}{3}$ lớn hơn. Nhớ dùng long long vì tích có thể tới $10^{18}$.
  • Tiền tệ: đổi hết ra đơn vị nhỏ nhất (đồng, xu) rồi lưu bằng số nguyên. Thử int(0.29 * 100) trong Python hoặc (int)(0.29 * 100) trong C++: em được 28 chứ không phải 29, vì 0.29 × 100 = 28.999999999999996.
  • So sánh khoảng cách: muốn biết điểm nào gần hơn, so sánh bình phương khoảng cách $dx^2 + dy^2$ (số nguyên) thay vì so sánh $\sqrt{dx^2 + dy^2}$.
Quy tắc vàng với số thực
1. Dùng double, không dùng float. 2. Không so sánh bằng ==, dùng eps. 3. Nếu được, biến đổi bài toán về số nguyên (nhân chéo, đổi đơn vị, bình phương) và chỉ chuyển sang số thực ở bước in kết quả cuối cùng.

Phần 4Chia lấy trần, căn bậc hai, lũy thừa chính xác

Bài toán: lớp có $n = 23$ học sinh, chia thành các nhóm tối đa $b = 5$ người. Cần ít nhất bao nhiêu nhóm? $23 / 5 = 4.6$, nhưng không thể có 4.6 nhóm, và 4 nhóm thì còn 3 bạn bị bỏ lại. Đáp án là $\lceil 23 / 5 \rceil = 5$: phải làm tròn lên. Những bài “cần bao nhiêu xe, bao nhiêu hộp, bao nhiêu ngày” đều là chia lấy trần.

Nhiều bạn viết ceil(1.0 * n / b). Cách này đúng với số nhỏ, nhưng với $n$ cỡ $10^{18}$ thì double không giữ đủ chữ số và kết quả có thể sai. Cách chuẩn là chỉ dùng số nguyên:

Khuôn mẫu chia lấy trần (với $a \ge 0$, $b > 0$)
long long q = (a + b - 1) / b;          // cách 1: cộng thêm b - 1
long long q = a / b + (a % b != 0);     // cách 2: dư thì thêm 1 nhóm
Trong Python còn có thể viết -(-a // b). Cả ba cách đều chỉ dùng số nguyên nên đúng tuyệt đối.

Vì sao cộng thêm $b - 1$ lại đúng? Nếu $a$ chia hết cho $b$ thì thêm $b - 1$ “chưa đủ” để tạo thêm một nhóm, thương giữ nguyên. Nếu $a$ dư $r \ge 1$ thì $r + (b - 1) \ge b$, thêm đúng một nhóm nữa. Em bấm mô phỏng để thấy các bạn “ảo” lấp vào nhóm lẻ:

MÔ PHỎNGChia n học sinh thành các nhóm b ngườiNhập n (1 … 40) và b (1 … 10). Xem vì sao nhóm lẻ cuối cùng khiến ta phải làm tròn lên, và mẹo cộng thêm b − 1.
nhóm đủ ngườinhóm lẻ (vẫn phải mở)bạn “ảo” thêm vàophần dư bị chia nguyên bỏ đi

Chia với số âm: C++ khác Python

Với số âm, phép chia nguyên của C++ và Python cho kết quả khác nhau:

Phép tínhC++PythonGiải thích
$-7$ chia nguyên $2$-7 / 2 = -3-7 // 2 = -4C++ cắt về 0 (trunc), Python làm tròn xuống (floor)
$-7$ chia lấy dư $2$-7 % 2 = -1-7 % 2 = 1dư C++ cùng dấu số bị chia, dư Python cùng dấu số chia
$7$ chia nguyên $2$7 / 2 = 37 // 2 = 3số dương: giống nhau

Bảng 7. Chia nguyên với số âm. Cả hai ngôn ngữ đều đảm bảo a == (a / b) * b + a % b (Python: //).

Như vậy Python có sẵn phép chia lấy sàn (a // b) và lấy trần (-(-a // b)) đúng với mọi dấu. Trong C++, phép / cắt về 0, nên với $b > 0$ ta sửa lại theo phần dư: dư âm thì thương đã bị làm tròn lên, muốn sàn phải trừ 1; dư dương thì thương đã bị làm tròn xuống, muốn trần phải cộng 1. Em chạy tay đoạn code sau với vài cặp số âm, dương để thấy rõ:

C++▶ Chạy tay từng dòng codefloorDiv và ceilDiv cho số nguyên bất kỳ (b > 0): sửa phép chia cắt-về-0 của C++ dựa vào dấu của phần dư.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4long long floorDiv(long long a, long long b){
5 long long q = a / b;
6 if(a % b < 0) q--;
7 return q;
8}
9
10long long ceilDiv(long long a, long long b){
11 long long q = a / b;
12 if(a % b > 0) q++;
13 return q;
14}
15
16int main(){
17 int t;
18 cin >> t;
19 while(t--){
20 long long a, b;
21 cin >> a >> b;
22 cout << a / b << " " << floorDiv(a, b) << " " << ceilDiv(a, b) << "\n";
23 }
24 return 0;
25}

Căn bậc hai chính xác cho số lớn

Kiểm tra $n$ có phải số chính phương không, cách tự nhiên là tính r = sqrt(n) rồi xem r * r == n. Với $n \le 10^{9}$ cách này chạy đúng. Nhưng với $n = 999999999999999999$ (18 chữ số 9), khi đổi sang double nó bị làm tròn thành $10^{18}$, sqrt cho đúng $10^9$, và ta kết luận $r = 10^9$. Sai, vì $(10^9)^2 = 10^{18} > n$! Căn nguyên đúng phải là $999999999$.

Cách chữa rất gọn: lấy kết quả của sqrt làm giá trị gần đúng, rồi dùng hai vòng while nhỏ để sửa bằng số nguyên: còn $r^2 > n$ thì giảm $r$, còn $(r+1)^2 \le n$ thì tăng $r$. Vì sqrt chỉ lệch rất ít, mỗi vòng while chạy nhiều nhất một vài lần. Em chạy tay với mẫu số 999999999999999999 để thấy vòng while thứ nhất “cứu” kết quả:

C++▶ Chạy tay từng dòng codeKiểm tra số chính phương với n ≤ 10^18: lấy sqrt gần đúng rồi sửa bằng hai vòng while chỉ dùng số nguyên.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int t;
6 cin >> t;
7 while(t--){
8 long long n;
9 cin >> n;
10 long long r = sqrt((double)n);
11 while(r * r > n) r--;
12 while((r + 1) * (r + 1) <= n) r++;
13 if(r * r == n) cout << "YES\n";
14 else cout << "NO\n";
15 }
16 return 0;
17}

Trong Python (từ phiên bản 3.8) có sẵn hàm math.isqrt(n) tính căn nguyên chính xác tuyệt đối cho số nguyên bao nhiêu chữ số cũng được, không cần sửa:

Xem code C++ / Python: kiểm tra số chính phương bằng math.isqrt (Python) (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int t;
    cin >> t;
    while(t--){
        long long n;
        cin >> n;
        long long r = sqrt((double)n);
        while(r * r > n) r--;
        while((r + 1) * (r + 1) <= n) r++;
        if(r * r == n) cout << "YES\n";
        else cout << "NO\n";
    }
    return 0;
}
import sys, math

data = sys.stdin.read().split()
t = int(data[0])
out = []
for k in range(1, t + 1):
    n = int(data[k])
    r = math.isqrt(n)            # căn nguyên chính xác tuyệt đối (Python 3.8+)
    out.append("YES" if r * r == n else "NO")
print("\n".join(out))
Căn bậc ba cũng vậy
Với căn bậc ba, cbrt(n) (C++) hay n ** (1/3) (Python) càng dễ sai: thử int(999700029999000000 ** (1/3)) trong Python, em được 999899, trong khi đúng là 999900 (vì $999900^3 = 999700029999000000$). Python không có hàm căn bậc ba nguyên có sẵn, nên cả hai ngôn ngữ đều làm theo cùng một khuôn: lấy gần đúng rồi sửa bằng while. Bài tập 8 luyện đúng việc này.

pow trả về số thực: tự viết lũy thừa nguyên

Hàm pow(a, b) của C++ luôn tính bằng double. Với số nhỏ thì không sao, nhưng $3^{39} = 4052555153018976267$ có 19 chữ số, double chỉ giữ được khoảng 16 chữ số, nên (long long)pow(3, 39) ra 4052555153018976256, lệch mất 11. Thậm chí có máy pow(10, 2) trả về 99.9999… và ép về int thành 99. Khi cần lũy thừa của số nguyên, em hãy tự viết vòng lặp nhân (hoặc dùng lũy thừa nhanh khi số mũ lớn). Python thì dùng a ** b với số nguyên là đúng tuyệt đối.

Xem code C++ / Python: pow sai với số lớn và lũy thừa bằng vòng lặp (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

long long power(long long a, int b){       // a^b bằng số nguyên, không qua double
    long long res = 1;
    for(int i = 0; i < b; i++) res *= a;
    return res;
}

int main(){
    long long a; int b;
    cin >> a >> b;                          // đề bảo đảm a^b <= 10^18
    cout << (long long)pow(a, b) << "\n";   // pow trả về double: có thể sai!
    cout << power(a, b) << "\n";            // luôn đúng
    return 0;
}
a, b = map(int, input().split())
print(int(float(a) ** b))   # đi qua số thực: có thể sai giống C++
print(a ** b)               # số nguyên của Python: luôn đúng

Phần 5Các hàm toán học hay dùng

Trong C++ các hàm toán học nằm trong thư viện <cmath> (đã có sẵn khi dùng bits/stdc++.h), còn Python cần import math. Ta điểm qua những hàm hay gặp nhất trong đề thi.

Giá trị tuyệt đối: abs, llabs, fabs

Trong C++, abs(x) dùng cho int, llabs(x) cho long long, fabs(x) cho số thực. Với bits/stdc++.h thì abs tự chọn đúng kiểu, nhưng viết rõ llabs/fabs giúp em không vô tình làm mất phần thập phân hay tràn số. Python chỉ có một hàm abs cho mọi kiểu số.

Khoảng cách hai điểm và diện tích tam giác

Khoảng cách giữa $A(x_1, y_1)$ và $B(x_2, y_2)$ là $AB = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$ (định lý Pitago). C++ và Python đều có hàm hypot(dx, dy) tính sẵn biểu thức này. Đây là chỗ bắt buộc dùng số thực, vì căn của số nguyên thường là số vô tỉ.

Còn diện tích tam giác thì không cần số thực! Với $\vec{AB} = (x_2 - x_1, y_2 - y_1)$ và $\vec{AC} = (x_3 - x_1, y_3 - y_1)$, ta tính tích có hướng:

$T = (x_2 - x_1)(y_3 - y_1) - (y_2 - y_1)(x_3 - x_1)$,    $S_{ABC} = \dfrac{|T|}{2}$.

$T$ là số nguyên, nên diện tích luôn có dạng “số nguyên” hoặc “số nguyên rưỡi”: $T$ chẵn thì phần lẻ là .0, $T$ lẻ thì là .5. In ra bằng T / 2 rồi thêm “.0” hoặc “.5”, không sai một li. Ngoài ra, $T > 0$ nghĩa là đi $A \to B \to C$ theo chiều ngược kim đồng hồ, $T < 0$ là cùng chiều, $T = 0$ là ba điểm thẳng hàng.

A(0, 0)B(4, 0)C(1, 3)AB = (4, 0), AC = (1, 3)tích có hướng = 4·3 − 0·1 = 12diện tích = |12| / 2 = 612 là số nguyên: chẵn → “.0”, lẻ → “.5”không cần đến số thực

Hình 2. Tam giác A(0, 0), B(4, 0), C(1, 3): đáy 4, cao 3 nên diện tích 6, đúng bằng |T| / 2 = 12 / 2.

Xem code C++ / Python: độ dài đoạn thẳng (số thực) và diện tích tam giác (số nguyên) (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long x1, y1, x2, y2, x3, y3;
    cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
    // độ dài AB: số thực, in 3 chữ số
    double ab = hypot(x2 - x1, y2 - y1);          // = sqrt(dx*dx + dy*dy)
    printf("%.3f\n", ab);
    // diện tích tam giác ABC: chỉ dùng số nguyên
    long long cr = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
    cr = llabs(cr);                                // 2 lần diện tích
    cout << cr / 2 << (cr % 2 ? ".5" : ".0") << "\n";
    return 0;
}
import math

x1, y1, x2, y2, x3, y3 = map(int, input().split())
ab = math.hypot(x2 - x1, y2 - y1)                  # = sqrt(dx*dx + dy*dy)
print(f"{ab:.3f}")
cr = abs((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1))   # 2 lần diện tích
print(str(cr // 2) + (".5" if cr % 2 else ".0"))

Số π, log2 và đếm chữ số

Số π: C++ có hằng M_PI (trên g++), nhưng một số trình chấm không có. Cách chắc ăn là tự định nghĩa const double PI = acos(-1.0); (vì $\cos \pi = -1$). Python dùng math.pi.

Đếm số chữ số nhị phân của $n > 0$ theo công thức là $\lfloor \log_2 n \rfloor + 1$. Nhưng log2 tính bằng số thực, và với $n = 2^{49} - 1$ thì floor(log2(n)) + 1 ra 50 thay vì 49, vì $\log_2 n$ quá sát 49 nên bị làm tròn thành 49.0. Hãy dùng hàm số nguyên: __lg(n) + 1 trong C++ (g++), n.bit_length() trong Python. Tương tự, đếm chữ số thập phân thì dùng to_string(n).size() / len(str(n)) hoặc vòng lặp chia 10 như ở chuyên đề 1, thay vì log10.

Việc cần làmC++PythonGhi chú
In $k$ chữ số thập phânfixed << setprecision(k), printf("%.2f")f"{x:.2f}"làm tròn tự động
Chia thực(double)a / ba / bC++: tránh 7 / 2 = 3
Làm tròn xuống / lênfloor, ceilmath.floor, math.ceilC++ trả về double
Bỏ phần thập phântrunc, (long long)xmath.trunc, int(x)về phía 0
Làm tròn gần nhấtround (.5 xa 0)round (.5 về số chẵn)khác nhau ở .5!
Chia lấy trần ($a \ge 0, b > 0$)(a + b - 1) / b-(-a // b)chỉ dùng số nguyên
Chia lấy sàn (mọi dấu, $b > 0$)sửa a / b theo dấu dưa // bC++ cắt về 0
Căn bậc hai nguyênsqrt + sửa bằng whilemath.isqrt(n)chính xác tới $10^{18}$
Lũy thừa số nguyêntự viết vòng lặpa ** bpow trả về double
Giá trị tuyệt đốiabs, llabs, fabsabs
Khoảng cáchhypot(dx, dy)math.hypot(dx, dy)so sánh thì dùng $dx^2 + dy^2$
Số πacos(-1.0)math.pi
Số chữ số nhị phân__lg(n) + 1n.bit_length()không dùng log2
So sánh số thựcfabs(a - b) < 1e-9abs(a - b) < 1e-9không dùng ==

Bảng 8. Tóm tắt C++ và Python cho chuyên đề. Em có thể lưu lại bảng này để tra nhanh khi luyện đề.

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 Số thực.

Bài tậpLuyện số thực và làm tròn

Bài 1Điểm trung bìnhDễ

Cho điểm của $n$ bài kiểm tra ($1 \le n \le 1000$), mỗi điểm là số nguyên từ $0$ đến $10^6$. In ra điểm trung bình với đúng 2 chữ số sau dấu chấm thập phân. (Dữ liệu được chọn để kết quả không rơi vào trường hợp “đúng giữa” khi làm tròn, nên mọi cách làm tròn đều in giống nhau.)

Input
4
7 8 10 6
Output
7.75

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

💡 Gợi ý
Tổng là 31, trung bình là $31 / 4 = 7.75$. Cái bẫy duy nhất: trong C++ nếu viết tong / n với hai biến int thì ra 7. Phải viết (double)tong / n, rồi in bằng fixed << setprecision(2). Tổng có thể tới $10^9$ nên dùng long long cho chắc.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    long long tong = 0;
    for(int i = 0; i < n; i++){
        long long x;
        cin >> x;
        tong += x;
    }
    cout << fixed << setprecision(2) << (double)tong / n << "\n";
    return 0;
}
import sys

data = sys.stdin.read().split()
n = int(data[0])
tong = sum(map(int, data[1:n + 1]))
print(f"{tong / n:.2f}")
Bài 2Thuê xe đi tham quanDễ

Trường có $n$ học sinh đi tham quan, mỗi xe chở được đúng $k$ người ($1 \le n, k \le 10^{18}$). Hỏi cần thuê ít nhất bao nhiêu xe, và khi đó còn thừa bao nhiêu ghế trống? In hai số trên một dòng.

Input
45 16
Output
3 3

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

💡 Gợi ý
Số xe là $\lceil n / k \rceil$ = (n + k - 1) / k. Ba xe có $3 \cdot 16 = 48$ ghế, thừa $48 - 45 = 3$ ghế. Không dùng ceil(1.0 * n / k) vì $n$ tới $10^{18}$ thì double làm tròn sai. Yên tâm về tràn số: $n + k - 1 < 2 \cdot 10^{18}$, vẫn vừa long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long n, k;
    cin >> n >> k;
    long long xe = (n + k - 1) / k;        // chia lấy trần
    cout << xe << " " << xe * k - n << "\n";
    return 0;
}
n, k = map(int, input().split())
xe = (n + k - 1) // k          # hoặc -(-n // k)
print(xe, xe * k - n)
Bài 3Làm tròn điểmDễ

Cho $n$ điểm số ($1 \le n \le 100$), mỗi điểm từ $0$ đến $10$ và được viết với đúng 2 chữ số sau dấu chấm (ví dụ 7.25, 10.00). Hãy làm tròn mỗi điểm tới 1 chữ số thập phân theo quy tắc ở trường: chữ số thứ hai từ 5 trở lên thì làm tròn lên, ngược lại thì bỏ đi. In mỗi kết quả trên một dòng, luôn có đúng 1 chữ số thập phân.

Input
4
7.25
8.24
9.95
6.50
Output
7.3
8.2
10.0
6.5

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

💡 Gợi ý
Nếu dùng printf("%.1f") thì 7.25 sẽ in ra 7.2 (phần 2 đã giải thích). Hãy tránh hẳn số thực: đọc điểm dạng xâu, bỏ dấu chấm để được số nguyên “phần trăm”: 7.25 → 725. Làm tròn tới “phần mười” bằng t = (725 + 5) / 10 = 73. In t / 10, dấu chấm, rồi t % 10: 7.3. Với 9.95: (995 + 5) / 10 = 100 → 10.0.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    for(int i = 0; i < n; i++){
        string s;
        cin >> s;                                  // ví dụ "7.25"
        int x = 0;
        for(char c : s)
            if(c != '.') x = x * 10 + (c - '0');   // 725 (đơn vị: phần trăm)
        int t = (x + 5) / 10;                      // 73 (đơn vị: phần mười)
        cout << t / 10 << "." << t % 10 << "\n";
    }
    return 0;
}
import sys

data = sys.stdin.read().split()
n = int(data[0])
out = []
for s in data[1:n + 1]:
    x = int(s.replace(".", ""))     # "7.25" -> 725
    t = (x + 5) // 10               # 73
    out.append(f"{t // 10}.{t % 10}")
print("\n".join(out))
Bài 4Quãng đường đi bộDễ

An đi bộ lần lượt qua $n$ điểm $P_1, P_2, \ldots, P_n$ ($2 \le n \le 1000$) trên mặt phẳng, giữa hai điểm liên tiếp đi theo đường thẳng. Toạ độ là số nguyên có giá trị tuyệt đối không quá 1000. In tổng quãng đường An đi, với đúng 2 chữ số thập phân.

Input
3
0 0
3 4
3 10
Output
11.00

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

💡 Gợi ý
Cộng dồn khoảng cách $\sqrt{dx^2 + dy^2}$ giữa từng cặp điểm liên tiếp, dùng hypot hoặc sqrt. Ở đây số thực là bắt buộc, chỉ cần nhớ dùng double và in đúng định dạng. Ví dụ: từ (0, 0) tới (3, 4) dài 5, từ (3, 4) tới (3, 10) dài 6, tổng 11.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<long long> x(n), y(n);
    for(int i = 0; i < n; i++) cin >> x[i] >> y[i];
    double s = 0;
    for(int i = 1; i < n; i++)
        s += hypot(x[i] - x[i - 1], y[i] - y[i - 1]);
    printf("%.2f\n", s);
    return 0;
}
import sys, math

data = sys.stdin.read().split()
n = int(data[0])
p = [(int(data[1 + 2 * i]), int(data[2 + 2 * i])) for i in range(n)]
s = 0.0
for i in range(1, n):
    s += math.hypot(p[i][0] - p[i - 1][0], p[i][1] - p[i - 1][1])
print(f"{s:.2f}")
Bài 5Diện tích mảnh vườn tam giácTrung bình

Một mảnh vườn hình tam giác có ba đỉnh toạ độ nguyên $(x_1, y_1), (x_2, y_2), (x_3, y_3)$ với $|x_i|, |y_i| \le 10^9$. In diện tích mảnh vườn với đúng 1 chữ số thập phân (ba điểm có thể thẳng hàng, khi đó diện tích là 0.0).

Input
0 0
4 0
1 3
Output
6.0

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

💡 Gợi ý
Dùng tích có hướng ở phần 5: $T = (x_2 - x_1)(y_3 - y_1) - (y_2 - y_1)(x_3 - x_1)$, diện tích $= |T| / 2$. In |T| / 2 rồi “.5” nếu $|T|$ lẻ, “.0” nếu chẵn. Mỗi tích có thể tới $2 \cdot 10^9 \cdot 2 \cdot 10^9 = 4 \cdot 10^{18}$, hiệu hai tích tới $8 \cdot 10^{18}$, vẫn vừa long long (tới khoảng $9.2 \cdot 10^{18}$). Nếu dùng double thì số cỡ $10^{18}$ sẽ mất mấy chữ số cuối. Ví dụ mẫu: $T = 4 \cdot 3 - 0 \cdot 1 = 12$, diện tích 6.0.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long x1, y1, x2, y2, x3, y3;
    cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
    long long T = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
    if(T < 0) T = -T;
    cout << T / 2 << (T % 2 == 1 ? ".5" : ".0") << "\n";
    return 0;
}
x1, y1, x2, y2, x3, y3 = map(int, open(0).read().split())
T = abs((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1))
print(str(T // 2) + (".5" if T % 2 == 1 else ".0"))
Bài 6So sánh hai phân sốTrung bình

Cho hai phân số $\frac{a}{b}$ và $\frac{c}{d}$ với $|a|, |c| \le 10^9$ và $1 \le b, d \le 10^9$. In ra một kí tự <, > hoặc = cho biết $\frac{a}{b}$ nhỏ hơn, lớn hơn hay bằng $\frac{c}{d}$. Không được dùng số thực.

Input
1 3 333333333 1000000000
Output
>

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

💡 Gợi ý
Vì $b, d > 0$ nên $\frac{a}{b} < \frac{c}{d} \Leftrightarrow a \cdot d < c \cdot b$ (nhân hai vế với $bd > 0$ không đổi chiều). Hai tích tới $10^{18}$ nên dùng long long. Ví dụ mẫu: $1 \cdot 10^9 = 1000000000$ còn $333333333 \cdot 3 = 999999999$, nên $\frac{1}{3}$ lớn hơn. Nếu mẫu số được phép âm, em phải đổi dấu cả tử và mẫu cho mẫu dương trước.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long a, b, c, d;
    cin >> a >> b >> c >> d;
    long long L = a * d, R = c * b;      // nhân chéo, b và d đều dương
    if(L < R) cout << "<\n";
    else if(L > R) cout << ">\n";
    else cout << "=\n";
    return 0;
}
a, b, c, d = map(int, input().split())
L, R = a * d, c * b              # nhân chéo, b và d đều dương
print("<" if L < R else (">" if L > R else "="))
Bài 7Số chính phương khổng lồTrung bình

Cho $t$ số nguyên $n$ ($1 \le t \le 10^5$, $0 \le n \le 10^{18}$). Với mỗi số, in YES nếu nó là số chính phương, ngược lại in NO.

Input
4
16
15
999999999999999999
1000000000000000000
Output
YES
NO
NO
YES

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

💡 Gợi ý
Chính là khung chạy tay ở phần 4: r = sqrt((double)n) rồi sửa bằng hai vòng while (giảm khi $r^2 > n$, tăng khi $(r + 1)^2 \le n$), cuối cùng kiểm tra $r^2 = n$. Với số 999999999999999999, sqrt trả về $10^9$ nhưng $(10^9)^2 > n$, vòng while sửa thành 999999999 và $999999999^2 \ne n$ nên in NO. Trong Python dùng thẳng math.isqrt.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int t;
    cin >> t;
    while(t--){
        long long n;
        cin >> n;
        long long r = sqrt((double)n);
        while(r * r > n) r--;
        while((r + 1) * (r + 1) <= n) r++;
        if(r * r == n) cout << "YES\n";
        else cout << "NO\n";
    }
    return 0;
}
import sys, math

data = sys.stdin.read().split()
t = int(data[0])
out = []
for k in range(1, t + 1):
    n = int(data[k])
    r = math.isqrt(n)            # căn nguyên chính xác tuyệt đối (Python 3.8+)
    out.append("YES" if r * r == n else "NO")
print("\n".join(out))
Bài 8Căn bậc ba nguyênTrung bình

Cho số nguyên $n$ ($0 \le n \le 10^{18}$). Tìm số nguyên $x$ lớn nhất sao cho $x^3 \le n$.

Input
999700029999000000
Output
999900

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

💡 Gợi ý
Lấy giá trị gần đúng r = cbrt((double)n) (Python: round(n ** (1/3))) rồi sửa như căn bậc hai: còn $r^3 > n$ thì giảm, còn $(r+1)^3 \le n$ thì tăng. Số mẫu chính là $999900^3$; nếu chỉ lấy int(n ** (1/3)) trong Python em được 999899, sai. $r$ chỉ cỡ $10^6$ nên $(r+1)^3$ không tràn long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long n;
    cin >> n;
    long long r = cbrt((double)n);                       // gần đúng
    while(r > 0 && r * r * r > n) r--;                   // sửa khi quá lớn
    while((r + 1) * (r + 1) * (r + 1) <= n) r++;         // sửa khi quá nhỏ
    cout << r << "\n";
    return 0;
}
n = int(input())
r = round(n ** (1 / 3))                  # gần đúng
while r > 0 and r * r * r > n:           # sửa khi quá lớn
    r -= 1
while (r + 1) ** 3 <= n:                 # sửa khi quá nhỏ
    r += 1
print(r)
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