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

Diện tích tam giác vuông

Diện tích tam giác vuông
Nguồn: CLUE OJ Độ khó: Dễ Nhập xuất Hình học Số thực

Đề bài

Cho tam giác vuông có hai cạnh góc vuông dài $a$ và $b$. Hãy tính diện tích của tam giác.

Dữ liệu vào: Một dòng gồm hai số nguyên dương $a$ và $b$ cách nhau bởi dấu cách.

Kết quả: In ra diện tích tam giác vuông, làm tròn đến 2 chữ số thập phân.

Giới hạn:

  • $1 \le a, b \le 10^6$

Subtask:

  • Subtask 1 (40% số điểm): $a, b \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 không quá $10^{-2}$.

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
5 6
Ví dụ 1 – output
15.00
Ví dụ 2 – input
1 1
Ví dụ 2 – output
0.50

Giải thích: Ví dụ 1: $S = \dfrac{5 \cdot 6}{2} = 15$, in ra 15.00.

Ví dụ 2: $S = \dfrac{1 \cdot 1}{2} = 0{,}5$, in ra 0.50.

Nguồn: phỏng theo bài "DIỆN TÍCH TAM GIÁC VUÔNG" – vinhdinhcoder.net.

Lời mở đầu

Diện tích tam giác vuông bằng nửa tích hai cạnh góc vuông – kiến thức lớp 5. Nhưng khi "dịch" sang C++ thì phép chia đôi và phép nhân lại giấu 2 cái bẫy rất kinh điển: chia nguyên làm mất 0,5 và tích hai số int bị tràn. Bài này chấm với sai số tuyệt đối $10^{-2}$, tức là kết quả chỉ được lệch đáp án tối đa $0{,}01$ – thiếu $0{,}5$ hay lệch vài nghìn đều bị bắt. Mình sẽ chạy thật hết để các bạn thấy rõ mỗi bẫy mất bao nhiêu điểm.

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 là một dòng 2 số. Mình đã kiểm tra: test 1–8 thoả subtask 1 ($a, b \le 1000$), test 9–20 là subtask 2.

NhómTest$a, b$ lớn nhất$a \cdot b$ lớn nhấtSố test $a \cdot b$ lẻ
(đáp án .50)
Số test $a \cdot b > 2^{31}-1$
Subtask 11–81.0001.000.00050
Subtask 29–201.000.0001.000.000.000.000410
  • Có 9 test mà $a \cdot b$ lẻ (test 2, 3, 5, 6, 8, 10, 12, 14, 17), khi đó diện tích có đuôi .50 – các test này "bắt" lỗi chia nguyên.
  • Có 10 test mà $a \cdot b$ vượt $2^{31} - 1 = 2\,147\,483\,647$ (test 9, 10, 12, 13, 15–20), lớn nhất $10^{12}$ – các test này "bắt" lỗi tràn int. Đáng chú ý test 13 là $65536 \cdot 65537$, chỉ vừa vượt $2^{32}$ một chút.

Kết quả chạy thật của các code trong bài theo từng subtask (chấm sai số tuyệt đối $10^{-2}$ như đề; dòng cuối: nếu so khớp từng ký tự):

SubtaskTestBẫy 1
/ 2 nguyên
Bẫy 2
int a, b
Bẫy 3
cout trơn
Cách 1
double
Cách 2
số nguyên
Subtask 1 (40%)
$a, b \le 1000$
1–83/88/88/88/88/8
Subtask 2 (60%)
đầy đủ: $a, b \le 10^6$
9–208/122/122/1212/1212/12
Điểm0%40%40%100%100%
Nếu so khớp
từng ký tự
11/2010/200/2020/2020/20

Ý tưởngNửa hình chữ nhật

banửa còn lại
Ghép thêm một tam giác y hệt (màu vàng) thì được hình chữ nhật $a \times b$.

Hai tam giác vuông bằng nhau ghép lại thành hình chữ nhật $a \times b$, nên $S = \dfrac{a \cdot b}{2}$. Vì $a \cdot b$ là số nguyên nên diện tích chỉ có hai khả năng:

  • $a \cdot b$ chẵn → $S$ là số nguyên, in ra đuôi .00;
  • $a \cdot b$ lẻ → $S$ có phần lẻ đúng bằng $0{,}5$, in ra đuôi .50.

