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
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.
(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ước | Số chữ số có nghĩa | Nhận xét |
|---|---|---|---|
float | 4 byte | khoảng 6–7 | quá ít, không nên dùng khi đi thi |
double | 8 byte | khoảng 15–16 | dùng mặc định |
long double | 10–16 byte | khoảng 18–19 (trên g++) | khi cần chính xác hơn double |
float của Python | 8 byte | khoảng 15–16 | chí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; | 3 | 7 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.5 | 7.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.5 | nhâ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ách | Câu lệnh | Với x = 22.0 / 7 |
|---|---|---|
C++ cout | cout << fixed << setprecision(2) << x; | 3.14 |
C++ printf | printf("%.2f", x); | 3.14 |
| Python f-string | print(f"{x:.2f}") | 3.14 |
Python % | print("%.2f" % x) | 3.14 |
Thiếu fixed | cout << 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ớ:
fixedvàsetprecisionchỉ cần viết một lần, chúng có tác dụng cho mọi lầncoutsố thực phía sau.- Cả hai cách đều tự làm tròn:
3.14285…in 2 chữ số thành3.14, in 3 chữ số thành3.143. - Khi in số thực bằng
printf, dùng%fcho cảfloatlẫndouble; khi đọc bằngscanfthìdoublephải dùng%lf. Cho gọn, em cứ dùngcin/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ố
// 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.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)xtrong 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:
| x | floor(x) | ceil(x) | trunc(x) | round(x) C++ | round(x) Python |
|---|---|---|---|---|---|
| 2.3 | 2 | 3 | 2 | 2 | 2 |
| 2.5 | 2 | 3 | 2 | 3 | 2 |
| 2.7 | 2 | 3 | 2 | 3 | 3 |
| 3.5 | 3 | 4 | 3 | 4 | 4 |
| −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ố:
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.
(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ết | Máy thật sự lưu (in 20 chữ số) | Lưu chính xác? |
|---|---|---|
| 0.5 | 0.50000000000000000000 | có, vì $0.5 = 2^{-1}$ |
| 0.25 | 0.25000000000000000000 | có, vì $0.25 = 2^{-2}$ |
| 0.1 | 0.10000000000000000555 | không |
| 0.3 | 0.29999999999999998890 | không |
| 0.1 + 0.2 | 0.30000000000000004441 | không, và khác với 0.3 |
| 2.675 | 2.67499999999999982236 | khô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:
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}$.
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 tra | Viết sai | Viết đúng (với eps) |
|---|---|---|
| $a = b$ | a == b | fabs(a - b) < EPS |
| $a \ne b$ | a != b | fabs(a - b) >= EPS |
| $a < b$ | a < b | a < b - EPS |
| $a \le b$ | a <= b | a < 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/bvới1.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ùnglong longvì 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}$.
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:
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ómTrong 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ẻ:
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ính | C++ | Python | Giải thích |
|---|---|---|---|
| $-7$ chia nguyên $2$ | -7 / 2 = -3 | -7 // 2 = -4 | C++ cắt về 0 (trunc), Python làm tròn xuống (floor) |
| $-7$ chia lấy dư $2$ | -7 % 2 = -1 | -7 % 2 = 1 | dư C++ cùng dấu số bị chia, dư Python cùng dấu số chia |
| $7$ chia nguyên $2$ | 7 / 2 = 3 | 7 // 2 = 3 | số 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ă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ả:
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))
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.
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àm | C++ | Python | Ghi chú |
|---|---|---|---|
| In $k$ chữ số thập phân | fixed << setprecision(k), printf("%.2f") | f"{x:.2f}" | làm tròn tự động |
| Chia thực | (double)a / b | a / b | C++: tránh 7 / 2 = 3 |
| Làm tròn xuống / lên | floor, ceil | math.floor, math.ceil | C++ trả về double |
| Bỏ phần thập phân | trunc, (long long)x | math.trunc, int(x) | về phía 0 |
| Làm tròn gần nhất | round (.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 // b | C++ cắt về 0 |
| Căn bậc hai nguyên | sqrt + sửa bằng while | math.isqrt(n) | chính xác tới $10^{18}$ |
| Lũy thừa số nguyên | tự viết vòng lặp | a ** b | pow trả về double |
| Giá trị tuyệt đối | abs, llabs, fabs | abs | |
| Khoảng cách | hypot(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) + 1 | n.bit_length() | không dùng log2 |
| So sánh số thực | fabs(a - b) < 1e-9 | abs(a - b) < 1e-9 | khô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 đề.
- Cột bò (Dễ)
- Ăn Bánh (cookies) (Dễ)
- Gấp Giấy (GAGI) (Dễ)
- Con Ốc Sên (SNAIL) (Dễ)
- Trung Bình Cộng (Trung bình)
- Trung Bình Cộng Lớn Nhất (CSLOJ 1404) (Khá)
Bài tậpLuyện số thực và làm tròn
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.)
4
7 8 10 67.75📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
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}")
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.
45 163 3📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
(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)
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.
4
7.25
8.24
9.95
6.507.3
8.2
10.0
6.5📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
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))
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.
3
0 0
3 4
3 1011.00📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
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}")
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).
0 0
4 0
1 36.0📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
|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"))
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.
1 3 333333333 1000000000>📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
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 "="))
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.
4
16
15
999999999999999999
1000000000000000000YES
NO
NO
YES📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
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))
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$.
999700029999000000999900📝 Nộp bài, chấm tự động trên ClueOJ ↗
💡 Gợi ý
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)
- 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.
- 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
Đăng nhận xét