Đây là bản xem thử. Phiên bản đầy đủ trong ứng dụng có bài tập, AI chấm ngay và theo dõi tiến độ học.

Bài học

Thiết kế hàm và chia bài toán thành hàm con

Mục tiêu bài học

Sau bài này, em chia được một chương trình dài, nhiều bước thành các hàm nhỏ — mỗi hàm làm đúng một việc, có tên rõ nghĩa, tham số và giá trị trả về tường minh — rồi kiểm thử từng hàm độc lập bằng assert trước khi ghép chúng lại thành chương trình hoàn chỉnh.

Cần nhớ trước

Em cần nhớ lại: cú pháp def, tham số và return trong Python; rẽ nhánh if/elif/else; và cách viết thuật toán thử chia để kiểm tra một số có phải số nguyên tố hay không.

**So với mức chuẩn:** bài tập lớp 10 thường chỉ yêu cầu viết một hàm cho một thao tác đơn giản, ví dụ `def binh_phuong(x): return x * x`. Đề HSG hiếm khi hỏi thẳng "viết một hàm" — nó cho một bài toán nhiều bước, và việc đầu tiên em phải tự làm là **quyết định chia bài toán thành những hàm nào**. Chia sai — gộp hết vào một khối lệnh dài — khiến code khó đọc, khó test, và cực khó sửa khi phát hiện lỗi giữa lúc làm bài dưới áp lực thời gian.

Bài toán mở đầu — khi một khối lệnh dài trở nên khó kiểm soát

Giả sử đề bài: chương trình đọc nhiều bộ test, mỗi bộ gồm hai số $L, R$, in ra số lượng số nguyên tố trong đoạn $[L, R]$. Nếu viết toàn bộ logic — thử chia từng số, đếm, in kết quả — trong một vòng lặp lồng duy nhất, chương trình vẫn có thể chạy đúng. Nhưng khi đề bài đổi thêm một yêu cầu nhỏ (ví dụ: "in luôn số nguyên tố lớn nhất trong đoạn"), em phải mò lại toàn bộ khối lệnh đó để chèn thêm logic vào đúng chỗ — và rất dễ chèn nhầm, làm hỏng phần đang chạy đúng.

Ý tưởng cốt lõi — mỗi hàm một việc

Chia chương trình thành các hàm nhỏ, mỗi hàm chịu trách nhiệm đúng một việc: một hàm kiểm tra tính chất của MỘT phần tử (ví dụ `is_prime(n)`), một hàm khác dùng lại hàm đó để xử lý cả một tập hợp (ví dụ `count_primes(l, r)`). Khi cần thêm yêu cầu mới, em chỉ thêm một hàm mới hoặc sửa đúng một hàm cũ — phần còn lại không bị ảnh hưởng. Tên hàm rõ nghĩa và tham số tường minh còn giúp em (và người chấm code) hiểu ngay hàm làm gì mà không cần đọc hết phần thân.
Ví dụ
Ví dụ 1 — Tách is_prime(n) và count_primes(l, r)
  1. 1
    Đề chuẩn chỉ hỏi "viết hàm kiểm tra số nguyên tố". Ở đây đề cần đếm số nguyên tố trong CẢ MỘT đoạn $[L, R]$, và có thể có nhiều bộ test khác nhau — nếu viết gộp việc kiểm tra và việc đếm vào chung một vòng lặp, em sẽ phải chép lại logic kiểm tra nguyên tố mỗi khi cần dùng nó ở chỗ khác.
  2. 2
    Tách thành hai hàm độc lập: `is_prime(n)` chỉ lo kiểm tra MỘT số, `count_primes(l, r)` gọi lại `is_prime` cho từng số trong đoạn rồi đếm. `count_primes` không cần biết CÁCH `is_prime` hoạt động bên trong — nó chỉ cần tin rằng hàm đó trả lời đúng.
  3. 3
    ```python def is_prime(n: int) -> bool: if n < 2: return False if n % 2 == 0: return n == 2 i = 3 while i * i <= n: if n % i == 0: return False i += 2 return True ``` Thử chia từ 3, chỉ xét số lẻ, dừng khi $i^2 > n$ — độ phức tạp $O(\sqrt{n})$ cho mỗi số.
  4. 4
    def count_primes(l: int, r: int) -> int:
        return sum(1 for x in range(l, r + 1) if is_prime(x))
    

    Hàm này chỉ có MỘT dòng vì toàn bộ việc khó — kiểm tra nguyên tố — đã được is_prime lo, đúng tinh thần "mỗi hàm một việc".

  5. 5
    ```python assert is_prime(2) is True assert is_prime(1) is False assert is_prime(97) is True assert count_primes(1, 10) == 4 # 2, 3, 5, 7 assert count_primes(10, 20) == 4 # 11, 13, 17, 19 ``` Em test `is_prime` TRƯỚC, riêng biệt — nếu nó đúng, em có thể tin `count_primes` mà không cần dò lại từng bước thử chia bên trong nữa.
  6. 6

    Khi một hàm gọi lại một hàm khác đã được test kỹ, em không cần hiểu lại toàn bộ chi tiết bên trong hàm đó — đây chính là lợi ích lớn nhất của việc chia nhỏ: mỗi lần chỉ cần nghĩ về MỘT tầng logic.

Kỹ thuật chia hàm không chỉ dùng cho các bài số học. Bài toán phân loại — vốn có nhiều điều kiện lồng nhau — cũng được lợi rất nhiều khi tách phần "kiểm tra hợp lệ" ra khỏi phần "phân loại".

Ví dụ
Ví dụ 2 — Tách is_valid_triangle và classify_triangle
  1. 1

    Đề chuẩn thường cho sẵn ba cạnh hợp lệ và chỉ hỏi loại tam giác. Đề HSG thường bắt em tự kiểm tra dữ liệu vào có hợp lệ không TRƯỚC — kể cả trường hợp nhập sai thứ tự cạnh hoặc ba cạnh không tạo thành tam giác — rồi mới phân loại nếu hợp lệ.

  2. 2
    Tách `is_valid_triangle(a, b, c)` — chỉ trả lời đúng/sai về tính hợp lệ — ra khỏi `classify_triangle(a, b, c)` — chỉ lo phân loại, và **gọi `is_valid_triangle` ngay dòng đầu tiên để chặn sớm** (early return) nếu dữ liệu không hợp lệ.
  3. 3
    ```python def is_valid_triangle(a, b, c): sides = sorted([a, b, c]) if sides[0] <= 0: return False return sides[0] + sides[1] > sides[2] def classify_triangle(a, b, c): if not is_valid_triangle(a, b, c): return "khong hop le" x, y, z = sorted([a, b, c]) if x == y == z: return "deu" if abs(x * x + y * y - z * z) < 1e-9: return "vuong can" if x == y else "vuong" if x == y or y == z: return "can" return "thuong" ``` `sorted([a, b, c])` giải quyết luôn việc "nhập sai thứ tự cạnh" — cạnh lớn nhất luôn ở vị trí cuối, nên điều kiện tam giác và điều kiện vuông (Pythagoras) chỉ cần viết đúng một lần.
  4. 4
    `classify_triangle` KHÔNG cần biết `is_valid_triangle` kiểm tra bằng bất đẳng thức tam giác nào — nó chỉ cần gọi và tin kết quả. Đây là early return: chặn trường hợp không hợp lệ ngay từ đầu để phần code phía dưới luôn được viết với giả định "dữ liệu chắc chắn hợp lệ", không cần lồng thêm điều kiện kiểm tra ở mọi nhánh.
  5. 5
    assert classify_triangle(2, 2, 2) == "deu"
    assert classify_triangle(3, 4, 5) == "vuong"
    assert classify_triangle(2, 2, 3) == "can"
    assert classify_triangle(2, 3, 4) == "thuong"
    assert classify_triangle(1, 2, 3) == "khong hop le"  # 1 + 2 = 3, khong tao tam giac
    
  6. 6
    Thói quen viết hàm `is_valid()`/`check()` tách riêng rồi gọi nó chặn sớm ở đầu hàm chính là kỹ năng tổ chức code rất hữu ích khi thi đấu dưới áp lực thời gian — tái sử dụng được cho nhiều bài kiểm tra điều kiện phức tạp khác, không riêng gì tam giác.

Chia hàm còn hữu ích khi bài toán không chỉ "kiểm tra" mà còn "tìm kiếm" trong một danh sách — tách phần so sánh ra một hàm phụ giúp hàm chính đọc gọn hơn hẳn.

Ví dụ
Ví dụ 3 — Tách diff(x, target) và closest_invoice(invoices, target)
  1. 1

    Cho danh sách hóa đơn (đơn vị đồng) và một mốc tiền target. Tìm hóa đơn có tổng tiền GẦN mốc đó nhất. Nếu danh sách rỗng, trả về None.

  2. 2
    Tách `diff(x, target)` — đo khoảng cách giữa một hóa đơn và mốc — ra khỏi `closest_invoice` — duyệt danh sách và giữ hóa đơn tốt nhất. `closest_invoice` kiểm tra danh sách rỗng NGAY ĐẦU HÀM (early return `None`) để phần duyệt phía dưới không cần lo trường hợp danh sách trống.
  3. 3
    def diff(x, target):
        return abs(x - target)
    
    def closest_invoice(invoices, target):
        if not invoices:
            return None
        best = invoices[0]
        for x in invoices[1:]:
            if diff(x, target) < diff(best, target):
                best = x
        return best
    
  4. 4
    ```python assert closest_invoice([120000, 95000, 150000, 99000], 100000) == 99000 assert closest_invoice([200, 150, 180], 160) == 150 assert closest_invoice([], 100) is None ``` Với `[120000, 95000, 150000, 99000]` và mốc $100000$: khoảng cách lần lượt là $20000, 5000, 50000, 1000$ — nhỏ nhất là $1000$, ứng với hóa đơn $99000$.
  5. 5
    Vì `diff` là một hàm riêng, muốn đổi bài toán từ "tìm hóa đơn GẦN mốc nhất" sang "tìm hóa đơn XA mốc nhất", em chỉ cần đổi dấu so sánh trong vòng lặp (`<` thành `>`) — không phải viết lại công thức khoảng cách.
Bẫy thường gặp
Ba lỗi hay gặp khi chia hàm: (1) một hàm ôm quá nhiều việc — vừa đọc dữ liệu, vừa tính, vừa in kết quả — khiến không thể test riêng từng phần; (2) đặt tên hàm mơ hồ như `xu_ly()`, `ham1()` khiến chính em cũng quên hàm đó làm gì sau vài phút; (3) để hàm phụ thuộc vào biến toàn cục thay vì nhận tham số rõ ràng — hàm chạy đúng lúc test riêng nhưng cho kết quả sai khi ghép vào chương trình lớn vì biến toàn cục đã bị nơi khác thay đổi giá trị.
Bảng phân loại tên hàm: nhóm hàm rõ một việc dễ test bên trái, nhóm hàm ôm nhiều việc khó test bên phải.Hàm rõ một việc— dễ testis_prime(n)count_primes(l,r)is_valid_triangle(a,b,c)diff(x, target)Hàm ôm nhiềuviệc — khó testxu_ly_toan_bo()doc_tinh_in_mot_luot()ham_lam_moi_thu()giai_bai_toan()
Tên hàm ở cột trái mô tả đúng MỘT việc; tên ở cột phải không cho biết hàm đang làm gì bên trong.

Luyện tập có hướng dẫn

Luyện tập độc lập

Thử thách

Tóm tắt

Chia một chương trình thành các hàm nhỏ, mỗi hàm một việc, giúp em test từng phần độc lập bằng `assert`, dễ sửa khi có lỗi, và dễ tái sử dụng khi đề bài đổi yêu cầu. Thói quen viết hàm `is_valid()`/`check()` tách riêng để chặn sớm (early return) đặc biệt hữu ích khi bài toán có nhiều điều kiện phức tạp.

Bài tiếp theo học đệ quy — một cách đặc biệt để một hàm gọi lại chính nó, mở rộng thêm một công cụ chia nhỏ bài toán mà em vừa luyện tập ở đây.