Em nhập $a$, $b$ rồi bấm Tiếp để xem:

Ví dụ nhanh:

Bẫy 1Chia nguyên làm mất 0,5

Viết double S = a * b / 2; và nghĩ rằng "S là double thì sẽ ra số thực". Sai! a * b và 2 đều là số nguyên nên a * b / 2 là phép chia nguyên, tính xong (mất phần lẻ) rồi mới gán vào S: $1 \cdot 1 / 2 = 0$, $3 \cdot 7 / 2 = 10$.

C++▶ Chạy tay từng dòng codeBẫy 1: a * b / 2 là chia nguyên – thử 3 × 7.
Dữ liệu mẫu:

Kết quả chạy thật: code đúng đúng 11/20 test, sai test 2, 3, 5, 6, 8, 10, 12, 14, 17, chạy lâu nhất 0.003s – sai đúng 9 test có tích lẻ, kể cả 5/8 test của subtask 1 → 0 điểm. Thiếu $0{,}5$ lớn hơn nhiều so với sai số cho phép $0{,}01$ nên test nào có đuôi .50 cũng bị bắt, dù đáp án lớn hay nhỏ.

Chuyện bên lề: lúc đầu bài này chấm với sai số tương đối $10^{-2}$ (lệch tối đa 1% giá trị đáp án). Khi đó thiếu $0{,}5$ ở đáp án $499{,}5$ chỉ là lệch $0{,}1\%$, nên code chia nguyên này lọt tới 17/20 test – sai mà vẫn được gần hết điểm từng test. Vì vậy đề đã đổi sang sai số tuyệt đối.

Cách sửa: chia cho số thực 2.0 (hoặc nhân 1.0 * vào trước). Bản Python cũng có lỗi y hệt nếu dùng //: đúng đúng 11/20 test, sai test 2, 3, 5, 6, 8, 10, 12, 14, 17, chạy lâu nhất 0.022s.

Bẫy 2Tích a * b tràn int

Lần này đã chia 2.0 rồi, nhưng khai báo int a, b;. Phép nhân a * b vẫn được tính bằng int trước khi chia – mà $10^6 \cdot 10^6 = 10^{12}$ vượt xa giới hạn $2\,147\,483\,647$.

C++▶ Chạy tay từng dòng codeBẫy 2: int a, b nên a * b tràn – thử 65536 × 65537.
Dữ liệu mẫu:

Kết quả chạy thật: code đúng đúng 10/20 test, sai test 9, 10, 12, 13, 15–20, chạy lâu nhất 0.002s – đúng y 10 test có tích vượt int. Subtask 1 không sao (tích tối đa $10^6$) nhưng mất trọn 60% của subtask 2. Ví dụ test 13 ($65536 \cdot 65537$ $= 4\,295\,032\,832$) bị tràn thành $65536$, code in ra 32768.00.

Cách sửa: khai báo long long a, b;, hoặc ép một thừa số lên: (long long)a * b hay 1.0 * a * b. Nếu dính cả hai bẫy (int và cout << a * b / 2) thì chỉ còn đúng đúng 4/20 test, sai test 2, 3, 5, 6, 8–10, 12–20, chạy lâu nhất 0.003s.

Bẫy 3cout << S không định dạng

Tính đúng rồi nhưng in bằng cout << S; thì ví dụ 1 ra 15 chứ không phải 15.00, và số lớn ra dạng 5e+11.

C++▶ Chạy tay từng dòng codeBẫy 3: cout << S không định dạng.
Dữ liệu mẫu:

Kết quả chạy thật: code đúng đúng 10/20 test, sai test 10, 12–20, chạy lâu nhất 0.002s. Subtask 1 vẫn đúng hết vì máy chấm so giá trị: 15 bằng $15{,}00$, và với số nhỏ thì 6 chữ số có nghĩa là đủ. Nhưng ở subtask 2, 4.99999e+11 lệch đáp án $499\,999\,000\,000{,}50$ tới khoảng một triệu – sai số tuyệt đối $0{,}01$ không tha → mất 60%. Còn nếu so khớp từng ký tự thì code này đúng 0/20. Luôn in đúng định dạng đề yêu cầu.

Cách 1long long + chia 2.0 + setprecision(2)

