Đâ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
Chọn thuật toán theo ràng buộc — ước lượng độ phức tạp theo lũy thừa của 10
Mục tiêu bài học
Sau bài này, em đọc ràng buộc `n` trong đề, ước lượng ngược ra lớp độ phức tạp thuật toán còn khả thi, và loại ngay những cách làm chắc chắn quá giờ — tất cả trước khi viết dòng code đầu tiên. Em cũng biết cách ước lượng đúng khi đề có nhiều ràng buộc cùng lúc, thay vì cộng hay nhân chúng một cách tùy tiện.
Cần nhớ trước
Em cần nhớ lại: cách đếm số vòng lặp lồng nhau để suy ra $O(n)$, $O(n^2)$... của một đoạn code (đã học ở các khóa trước trong lộ trình), và khái niệm giới hạn thời gian chạy (thường 1 giây) ghi trong đề thi.
Ở mức chuẩn, em quen làm theo một chiều: viết thuật toán, tính $O(...)$ của nó, rồi kiểm tra xem với `n` trong đề nó có kịp giờ hay không. Đề HSG thường lật ngược trình tự đó. Người ra đề chọn `n` chính là để **buộc** em phải nghĩ đến một lớp độ phức tạp cụ thể — đọc con số đó trước, loại các cách làm chắc chắn quá giờ, rồi mới đi tìm thuật toán nằm trong phần còn lại. Làm ngược — nghĩ ra thuật toán trước, kiểm tra `n` sau — dễ khiến em cắm cúi cài đặt một cách làm không bao giờ kịp.
Bài toán mở đầu — vì sao thuật toán đúng vẫn có thể trượt
Một đề cho `n ≤ 10^5` và giới hạn 1 giây. Một học sinh viết đúng thuật toán $O(n^2)$: hai vòng lặp lồng nhau duyệt mọi cặp phần tử, logic hoàn toàn chính xác, đối chiếu tay từng test đều ra đúng đáp số. Nộp bài — báo Time Limit Exceeded (TLE) ở gần hết các test lớn. Thuật toán không sai một dòng nào, nhưng vẫn trượt điểm — vì $O(n^2)$ với `n = 10^5` là $10^{10}$ phép tính, gấp khoảng một trăm lần con số máy chấm cho phép chạy trong 1 giây. Đây là dạng lỗi mà việc "dò lại code cho kỹ" không bao giờ sửa được — phải sửa từ khâu chọn thuật toán.
Ý tưởng cốt lõi — ngân sách phép tính suy ra ngưỡng n
Mọi máy chấm gán cho mỗi giây chạy một "ngân sách" phép tính đơn giản nhất định. Con số thường được dùng làm mốc tham khảo thực dụng là khoảng $10^8$ phép tính/giây — **không phải một định luật vật lý**, chỉ là kinh nghiệm được đúc kết qua nhiều đề thi, và bản thân mốc này vốn được hiệu chỉnh theo tốc độ của ngôn ngữ biên dịch (như C++), chưa tính đến việc Python thường chạy chậm hơn cho cùng một phép tính — phần thực nghiệm cuối bài sẽ đo đúng con số đó trên Python. Từ ngân sách này và giới hạn thời gian của đề, em tính NGƯỢC ra `n` lớn nhất mà mỗi lớp độ phức tạp còn kịp giờ — đây chính là ý tưởng cốt lõi: `n` trong đề không phải một con số vô nghĩa, nó LÀ đầu mối chọn thuật toán.
Vì sao cách nghĩ này tự nhiên? Vì độ phức tạp $O(f(n))$ vốn được định nghĩa để đo tốc độ tăng của số phép tính theo `n`. Khi đã có một ngân sách cố định, phương trình $f(n) \le \text{ngân sách}$ chỉ còn là giải ngược tìm `n` — mỗi lớp $f(n)$ cho một ngưỡng `n` khác nhau, và ngưỡng đó chính là ranh giới giữa "kịp giờ" và "chắc chắn TLE".
Ví dụ
Ví dụ 1 — Bảng ngưỡng n theo từng lớp độ phức tạp
1
Bài chuẩn dừng ở việc TÍNH $O(...)$ của một thuật toán đã cho sẵn. Ở đây đề không cho thuật toán nào cả — chỉ cho `n` và giới hạn thời gian, em phải tự dựng cả bảng ngưỡng rồi tra ngược, một thao tác đòi hỏi hiểu bản chất Big-O chứ không chỉ tính theo công thức.
2
Với ngân sách $B = 10^8$ và giới hạn 1 giây, ngưỡng `n` của mỗi lớp là nghiệm lớn nhất của $f(n) \le B$. Dò chính xác bằng chặt nhị phân trên `n` (vì $f$ tăng đơn điệu), không đoán:
```python
def max_n_binary_search(op_count, budget=10**8, lo=1, hi=10**18):
"""n lon nhat sao cho op_count(n) <= budget (op_count khong giam theo n).
Mac dinh budget = 10**8 -- viet lai gia tri (thay vi tham chieu hang so
BUDGET o module-level) de khoi code nay TU DAY DU khi trich rieng ra lam
vi du cho hoc sinh, khong phu thuoc ten bien ngoai pham vi ham."""
best = lo - 1
while lo <= hi:
mid = (lo + hi) // 2
if op_count(mid) <= budget:
best = mid
lo = mid + 1
else:
hi = mid - 1
return best
```
Bảng kinh nghiệm quen thuộc nói $n \le 10$ cho $O(n!)/O(2^n)$, $n \le 20$–$22$ cho $O(2^n)$, $n \le 500$ cho $O(n^3)$, $n \le 5000$ cho $O(n^2)$, $n \le 10^6$ cho $O(n\log n)$, $n \le 10^8$ cho $O(n)$. So với con số dò chính xác ở trên, phần lớn bảng kinh nghiệm **thấp hơn** ngưỡng thật — đó là biên an toàn có chủ đích, vì một đề thi thật thường chạy nhiều test/subtask liên tiếp chứ không phải đúng một lần, và hằng số ẩn của cách cài đặt cụ thể luôn lớn hơn 1 chút ít. Riêng $O(n^3)$ là một ngoại lệ đáng chú ý: $500^3 = 1{,}25 \times 10^8$ — bản thân con số 500 đã VƯỢT ngân sách $10^8$ gần một phần tư, nên đây là quy ước làm tròn hơi lạc quan, không phải một biên an toàn. Ngưỡng dò chính xác (464) mới thật sự nằm trong ngân sách.
5
Ta có ngân sách $B = 10^8$ phép tính cho 1 giây. Với lớp $O(n^3)$, giải $n^3 \le 10^8 \Leftrightarrow n \le \sqrt[3]{10^8} \approx 464{,}16$, nên $n$ lớn nhất thỏa mãn là $n = 464$. Vậy một thuật toán $O(n^3)$ chỉ thật sự an toàn khi đề cho $n$ không vượt quá 464 — dùng mốc "500" cho lớp này cần thêm một biên dự phòng, vì bản thân 500 đã sát hoặc vượt ngân sách.
Chiều cao mỗi cột = log10(n lớn nhất còn kịp giờ) — đúng tinh thần "ước lượng độ phức tạp theo lũy thừa của 10" của bài học. Từ trái sang phải: n ≈ 11, 26, 464, 10.000, 4,5 triệu, 100 triệu.
Khi đề cho sẵn vài bài toán — luyện phân loại nhanh
Kỹ năng cần luyện không phải là thuộc lòng bảng trên, mà là phản xạ: nhìn `n`, tra ngay ra lớp độ phức tạp còn khả thi, và loại các lớp chắc chắn quá giờ — TRƯỚC khi nghĩ đến thuật toán cụ thể. Dưới đây là năm đề với `n` khác nhau; với mỗi đề, hãy chọn lớp độ phức tạp RỘNG NHẤT còn kịp giờ (rộng hơn thì thừa an toàn nhưng phí thời gian nghĩ thuật toán, hẹp hơn thì bỏ lỡ cách làm đơn giản hơn mà vẫn kịp).
Ví dụ
Ví dụ 2 — Phân loại năm đề bài theo ràng buộc n
1
Bài chuẩn hỏi "thuật toán này chạy nhanh không" cho MỘT thuật toán cụ thể. Ở đây đề không cho thuật toán nào — chỉ cho n, và em phải tự suy ra lớp độ phức tạp PHÙ HỢP cho từng trường hợp, dựa đúng bảng ngưỡng đã dò ở Ví dụ 1.
2
Nhỏ hơn cả ngưỡng $O(n!)$ (11) một chút và nằm sâu trong ngưỡng $O(2^n)$ (26) — dấu hiệu đề đang cho phép duyệt toàn bộ tập con hoặc hoán vị (vét cạn có kiểm soát), một việc chỉ khả thi với `n` cực nhỏ.
3
Vượt xa ngưỡng $O(n^3)$ (464) nên một thuật toán ba tầng vòng lặp lồng nhau chắc chắn TLE. Ngưỡng $O(n^2)$ (10.000) còn dư nhiều — đây là lớp rộng nhất còn an toàn cho `n` cỡ này.
4
Vượt xa ngưỡng $O(n^2)$. Ngưỡng $O(n\log n)$ (khoảng 4,5 triệu) còn dư rất nhiều — dấu hiệu đề đang chờ một thuật toán sắp xếp, hoặc một cấu trúc dữ liệu xử lý truy vấn theo $\log n$ mỗi lần.
5
Ngay cả $O(n)$ đơn thuần ($10^9$ phép tính) đã VƯỢT ngân sách $10^8$ tới 10 lần — một vòng lặp duyệt hết `n` phần tử theo nghĩa đen là không kịp giờ. Đề đang chờ một công thức đóng, hoặc một thuật toán $O(\sqrt{n})$/$O(\log n)$.
6
Ngay cả $O(\sqrt{n})$ ($10^9$) cũng đã vượt ngân sách. Chỉ còn lớp $O(\log n)$ (khoảng 60 phép tính, vì $\log_2 10^{18} \approx 60$) hoặc $O(1)$ là chắc chắn kịp giờ — dấu hiệu rõ ràng của công thức đóng hoặc lũy thừa nhanh (bình phương liên tiếp).
7
Tra bảng ngưỡng Ví dụ 1 theo thứ tự từ hẹp đến rộng: $n \le 15$ ứng với $O(2^n)/O(n!)$; $n \le 1.000$ ứng với $O(n^2)$ (vì đã vượt ngưỡng $O(n^3)=464$); $n \le 10^5$ ứng với $O(n\log n)$ (vì đã vượt ngưỡng $O(n^2)=10.000$); $n \le 10^9$ và $n \le 10^{18}$ đều chỉ còn $O(\log n)$ hoặc $O(\sqrt{n})$ là an toàn, vì cả hai đã vượt ngưỡng $O(n) = 10^8$. Vậy quy trình là: so `n` của đề với TỪNG ngưỡng trong bảng, từ lớp phức tạp cao nhất xuống thấp nhất, và dừng ở lớp ĐẦU TIÊN mà `n` đề bài còn nằm trong ngưỡng cho phép.
Xếp mỗi ràng buộc n vào lớp độ phức tạp RỘNG NHẤT mà nó còn kịp giờ, theo bảng ngưỡng Ví dụ 1.
Mở rộng dành cho HSG
Mở rộng dành cho HSG: khi đề có NHIỀU ràng buộc đồng thời — ví dụ số đỉnh $V \le 10^5$, số cạnh $E \le 10^6$, và $q \le 10^5$ truy vấn — không nằm trong yêu cầu chuẩn của lớp 12, vì bài chuẩn thường chỉ có một ràng buộc `n` duy nhất. Kỹ thuật xử lý: viết độ phức tạp là một BIỂU THỨC của tất cả các ràng buộc liên quan, thay số vào chính biểu thức đó, rồi mới so với ngân sách — không được nhân bừa mọi ràng buộc với nhau.
Ví dụ
Ví dụ 3 — Nhân đúng nhiều ràng buộc đồng thời
1
Bài chuẩn chỉ có một `n`. Ở đây một thuật toán đồ thị điển hình — tiền xử lý một lần rồi trả lời từng truy vấn — có độ phức tạp phụ thuộc CẢ BA đại lượng $V$, $E$, $q$ cùng lúc, và ghép sai công thức (nhân hết mọi thứ với nhau) sẽ cho một con số sai lệch khủng khiếp so với thực tế.
2
Giả sử thuật toán tiền xử lý tốn $O((V+E)\log V)$ (kiểu Dijkstra/BFS có hàng đợi ưu tiên), rồi mỗi truy vấn tốn thêm $O(\log V)$. Tổng chi phí là $O((V+E)\log V + q\log V)$ — CỘNG hai phần việc tách biệt (tiền xử lý MỘT LẦN, và $q$ truy vấn SAU ĐÓ), không nhân chúng vào nhau vì chúng không lồng nhau.
3
Với $V = 10^5$, $E = 10^6$, $q = 10^5$: $(V+E)\log_2 V + q\log_2 V \approx 19,931,569$ phép tính — nằm gọn trong ngân sách $10^8$, khả thi.
4
Nếu nhầm coi cả ba đại lượng đều lồng nhau và viết $O(V \cdot E \cdot q)$: thay số cho $V \cdot E \cdot q = 10,000,000,000,000,000$ — lớn hơn ngân sách gấp hàng trăm triệu lần. Ước lượng kiểu này khiến em tưởng nhầm một thuật toán hoàn toàn khả thi là bất khả thi, rồi bỏ lỡ cách làm đúng, hoặc ngược lại tưởng nhầm một thuật toán chậm là chấp nhận được vì "cứ nhân đại cho chắc".
5
Ta có tổng chi phí $O((V+E)\log V + q\log V)$ do hai giai đoạn (tiền xử lý và trả lời truy vấn) độc lập, CỘNG với nhau chứ không nhân. Thay $V=10^5, E=10^6, q=10^5$: kết quả xấp xỉ $19,931,569$, nhỏ hơn ngân sách $10^8$ nên thuật toán khả thi. Vậy khi đề có nhiều ràng buộc, việc đầu tiên là xác định các đại lượng đó CỘNG hay NHÂN vào nhau trong công thức độ phức tạp thật, dựa trên cấu trúc thật của thuật toán — không suy đoán theo cảm tính.
Thực nghiệm — tốc độ Python thật so với mốc 10^8/giây
Đo trực tiếp trên máy chạy bài học này: một vòng lặp Python thực hiện phép cộng, nhân, và lấy dư liên tiếp đạt khoảng 29,198,360 phép tính/giây — thấp hơn mốc $10^8$/giây khá nhiều, dù đây chỉ là phép tính số học đơn giản nhất có thể. Con số thật sẽ còn thấp hơn nữa với các phép tính phức tạp hơn (truy cập phần tử trong `list`, gọi hàm, xử lý chuỗi). Mốc "$10^8$/giây" trong bảng ngưỡng vốn được hiệu chỉnh cho ngôn ngữ biên dịch nhanh như C++ — đây là lý do nhiều đội thi HSG chọn nộp bài bằng C++ dù thiết kế và thử ý tưởng bằng Python trước, một điểm sẽ được nói kỹ hơn ở bài sau.
Sai lầm thường gặp
Sai lầm thường gặp: áp dụng máy móc bảng ngưỡng $10^8$/giây cho MỌI ngôn ngữ và MỌI phép tính như nhau. Với Python, một biên an toàn thực dụng hơn là hạ ngân sách xuống khoảng $10^7$ phép tính "nặng" (có truy cập cấu trúc dữ liệu, gọi hàm) cho mỗi giây, thay vì tin tưởng tuyệt đối vào $10^8$. Ràng buộc `n` trong đề không đổi, nhưng ngôn ngữ cài đặt quyết định em còn bao nhiêu dư địa thật sự.
Luyện tập có hướng dẫn
Luyện tập độc lập
Thử thách tổng hợp
Ghi nhớ
Ràng buộc `n` trong đề là đầu mối chọn thuật toán, không phải chi tiết phụ để đọc lướt. Ngân sách $10^8$ phép tính/giây là một mốc thực dụng, không tuyệt đối — và với Python, một biên an toàn hơn thường gần $10^7$. Khi đề có nhiều ràng buộc cùng lúc, viết đúng biểu thức độ phức tạp theo cấu trúc thật của thuật toán (cộng hay nhân) trước khi thay số, không suy đoán theo cảm tính.
Chọn đúng thuật toán mới là điều kiện cần. Bài tiếp theo xử lý điều kiện đủ: cùng một thuật toán, cách đọc dữ liệu vào và in kết quả ra cũng có thể quyết định một bài kịp giờ hay TLE — đặc biệt trong Python.
Sau bài này, em đọc ràng buộc n trong đề, ước lượng ngược ra lớp độ phức tạp thuật toán còn khả thi, và loại ngay những cách làm chắc chắn quá giờ — tất cả trước khi viết dòng code đầu tiên. Em cũng biết cách ước lượng đúng khi đề có nhiều ràng buộc cùng lúc, thay vì cộng hay nhân chúng một cách tùy tiện.
Em cần nhớ lại: cách đếm số vòng lặp lồng nhau để suy ra O(n), O(n2)... của một đoạn code (đã học ở các khóa trước trong lộ trình), và khái niệm giới hạn thời gian chạy (thường 1 giây) ghi trong đề thi.
Ở mức chuẩn, em quen làm theo một chiều: viết thuật toán, tính O(...) của nó, rồi kiểm tra xem với n trong đề nó có kịp giờ hay không. Đề HSG thường lật ngược trình tự đó. Người ra đề chọn n chính là để buộc em phải nghĩ đến một lớp độ phức tạp cụ thể — đọc con số đó trước, loại các cách làm chắc chắn quá giờ, rồi mới đi tìm thuật toán nằm trong phần còn lại. Làm ngược — nghĩ ra thuật toán trước, kiểm tra n sau — dễ khiến em cắm cúi cài đặt một cách làm không bao giờ kịp.
Một đề cho n ≤ 10^5 và giới hạn 1 giây. Một học sinh viết đúng thuật toán O(n2): hai vòng lặp lồng nhau duyệt mọi cặp phần tử, logic hoàn toàn chính xác, đối chiếu tay từng test đều ra đúng đáp số. Nộp bài — báo Time Limit Exceeded (TLE) ở gần hết các test lớn. Thuật toán không sai một dòng nào, nhưng vẫn trượt điểm — vì O(n2) với n = 10^5 là 1010 phép tính, gấp khoảng một trăm lần con số máy chấm cho phép chạy trong 1 giây. Đây là dạng lỗi mà việc "dò lại code cho kỹ" không bao giờ sửa được — phải sửa từ khâu chọn thuật toán.
Mọi máy chấm gán cho mỗi giây chạy một "ngân sách" phép tính đơn giản nhất định. Con số thường được dùng làm mốc tham khảo thực dụng là khoảng 108 phép tính/giây — không phải một định luật vật lý, chỉ là kinh nghiệm được đúc kết qua nhiều đề thi, và bản thân mốc này vốn được hiệu chỉnh theo tốc độ của ngôn ngữ biên dịch (như C++), chưa tính đến việc Python thường chạy chậm hơn cho cùng một phép tính — phần thực nghiệm cuối bài sẽ đo đúng con số đó trên Python. Từ ngân sách này và giới hạn thời gian của đề, em tính NGƯỢC ra n lớn nhất mà mỗi lớp độ phức tạp còn kịp giờ — đây chính là ý tưởng cốt lõi: n trong đề không phải một con số vô nghĩa, nó LÀ đầu mối chọn thuật toán.
Vì sao cách nghĩ này tự nhiên? Vì độ phức tạp O(f(n)) vốn được định nghĩa để đo tốc độ tăng của số phép tính theo n. Khi đã có một ngân sách cố định, phương trình f(n)≤ngaˆn saˊch chỉ còn là giải ngược tìm n — mỗi lớp f(n) cho một ngưỡng n khác nhau, và ngưỡng đó chính là ranh giới giữa "kịp giờ" và "chắc chắn TLE".
Bài chuẩn dừng ở việc TÍNH O(...) của một thuật toán đã cho sẵn. Ở đây đề không cho thuật toán nào cả — chỉ cho n và giới hạn thời gian, em phải tự dựng cả bảng ngưỡng rồi tra ngược, một thao tác đòi hỏi hiểu bản chất Big-O chứ không chỉ tính theo công thức.
Với ngân sách B=108 và giới hạn 1 giây, ngưỡng n của mỗi lớp là nghiệm lớn nhất của f(n)≤B. Dò chính xác bằng chặt nhị phân trên n (vì f tăng đơn điệu), không đoán:
def max_n_binary_search(op_count, budget=10**8, lo=1, hi=10**18):
"""n lon nhat sao cho op_count(n) <= budget (op_count khong giam theo n).
Mac dinh budget = 10**8 -- viet lai gia tri (thay vi tham chieu hang so
BUDGET o module-level) de khoi code nay TU DAY DU khi trich rieng ra lam
vi du cho hoc sinh, khong phu thuoc ten bien ngoai pham vi ham."""
best = lo - 1
while lo <= hi:
mid = (lo + hi) // 2
if op_count(mid) <= budget:
best = mid
lo = mid + 1
else:
hi = mid - 1
return best
Bảng kinh nghiệm quen thuộc nói n≤10 cho O(n!)/O(2n), n≤20–22 cho O(2n), n≤500 cho O(n3), n≤5000 cho O(n2), n≤106 cho O(nlogn), n≤108 cho O(n). So với con số dò chính xác ở trên, phần lớn bảng kinh nghiệm thấp hơn ngưỡng thật — đó là biên an toàn có chủ đích, vì một đề thi thật thường chạy nhiều test/subtask liên tiếp chứ không phải đúng một lần, và hằng số ẩn của cách cài đặt cụ thể luôn lớn hơn 1 chút ít. Riêng O(n3) là một ngoại lệ đáng chú ý: 5003=1,25×108 — bản thân con số 500 đã VƯỢT ngân sách 108 gần một phần tư, nên đây là quy ước làm tròn hơi lạc quan, không phải một biên an toàn. Ngưỡng dò chính xác (464) mới thật sự nằm trong ngân sách.
Ta có ngân sách B=108 phép tính cho 1 giây. Với lớp O(n3), giải n3≤108⇔n≤3108≈464,16, nên n lớn nhất thỏa mãn là n=464. Vậy một thuật toán O(n3) chỉ thật sự an toàn khi đề cho n không vượt quá 464 — dùng mốc "500" cho lớp này cần thêm một biên dự phòng, vì bản thân 500 đã sát hoặc vượt ngân sách.
Kỹ năng cần luyện không phải là thuộc lòng bảng trên, mà là phản xạ: nhìn n, tra ngay ra lớp độ phức tạp còn khả thi, và loại các lớp chắc chắn quá giờ — TRƯỚC khi nghĩ đến thuật toán cụ thể. Dưới đây là năm đề với n khác nhau; với mỗi đề, hãy chọn lớp độ phức tạp RỘNG NHẤT còn kịp giờ (rộng hơn thì thừa an toàn nhưng phí thời gian nghĩ thuật toán, hẹp hơn thì bỏ lỡ cách làm đơn giản hơn mà vẫn kịp).
Nhỏ hơn cả ngưỡng O(n!) (11) một chút và nằm sâu trong ngưỡng O(2n) (26) — dấu hiệu đề đang cho phép duyệt toàn bộ tập con hoặc hoán vị (vét cạn có kiểm soát), một việc chỉ khả thi với n cực nhỏ.
Vượt xa ngưỡng O(n3) (464) nên một thuật toán ba tầng vòng lặp lồng nhau chắc chắn TLE. Ngưỡng O(n2) (10.000) còn dư nhiều — đây là lớp rộng nhất còn an toàn cho n cỡ này.
Vượt xa ngưỡng O(n2). Ngưỡng O(nlogn) (khoảng 4,5 triệu) còn dư rất nhiều — dấu hiệu đề đang chờ một thuật toán sắp xếp, hoặc một cấu trúc dữ liệu xử lý truy vấn theo logn mỗi lần.
Ngay cả O(n) đơn thuần (109 phép tính) đã VƯỢT ngân sách 108 tới 10 lần — một vòng lặp duyệt hết n phần tử theo nghĩa đen là không kịp giờ. Đề đang chờ một công thức đóng, hoặc một thuật toán O(n)/O(logn).
Ngay cả O(n) (109) cũng đã vượt ngân sách. Chỉ còn lớp O(logn) (khoảng 60 phép tính, vì log21018≈60) hoặc O(1) là chắc chắn kịp giờ — dấu hiệu rõ ràng của công thức đóng hoặc lũy thừa nhanh (bình phương liên tiếp).
Tra bảng ngưỡng Ví dụ 1 theo thứ tự từ hẹp đến rộng: n≤15 ứng với O(2n)/O(n!); n≤1.000 ứng với O(n2) (vì đã vượt ngưỡng O(n3)=464); n≤105 ứng với O(nlogn) (vì đã vượt ngưỡng O(n2)=10.000); n≤109 và n≤1018 đều chỉ còn O(logn) hoặc O(n) là an toàn, vì cả hai đã vượt ngưỡng O(n)=108. Vậy quy trình là: so n của đề với TỪNG ngưỡng trong bảng, từ lớp phức tạp cao nhất xuống thấp nhất, và dừng ở lớp ĐẦU TIÊN mà n đề bài còn nằm trong ngưỡng cho phép.
Mở rộng dành cho HSG: khi đề có NHIỀU ràng buộc đồng thời — ví dụ số đỉnh V≤105, số cạnh E≤106, và q≤105 truy vấn — không nằm trong yêu cầu chuẩn của lớp 12, vì bài chuẩn thường chỉ có một ràng buộc n duy nhất. Kỹ thuật xử lý: viết độ phức tạp là một BIỂU THỨC của tất cả các ràng buộc liên quan, thay số vào chính biểu thức đó, rồi mới so với ngân sách — không được nhân bừa mọi ràng buộc với nhau.
Bài chuẩn chỉ có một n. Ở đây một thuật toán đồ thị điển hình — tiền xử lý một lần rồi trả lời từng truy vấn — có độ phức tạp phụ thuộc CẢ BA đại lượng V, E, q cùng lúc, và ghép sai công thức (nhân hết mọi thứ với nhau) sẽ cho một con số sai lệch khủng khiếp so với thực tế.
Giả sử thuật toán tiền xử lý tốn O((V+E)logV) (kiểu Dijkstra/BFS có hàng đợi ưu tiên), rồi mỗi truy vấn tốn thêm O(logV). Tổng chi phí là O((V+E)logV+qlogV) — CỘNG hai phần việc tách biệt (tiền xử lý MỘT LẦN, và q truy vấn SAU ĐÓ), không nhân chúng vào nhau vì chúng không lồng nhau.
Với V=105, E=106, q=105: (V+E)log2V+qlog2V≈19,931,569 phép tính — nằm gọn trong ngân sách 108, khả thi.
Nếu nhầm coi cả ba đại lượng đều lồng nhau và viết O(V⋅E⋅q): thay số cho V⋅E⋅q=10,000,000,000,000,000 — lớn hơn ngân sách gấp hàng trăm triệu lần. Ước lượng kiểu này khiến em tưởng nhầm một thuật toán hoàn toàn khả thi là bất khả thi, rồi bỏ lỡ cách làm đúng, hoặc ngược lại tưởng nhầm một thuật toán chậm là chấp nhận được vì "cứ nhân đại cho chắc".
Ta có tổng chi phí O((V+E)logV+qlogV) do hai giai đoạn (tiền xử lý và trả lời truy vấn) độc lập, CỘNG với nhau chứ không nhân. Thay V=105,E=106,q=105: kết quả xấp xỉ 19,931,569, nhỏ hơn ngân sách 108 nên thuật toán khả thi. Vậy khi đề có nhiều ràng buộc, việc đầu tiên là xác định các đại lượng đó CỘNG hay NHÂN vào nhau trong công thức độ phức tạp thật, dựa trên cấu trúc thật của thuật toán — không suy đoán theo cảm tính.
Đo trực tiếp trên máy chạy bài học này: một vòng lặp Python thực hiện phép cộng, nhân, và lấy dư liên tiếp đạt khoảng 29,198,360 phép tính/giây — thấp hơn mốc 108/giây khá nhiều, dù đây chỉ là phép tính số học đơn giản nhất có thể. Con số thật sẽ còn thấp hơn nữa với các phép tính phức tạp hơn (truy cập phần tử trong list, gọi hàm, xử lý chuỗi). Mốc "108/giây" trong bảng ngưỡng vốn được hiệu chỉnh cho ngôn ngữ biên dịch nhanh như C++ — đây là lý do nhiều đội thi HSG chọn nộp bài bằng C++ dù thiết kế và thử ý tưởng bằng Python trước, một điểm sẽ được nói kỹ hơn ở bài sau.
Sai lầm thường gặp: áp dụng máy móc bảng ngưỡng 108/giây cho MỌI ngôn ngữ và MỌI phép tính như nhau. Với Python, một biên an toàn thực dụng hơn là hạ ngân sách xuống khoảng 107 phép tính "nặng" (có truy cập cấu trúc dữ liệu, gọi hàm) cho mỗi giây, thay vì tin tưởng tuyệt đối vào 108. Ràng buộc n trong đề không đổi, nhưng ngôn ngữ cài đặt quyết định em còn bao nhiêu dư địa thật sự.
Ràng buộc n trong đề là đầu mối chọn thuật toán, không phải chi tiết phụ để đọc lướt. Ngân sách 108 phép tính/giây là một mốc thực dụng, không tuyệt đối — và với Python, một biên an toàn hơn thường gần 107. Khi đề có nhiều ràng buộc cùng lúc, viết đúng biểu thức độ phức tạp theo cấu trúc thật của thuật toán (cộng hay nhân) trước khi thay số, không suy đoán theo cảm tính.