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

Thể tích hình nón

Thể tích hình nón
Nguồn: CLUE OJ Độ khó: Dễ Nhập xuất Hình học Số thực

Đề 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.$$

hRđáy là hình trònbán kính R
Hình nón: chiều cao $h$ (nét đỏ), đáy là hình tròn bán kính $R$ (nét xanh).

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.

Ví dụ 1 – input
2 7
Ví dụ 1 – output
102.62536002
Ví dụ 2 – input
1 1
Ví dụ 2 – output
1.04719755

Giả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.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

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 OJ

Lộ 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ómTest$h$ lớn nhất$R$ lớn nhất$R^2 h$ lớn nhấtSố test $R^2h > 2^{31}-1$$V$ lớn nhất
Subtask 11–81.0001.000$9,95 \cdot 10^{8}$0$1,04 \cdot 10^{9}$
Subtask 29–201.000.0001.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:

SubtaskTestBẫy 1
đảo h, R
Bẫy 2
1 / 3
Bẫy 3
tràn int
Bẫy 4
cout trơn
Code
chuẩn
Subtask 1 (40%)
$h, R \le 1000$
1–82/80/88/83/88/8
Subtask 2 (60%)
đầy đủ: $h, R \le 10^6$
9–201/120/122/126/1212/12
Điểm0%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ẵn math.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ằng long long hoặc double.
  • Cách in: phải in đúng 8 chữ số sau dấu phẩy: fixed << setprecision(8) hoặc printf("%.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):

Ví dụ nhanh:

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$.

C++▶ Chạy tay từng dòng codeBẫy 1: đọc R trước h – hai biến bị đổi chỗ.
Dữ liệu mẫu:

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$.

C++▶ Chạy tay từng dòng codeBẫy 2: 1 / 3 là chia nguyên nên V luôn bằng 0.
Dữ liệu mẫu:

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).

Quy tắc nhớ
Trong C++, kiểu của phép tính do hai toán hạng quyết định, không phải do biến nhận kết quả: 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.

C++▶ Chạy tay từng dòng codeBẫy 3: R * R * h tính bằng int – thử ví dụ "Tràn int" để thấy kết quả rác.
Dữ liệu mẫu:

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.

C++▶ Chạy tay từng dòng codeBẫy 4: cout << V mặc định chỉ in 6 chữ số có nghĩa.
Dữ liệu mẫu:

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).

C++▶ Chạy tay từng dòng codeCode chuẩn: double, acos(-1), chia 3 ở cuối, in 8 chữ số.
Dữ liệu mẫu:
  1. PI * R * R * h / 3 được tính từ trái sang phải: PI * R là 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.
  2. fixed nghĩa là "in dạng thập phân thường, không dùng e+XX"; setprecision(8) đi kèm fixed nghĩa là 8 chữ số sau dấu chấm. Có thể thay cả dòng bằng printf("%.8f", V);.
  3. 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.pi và f"{V:.8f}".
Không gian$O(1)$
Thời gian$O(1)$

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 = ...; – float chỉ 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ì code float này đúng 0/20, trong khi code double vẫn đúng 20/20. Vậy nên cứ dùng double cho chắc.
  • Dùng $\pi = 3{,}14$ (dù là float hay double): 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ố (vd 102.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

CodeNgôn ngữĐúngĐiểmGhi chú
Bẫy 1: đọc R trước hC++3/200%sai test 1, 3, 5–8, 10–20 – chỉ đúng khi tình cờ $h = R$
Bẫy 2: 1 / 3 * ...C++0/200%luôn in 0.00000000
Bẫy 3: R * R * h kiểu intC++10/2040%sai test 9, 10, 12, 14–20 – đúng các test tích $\le 2^{31}-1$
Bẫy 4: cout << VC++9/200%sai test 1, 2, 4–6, 9–11, 14, 16, 17 – chỉ 6 chữ số có nghĩa
float + $\pi = 3{,}14$C++0/200%sai hết vì $\pi = 3{,}14$ lệch tới $5 \cdot 10^{-4}$
double + $\pi = 3{,}14$C++0/200%vẫn sai hết: lỗi nằm ở $\pi$
float + acos(-1)C++20/20100%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/20100%Full điểm, chạy lâu nhất 0.004s
Code chuẩn (math.pi, f"{V:.8f}")Python20/20100%Full điểm, chạy lâu nhất 0.021s
print(V) không định dạngPython20/20100%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"
Ghi chú của mình: mọi kết quả trong bài là chạy thật trên 20 test chính thức, chấm với sai số tương đối/tuyệt đối $10^{-6}$ như đề; thời gian đều cỡ vài phần nghìn giây. Thuật toán chỉ là $O(1)$, cái khó nằm hết ở kiểu dữ liệu và cách in: đọc đúng thứ tự, tránh chia nguyên, tránh tràn int, dùng acos(-1) và in fixed << setprecision(8).
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 😇

Nhận xét