C++▶ Chạy tay từng dòng codeCách 1: long long, chia 2.0, in 2 chữ số thập phân.
Dữ liệu mẫu:
  1. long long a, b: tích a * b tối đa $10^{12}$, nằm gọn trong long long (tối đa $\approx 9{,}2 \cdot 10^{18}$).
  2. / 2.0: chia số thực, giữ được phần $0{,}5$. double biểu diễn chính xác mọi số dạng $k/2$ với $k \le 10^{12}$, nên kết quả đúng tuyệt đối.
  3. fixed << setprecision(2): in đúng 2 chữ số sau dấu chấm (hoặc printf("%.2f", S)). Python: f"{S:.2f}".
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.003s. Bản Python đúng 20/20 test, chạy lâu nhất 0.014s. So khớp từng ký tự cũng đúng 20/20.

Dùng float S thay double thì sao? float chỉ có khoảng 7 chữ số chính xác: test 9 in ra 499999997952.00 thay vì 500000000000.00, lệch hơn 2000 đơn vị. Kết quả: đúng đúng 11/20 test, sai test 9, 10, 12, 15–20, chạy lâu nhất 0.003s – mất trọn subtask 2. Cứ dùng double.

Cách 2Không cần số thực

Vì đáp án chỉ có đuôi .00 hoặc .50, ta có thể làm hoàn toàn bằng số nguyên: phần nguyên là p / 2 (chia nguyên), đuôi là .00 nếu p chẵn, .50 nếu p lẻ. Không có sai số làm tròn nào, dù số có lớn đến đâu.

C++▶ Chạy tay từng dòng codeCách 2: chỉ dùng số nguyên – phần nguyên p / 2, đuôi .00 hoặc .50.
Dữ liệu mẫu:

Kết quả chạy thật: bản C++ đúng 20/20 test, chạy lâu nhất 0.002s. Đây là một mẹo hay dùng: khi kết quả số thực có dạng đặc biệt (chia 2, chia 4, chia 100...), tách phần nguyên và phần lẻ ra in riêng sẽ tránh được hoàn toàn chuyện sai số.

Tổng kếtKết quả chạy thật trên bộ test

CodeNgôn ngữĐúng
(sai số tuyệt đối $10^{-2}$)
ĐiểmSo khớp
từng ký tự
Ghi chú
Bẫy 1: a * b / 2 (chia nguyên)C++11/200%11/20sai test 2, 3, 5, 6, 8, 10, 12, 14, 17 – đúng y các test có tích lẻ, mất .5
Bẫy 2: int a, bC++10/2040%10/20sai test 9, 10, 12, 13, 15–20 – a * b tràn
int + cout << a * b / 2C++4/200%0/20sai test 2, 3, 5, 6, 8–10, 12–20 – dính cả hai bẫy trên
Bẫy 3: cout << SC++10/2040%0/20sai test 10, 12–20 – số lớn in dạng 4.99999e+11, lệch cả triệu
float SC++11/2040%11/20sai test 9, 10, 12, 15–20 – vd in 499999997952.00 thay vì 500000000000.00
Cách 1: long long, / 2.0, setprecision(2)C++20/20100%20/20Full điểm, chạy lâu nhất 0.003s
Cách 2: chỉ dùng số nguyênC++20/20100%20/20Full điểm, chạy lâu nhất 0.002s
Code chuẩn: f"{a * b / 2:.2f}"Python20/20100%20/20Full điểm, chạy lâu nhất 0.014s
print(a * b // 2)Python11/200%0/20sai test 2, 3, 5, 6, 8, 10, 12, 14, 17 – // là chia nguyên
print(a * b / 2)Python20/20100%0/20vẫn "lọt": giá trị đúng nhưng in 15.0, sai định dạng
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; cột "Đúng" chấm với sai số tuyệt đối $10^{-2}$ như đề (checker floatsabs của CLUE), cột "So khớp từng ký tự" để tham khảo. Với sai số tuyệt đối, mọi bẫy về tính toán đều bị bắt; chỉ còn bản Python print(a * b / 2) (in 15.0) là "lọt" vì giá trị in ra vẫn đúng tuyệt đối – nhưng sai định dạng đề yêu cầu. Thời gian đều cỡ vài phần nghìn giây. Hai bài học lớn: kiểu của phép tính do toán hạng quyết định (a * b / 2 là chia nguyên, int * int tràn trước khi gán), và in đúng định dạng đề yêu cầu.
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