Thể tích hình nón
Đề bài
Cho một hình nón có chiều cao $h$ và bán kính đáy $R$. Hãy tính thể tích của hình nón, biết
$$V = \dfrac{1}{3} \pi R^2 h.$$
Dữ liệu vào: Một dòng gồm hai số nguyên dương $h$ và $R$ cách nhau bởi dấu cách (chú ý: chiều cao $h$ đứng trước).
Kết quả: In ra thể tích hình nón, làm tròn đến 8 chữ số thập phân.
Giới hạn:
- $1 \le h, R \le 10^6$
Subtask:
- Subtask 1 (40% số điểm): $h, R \le 1000$.
- Subtask 2 (60% số điểm): không có ràng buộc gì thêm.
Bài được chấm với sai số tuyệt đối hoặc tương đối không quá $10^{-6}$.
Giới hạn thời gian: 1 giây, bộ nhớ: 256 MB. Dữ liệu nhập từ bàn phím và in ra màn hình.
2 7102.625360021 11.04719755Giải thích: Ví dụ 1: $h = 2$, $R = 7$ nên $V = \dfrac{1}{3} \pi \cdot 49 \cdot 2$ $\approx 102{,}62536002$.
Ví dụ 2: $h = R = 1$ nên $V = \dfrac{\pi}{3} \approx 1{,}04719755$.
Nguồn: phỏng theo bài "THỂ TÍCH HÌNH NÓN" – vinhdinhcoder.net.
Lời mở đầu
Đề cho sẵn công thức, chỉ việc "gõ vào máy" – nghe thì dễ ợt. Nhưng đây đúng là bài mà mình thấy các bạn mới học mất điểm nhiều nhất, vì nó gom đủ 4 cái bẫy kinh điển của bài nhập xuất số thực: đọc nhầm thứ tự, 1 / 3 bằng 0, tích bị tràn int, và in thiếu chữ số. Mình sẽ viết từng code "dính bẫy", chấm thật trên bộ test để các bạn thấy nó mất bao nhiêu điểm, rồi mới tới code chuẩn.
Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.
Nộp bài trên CLUE OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Bộ testBộ test chính thức có gì?
Bộ test có 20 test, mỗi test chỉ là một dòng 2 số. Mình đã kiểm tra: test 1–8 thoả điều kiện subtask 1 ($h, R \le 1000$), test 9–20 là subtask 2. Đáp án được tính bằng số học chính xác (không phải bằng double) rồi làm tròn 8 chữ số.
| Nhóm | Test | $h$ lớn nhất | $R$ lớn nhất | $R^2 h$ lớn nhất | Số test $R^2h > 2^{31}-1$ | $V$ lớn nhất |
|---|---|---|---|---|---|---|
| Subtask 1 | 1–8 | 1.000 | 1.000 | $9,95 \cdot 10^{8}$ | 0 | $1,04 \cdot 10^{9}$ |
| Subtask 2 | 9–20 | 1.000.000 | 1.000.000 | $1,00 \cdot 10^{18}$ | 10 | $1,05 \cdot 10^{18}$ |
- Ở subtask 1, $R^2 h \le 1000^2 \cdot 1000 = 10^9$ – vẫn nằm vừa trong kiểu
int(tối đa $2^{31} - 1 \approx 2{,}147 \cdot 10^9$). - Ở subtask 2 có 10 test mà $R^2 h$ vượt quá $2^{31} - 1$ (test 9, 10, 12, 14–20) – lớn nhất là $10^{18}$. Đây là "cái bẫy" dành cho bạn nào tính tích bằng
int. - Thể tích lớn nhất cỡ $10^{18}$, tức là có tới 19 chữ số phần nguyên – lát nữa mình sẽ nói vì sao điều này buộc người ra đề phải chấm "có sai số".
Kết quả chạy thật của các code trong bài theo từng subtask:
| Subtask | Test | Bẫy 1 đảo h, R | Bẫy 2 1 / 3 | Bẫy 3 tràn int | Bẫy 4cout trơn | Code chuẩn |
|---|---|---|---|---|---|---|
| Subtask 1 (40%) $h, R \le 1000$ | 1–8 | 2/8 | 0/8 | 8/8 | 3/8 | 8/8 |
| Subtask 2 (60%) đầy đủ: $h, R \le 10^6$ | 9–20 | 1/12 | 0/12 | 2/12 | 6/12 | 12/12 |
| Điểm | 0% | 0% | 40% | 0% | 100% |
Ý tưởngChỉ là thay số vào công thức
Thể tích hình nón bằng $\dfrac{1}{3}$ thể tích hình trụ cùng đáy, cùng chiều cao: $V = \dfrac{1}{3} \cdot (\pi R^2) \cdot h$, trong đó $\pi R^2$ là diện tích đáy. Máy tính sẽ làm lần lượt: $R^2$ → $R^2 h$ → nhân $\pi$ → chia 3 → in ra 8 chữ số thập phân. Có 3 chỗ phải cẩn thận:
- Giá trị $\pi$: không dùng $3{,}14$. Trong C++ dùng
acos(-1)(vì $\cos \pi = -1$) cho $\pi$ chính xác tới khoảng 16 chữ số; Python có sẵnmath.pi. - Kiểu dữ liệu: $R^2 h$ có thể tới $10^{18}$ – vượt
int. Các phép tính phải làm bằnglong longhoặcdouble. - Cách in: phải in đúng 8 chữ số sau dấu phẩy:
fixed << setprecision(8)hoặcprintf("%.8f").
Em nhập $h$, $R$ rồi bấm Tiếp để xem máy tính từng bước (và thấy lúc nào kiểu int bị tràn):
Bẫy 1Đọc nhầm thứ tự h và R
Thói quen viết công thức "$R$ trước, $h$ sau" làm nhiều bạn gõ cin >> R >> h;. Nhưng đề cho $h$ đứng trước. Với ví dụ 1 (2 7), máy sẽ hiểu $R = 2$, $h = 7$ và tính $\dfrac{1}{3}\pi \cdot 4 \cdot 7 \approx 29{,}32$ thay vì $102{,}63$.
Kết quả chạy thật: code đúng đúng 3/20 test, sai test 1, 3, 5–8, 10–20, chạy lâu nhất 0.004s. Ba test đúng là ba test "may mắn" có $h = R$ (đổi chỗ cũng như không). Bài học: đọc kỹ dòng "Dữ liệu vào", và luôn chạy thử ví dụ của đề trước khi nộp – ví dụ 1 bắt được lỗi này ngay.
Bẫy 2Một phần ba viết thành 1 / 3
Chép công thức nguyên văn thành 1 / 3 * PI * R * R * h trông rất "toán học". Nhưng trong C++, hai số nguyên chia nhau là phép chia nguyên: 1 / 3 = 0. Thế là cả biểu thức bằng $0 \cdot \pi \cdot R^2 \cdot h = 0$.
Kết quả chạy thật: code đúng đúng 0/20 test, sai test 1–20, chạy lâu nhất 0.003s – test nào cũng in 0.00000000. Cách sửa: viết 1.0 / 3, hoặc đơn giản là đưa phép chia xuống cuối: PI * R * R * h / 3 (lúc đó vế trái đã là double nên chia 3 là chia số thực).
double V = 1 / 3; vẫn cho V = 0, vì 1 / 3 đã được tính xong (bằng 0) trước khi gán.Bẫy 3Tích R * R * h tràn int
Khai báo int h, R; (vì đề nói là số nguyên) rồi tính R * R * h trước. Ba số int nhân nhau thì kết quả cũng là int – giới hạn $2^{31} - 1 = 2\,147\,483\,647$. Với $h = R = 10^6$ thì $R^2 h = 10^{18}$, vượt xa, kết quả bị "quay vòng" thành số rác (thậm chí âm). Việc gán vào double sau đó không cứu được – tràn đã xảy ra trong lúc nhân.
Kết quả chạy thật: code đúng đúng 10/20 test, sai test 9, 10, 12, 14–20, chạy lâu nhất 0.003s. Subtask 1 đúng hết (vì $R^2 h \le 10^9$), subtask 2 chỉ đúng 2 test có tích nhỏ (test 11: $h = 10^6, R = 1$; test 13: $R = 2$) → mất trọn 60%. Ví dụ ở test 9 code in ra -1556783382.61605549 – thể tích âm!
Cách sửa: khai báo long long h, R; (khi đó R * R * h tính bằng long long, tối đa $9{,}2 \cdot 10^{18}$, đủ chứa $10^{18}$), hoặc để PI đứng đầu: PI * R * R * h được tính từ trái sang phải, PI * R đã là double nên các phép nhân sau cũng là double.
Bẫy 4In bằng cout << V trơn
Tính đúng hết rồi, nhưng in bằng cout << V;. Mặc định cout chỉ in 6 chữ số có nghĩa và tự chuyển sang dạng khoa học khi số lớn: ví dụ 1 in ra 102.625, còn $V \approx 1{,}047 \cdot 10^6$ in ra 1.0472e+06.
Kết quả chạy thật: code đúng đúng 9/20 test, sai test 1, 2, 4–6, 9–11, 14, 16, 17, chạy lâu nhất 0.003s. Các test đúng là những test mà 6 chữ số có nghĩa tình cờ đã đủ sát đáp án (sai số tương đối dưới $10^{-6}$), ví dụ $V = 314{,}159$ (đáp án $314{,}15926536$). Chỉ cần lệch 1 chút như 102.625 so với $102{,}62536002$ (sai số $3{,}5 \cdot 10^{-6}$) là trượt. Bài học: đề bảo "làm tròn 8 chữ số thập phân" thì phải in đúng 8 chữ số: cout << fixed << setprecision(8) << V;.
Lời giảiCode chuẩn
Gom 4 bài học lại: đọc h trước, dùng long long + double, acos(-1), chia 3 ở cuối, in fixed << setprecision(8).
PI * R * R * h / 3được tính từ trái sang phải:PI * Rlàdouble, nên mọi phép nhân, chia sau đó đều là số thực – không tràn, không chia nguyên.fixednghĩa là "in dạng thập phân thường, không dùng e+XX";setprecision(8)đi kèmfixednghĩa là 8 chữ số sau dấu chấm. Có thể thay cả dòng bằngprintf("%.8f", V);.- Bản Python: phép
/của Python luôn ra số thực, số nguyên Python không bao giờ tràn, nên chỉ cần nhớmath.pivàf"{V:.8f}".
Kết quả chạy thật: bản C++ đúng 20/20 test, chạy lâu nhất 0.004s. Bản Python đúng 20/20 test, chạy lâu nhất 0.021s.
Sai sốVì sao đề phải chấm "có sai số"?
Kiểu double chỉ lưu được khoảng 15–16 chữ số có nghĩa. Ở test 9, đáp án chính xác là 1047197551196597746.15421446 – tới 27 chữ số! Code chuẩn của mình in ra 1047197551196597632.00000000: 16 chữ số đầu đúng, phần sau là "nhiễu" của double. Nếu chấm so khớp từng ký tự thì ngay cả code chuẩn cũng chỉ đúng 9/20 test (mình đã chạy thử). Vì vậy đề cho phép sai số tương đối $10^{-6}$: chỉ cần khoảng 7 chữ số có nghĩa đầu tiên đúng là được.
Điều đó cũng giải thích vài kết quả "bất ngờ" khi mình thử thêm các biến thể:
float PI = acos(-1); float V = ...;–floatchỉ có khoảng 7 chữ số chính xác, vừa đủ lọt sai số $10^{-6}$ nên vẫn đúng 20/20. Nhưng nếu đề chấm chặt hơn ($10^{-9}$) thì codefloatnày đúng 0/20, trong khi codedoublevẫn đúng 20/20. Vậy nên cứ dùngdoublecho chắc.- Dùng $\pi = 3{,}14$ (dù là
floathaydouble): sai số tương đối khoảng $5 \cdot 10^{-4}$, lớn hơn $10^{-6}$ cả trăm lần → sai cả 20 test. - Python
print(V)không định dạng in tới 17 chữ số (vd102.62536001726657) nên vẫn lọt sai số – nhưng đó là "may", đề yêu cầu 8 chữ số thì nên in đúng 8 chữ số.
Tổng kếtKết quả chạy thật trên bộ test
| Code | Ngôn ngữ | Đúng | Điểm | Ghi chú |
|---|---|---|---|---|
Bẫy 1: đọc R trước h | C++ | 3/20 | 0% | sai test 1, 3, 5–8, 10–20 – chỉ đúng khi tình cờ $h = R$ |
Bẫy 2: 1 / 3 * ... | C++ | 0/20 | 0% | luôn in 0.00000000 |
Bẫy 3: R * R * h kiểu int | C++ | 10/20 | 40% | sai test 9, 10, 12, 14–20 – đúng các test tích $\le 2^{31}-1$ |
Bẫy 4: cout << V | C++ | 9/20 | 0% | sai test 1, 2, 4–6, 9–11, 14, 16, 17 – chỉ 6 chữ số có nghĩa |
float + $\pi = 3{,}14$ | C++ | 0/20 | 0% | sai hết vì $\pi = 3{,}14$ lệch tới $5 \cdot 10^{-4}$ |
double + $\pi = 3{,}14$ | C++ | 0/20 | 0% | vẫn sai hết: lỗi nằm ở $\pi$ |
float + acos(-1) | C++ | 20/20 | 100% | qua với sai số $10^{-6}$, nhưng nếu chấm $10^{-9}$ thì chỉ đúng 0/20 |
Code chuẩn (double, acos(-1), 8 chữ số) | C++ | 20/20 | 100% | Full điểm, chạy lâu nhất 0.004s |
Code chuẩn (math.pi, f"{V:.8f}") | Python | 20/20 | 100% | Full điểm, chạy lâu nhất 0.021s |
print(V) không định dạng | Python | 20/20 | 100% | qua nhờ sai số $10^{-6}$ (Python in đủ 17 chữ số), nhưng sai yêu cầu "8 chữ số thập phân" |
int, dùng acos(-1) và in fixed << setprecision(8).
Nhận xét
Đăng nhận xét