Ca biên — rỗng, một phần tử, số 0, số âm, trùng
Lời giải chạy đúng với ca thường rồi gãy với danh sách rỗng. Năm ca biên này gây phần lớn lỗi của người mới, và cả năm đều soi ra được trước khi chạy dòng nào.
TL;DR: Lời giải tìm điểm cao nhất chạy đúng với [7, 9, 5] không nói gì về việc nó có chạy đúng với mọi danh sách hay không. Bài này soi một đoạn code qua năm ca: danh sách rỗng làm nó gãy ngay dòng thứ hai, một phần tử và hai điểm trùng thì chạy đúng, có số 0 cũng đúng nhưng chỉ vì code tránh được một bẫy khởi tạo, còn số âm thì đề cấm mà code chẳng chặn. Cả năm ca đều đọc ra được từ ô Vào, trước khi chạy dòng nào.
Đoạn code tìm điểm cao nhất trong [7, 9, 5] chạy ra đúng một con số: 9. Bạn thử lại với [3, 8, 1], vẫn đúng. Bạn tin nó đã xong việc.
Nó chưa xong. Bạn mới thử những con số bình thường — không rỗng, không âm, không trùng nhau. Đề bài không hứa điều đó với mọi lần chạy sau; chỉ bạn tự hứa với chính mình khi chọn số để thử tay.
Lỗi lệch một đơn vị chính là ca biên đầu tiên bạn đã gặp, chỉ chưa ai gọi tên nó. Ca biên không mới — bạn dính nó rồi, chỉ chưa ai gọi tên cho nó thôi.
Bài này lo đúng một việc: soi năm ca dữ liệu mà người mới hầu như không tự nghĩ ra để thử, trên đúng đoạn code bạn đã tin là đúng.
1. Analogy — biển tải trọng ở đầu cầu
Bạn không cần lái xe tải qua cầu để biết cầu có sập hay không. Đầu cầu có sẵn tấm biển ghi giới hạn — "tối đa 8 tấn" — kỹ sư đã tính con số đó từ lúc thiết kế, không đợi cầu sập thật mới biết.
Ràng buộc trong ô Vào đóng đúng vai tấm biển đó — nói trước, ngay khi bạn đọc đề, dữ liệu nào an toàn và dữ liệu nào không, không đợi bạn chạy thử rồi mới báo lỗi.
| Đầu cầu | Đề bài |
|---|---|
| Biển ghi tải trọng tối đa | Ràng buộc ghi trong ô Vào |
| Đọc biển trước khi lái qua | Soi ca biên trước khi chạy dòng nào |
| Xe quá tải làm cầu sập | Dữ liệu ca biên làm code gãy hoặc trả lời sai |
| Kỹ sư tính giới hạn từ lúc thiết kế, không đợi cầu sập | Bạn đọc ra ca biên từ lúc đọc đề, không đợi máy báo lỗi |
| Cầu này giới hạn 8 tấn, cầu khác giới hạn 30 tấn | Mỗi đề bài có tập ca biên riêng — rỗng, 0, âm, trùng chỉ là năm loại phổ biến nhất |
Biển báo không đợi tai nạn xảy ra mới treo lên. Ca biên cũng vậy — đọc ra được trước khi chạy dòng đầu tiên.
2. Lời giải cũ, năm câu hỏi mới
Đây là lời giải cho chính cái đề bạn đã bóc thành ba ô ở bài 1 — tìm điểm cao nhất — và nó chạy đúng với [7, 9, 5]:
diem = [7, 9, 5]
cao_nhat = diem[0]
for d in diem:
if d > cao_nhat:
cao_nhat = d
print(cao_nhat)
Output thật:
9
Đúng đoạn code này, không đổi một dòng nào, năm câu hỏi khác nhau đặt ra khi diem là:
- danh sách rỗng
- đúng một phần tử
- danh sách có số 0
- danh sách có số âm
- hai bạn trùng điểm cao nhất
3. Ca biên đọc ra được từ đâu, nếu không phải chạy thử?
Từ chính ô Vào. Đề của lời giải trên chỉ nói "cho một danh sách điểm, tìm điểm cao nhất" — và câu đó im lặng về ba thứ:
- Danh sách có bắt buộc khác rỗng không? Đề không nói.
- Điểm số có bắt buộc lớn hơn 0 không? Đề không nói (hoặc nói mà code không kiểm tra).
- Nếu hai người cùng đạt điểm cao nhất thì kết quả tính sao? Đề không nói.
Mỗi chỗ đề "không nói" chính là một ca biên. Không cần chạy máy để phát hiện ra chúng — chỉ cần đọc lại ô Vào và tự hỏi: giá trị nhỏ nhất mà ô này còn chấp nhận là gì, và giá trị ngay sát bên ngoài giới hạn đó thì sao?
4. Thử đoán — năm ca, một đoạn code
Với đúng đoạn code ở mục 2, không sửa gì cả, đoán xem chuyện gì xảy ra khi diem lần lượt là: [], [8], [0, 6, 2], [7, -3, 9], [9, 5, 9]. Với mỗi trường hợp, viết ra: chạy đúng, chạy sai, hay dừng chương trình vì lỗi — và vì sao.
diem = [] — GÃY. Output thật khi chạy:
Traceback (most recent call last):
File "tim_diem_cao_nhat.py", line 2, in <module>
cao_nhat = diem[0]
~~~~^^^
IndexError: list index out of range
Chương trình chưa kịp vào vòng lặp đã dừng. Dòng gãy là cao_nhat = diem[0] — lấy phần tử đầu tiên làm mốc, nhưng danh sách rỗng không có phần tử đầu tiên nào cả.
Danh sách rỗng nghĩa là thân vòng lặp for chạy đúng 0 vòng — điều kiện "còn phần tử để duyệt" sai ngay từ lần hỏi đầu tiên, nên mọi thứ bạn định làm bên trong thân đều không xảy ra.
Nhưng cái gãy ở đây không nằm trong thân vòng lặp — nó nằm ở dòng khởi tạo, chạy TRƯỚC khi vòng lặp kịp hỏi câu đầu tiên. Vòng lặp "an toàn với 0 vòng" hay không cũng không cứu được dòng đọc trước nó.
diem = [8] — chạy đúng, ra 8. Mốc cao_nhat được đặt bằng chính phần tử duy nhất. Vòng lặp so nó với chính mình, 8 > 8 sai, không có gì đổi — đúng ý nghĩa "điểm cao nhất của một người là điểm của người đó".
diem = [0, 6, 2] — chạy đúng, ra 6. Code KHÔNG khởi tạo cao_nhat bằng 0 — nó khởi tạo bằng diem[0], phần tử thật đầu tiên, dù phần tử đó là 0 hay không. Số 0 được so sánh như một điểm thật, không bị coi là "ô trống".
So với phiên bản khởi tạo bằng hằng số 0 — kiểu code nhiều người mới viết theo bản năng:
diem = [-8, -3, -5]
cao_nhat = 0
for d in diem:
if d > cao_nhat:
cao_nhat = d
print(cao_nhat)
Output thật:
0
Không ai đạt điểm 0 ở đây — điểm cao nhất thật là -3. Nhưng vì mốc là hằng số 0 và không phần tử nào vượt qua nó, cao_nhat không bao giờ bị ghi đè, và kết quả là một con số không hề có trong dữ liệu. Lời giải ở mục 2 tránh được bẫy này chỉ vì nó lấy mốc từ chính dữ liệu, không phải từ một hằng số nghĩ sẵn.
diem = [7, -3, 9] — chạy đúng để tìm max, ra 9, nhưng đó không phải toàn bộ câu chuyện. Về số học, 9 đúng là lớn nhất trong ba số. Nhưng đề ngầm định điểm không âm — -3 lẽ ra không nên xuất hiện. Code ở mục 2 không kiểm tra chuyện đó; nó nhận -3 y như nhận 9, không báo gì cả. Ràng buộc "không âm" chỉ tồn tại trên giấy, không tồn tại trong code.
diem = [9, 5, 9] — chạy đúng, ra 9. Hai bạn cùng đạt 9 không làm > bối rối: mốc đã là 9, gặp thêm một số 9 thì 9 > 9 sai, không đổi gì. Code trả lời đúng "điểm cao nhất là bao nhiêu" — nó chỉ chưa từng được hỏi "có mấy người đạt điểm đó".
Năm ca gộp lại thành một bức tranh — và đây là chỗ đối chiếu với những gì bạn vừa đoán:

Ba hộp cam là ca gãy hoặc trả lời sai. Hai hộp xanh lá chạy đúng — nhưng "chạy đúng" không có nghĩa là khỏi cần soi; bạn chỉ biết nó đúng SAU khi đã soi.
5. Tới lượt bạn
Cùng kỹ năng đọc ô Vào trước khi chạy, áp cho một hàm khác: tính điểm trung bình của một lớp.
diem = []
tong = 0
for d in diem:
tong = tong + d
trung_binh = tong / len(diem)
print(trung_binh)6. Bẫy thường gặp
❌ Nhầm 1 — thử vài ca "bình thường" rồi coi như đã kiểm tra đủ. [7, 9, 5] và [3, 8, 1] đều là dữ liệu tự nghĩ ra, và dữ liệu tự nghĩ ra gần như luôn "đẹp": không rỗng, không âm, không trùng.
✅ Quay lại ô Vào và hỏi riêng cho từng ràng buộc: giá trị nhỏ nhất hợp lệ là gì, và giá trị ngay sát bên ngoài đó thì sao.
❌ Nhầm 2 — khởi tạo biến lớn nhất bằng 0 cho "chắc ăn". Nhìn có vẻ an toàn vì 0 là số nhỏ, nhưng nó âm thầm giả định điểm cao nhất luôn lớn hơn 0.
✅ Khởi tạo mốc bằng chính một phần tử thật của dữ liệu — diem[0] — không phải một hằng số nghĩ sẵn.
❌ Nhầm 3 — tin rằng đề bài cấm là code cũng cấm. Đề nói "điểm không âm" không có nghĩa dòng code nào đó tự động từ chối số âm.
✅ Ràng buộc trên giấy chỉ có hiệu lực khi có một dòng if thật sự kiểm tra nó.
7. 📚 Đào sâu — vì sao ca biên hay bị bỏ sót
Lý do không nằm ở việc bạn lười, mà ở cách con người chọn ví dụ. Khi cần một con số để thử tay, bộ não gần như luôn chọn một con số "điển hình" — không to, không nhỏ, không đặc biệt. Kỹ thuật kiểm thử gọi nhóm dữ liệu đặc biệt này là giá trị biên (boundary value) — đúng ranh giới của một ràng buộc, hoặc ngay sát bên ngoài nó — và chúng gần như không bao giờ xuất hiện tự nhiên trong đầu người thử tay, vì theo định nghĩa chúng không "điển hình".
Cách chống là đọc lại từng ràng buộc trong ô Vào và hỏi đúng một cặp câu: giá trị nhỏ nhất còn hợp lệ là gì, và giá trị ngay sát bên ngoài — không còn hợp lệ nữa — là gì. "Danh sách điểm" kéo ra câu hỏi về số lượng tối thiểu; "điểm số" kéo ra câu hỏi về 0 và số âm.
8. Liên hệ các bài khác
- Bài 1 — Đọc đề — ô Vào là nơi mọi ca biên trong bài này bắt nguồn.
- Bài 4 — Phân rã — mỗi bước phân rã cũng cần soi ca biên riêng, không chỉ ở bước cuối.
- Bài 3 module 2 — Vòng lặp while — cơ chế "0 vòng chạy" là nền cho việc hiểu ca danh sách rỗng.
- Bài 5 module 2 — Lặp lồng và lệch một đơn vị — ca biên đầu tiên bạn từng gặp, chỉ chưa được gọi tên.
- Bài 7 — Mini challenge — tự soi ca biên cho một đề hoàn toàn mới, không có bảng dựng sẵn.
9. Tóm tắt
- Trước khi tin một lời giải đã "chạy đúng", tự hỏi mình đã thử ca nào ngoài dữ liệu đẹp chưa — nếu chưa, bạn mới chỉ chạy một nửa phép thử.
- Danh sách rỗng làm lời giải ở mục 2 gãy ngay dòng khởi tạo
cao_nhat = diem[0]; cùng ca đó làm hàm trung bình ở mục 5 gãy ở phép chia, vì rỗng lộ ra khác nhau tuỳ thuật toán dùng danh sách vào việc gì. - Mốc so sánh phải lấy từ chính dữ liệu, không phải từ một hằng số nghĩ sẵn —
cao_nhat = diem[0]sống sót với điểm âm,cao_nhat = 0thì không. - Ràng buộc chỉ tồn tại trên giấy cho tới khi có một dòng
ifkiểm tra nó; đọc lại ô VÀO rồi tìm xem dòng đó nằm ở đâu trong code. - Một phần tử và điểm trùng chạy đúng với lời giải này, nhưng "đúng" chỉ biết được sau khi soi, không phải mặc định.
10. Tự kiểm tra
- Q1Vì sao lời giải tìm điểm cao nhất chạy đúng với [7, 9, 5] không đủ để kết luận nó đúng với mọi danh sách?
- Q2Với đoạn code ở mục 2, vì sao
diem = []làm chương trình dừng bằng lỗi, chứ không chỉ đơn giản in ra rỗng hay 0? - Q3Vì sao khởi tạo biến lớn nhất bằng diem[0] an toàn hơn khởi tạo bằng hằng số 0?
- Q4Đề bài cấm điểm số âm. Vì sao đoạn code ở mục 2 vẫn nhận và xử lý được diem = [7, -3, 9]?
- Q5Hai bạn cùng đạt điểm cao nhất có làm đoạn code ở mục 2 chạy sai không? Vì sao?
- Q6Hàm tính điểm trung bình ở mục 5 gãy vì cùng lý do gì với lời giải tìm điểm cao nhất ở ca rỗng, và khác nhau ở chỗ nào?
Bài tiếp theo: Mini challenge — từ đề tới lưu đồ hoàn chỉnh
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên