- Vét cạn (Brute Force) cho kết quả đúng khi liệt kê đủ mọi ứng viên hợp lệ và kiểm tra, cập nhật đáp án đúng; chương trình vẫn có thể sai do tràn số hay đếm trùng.
- Nhìn
Nđể tính số cấu hình trước:2²⁰ = 1.048.576,400³ = 64.000.000,5000² = 25.000.000. Đây là số lượng thao tác lý thuyết, không bảo đảm thời gian trên máy chấm. - Kỹ thuật tối ưu vét cạn mạnh nhất là khử biến phụ thuộc bằng đại số: nếu ẩn cuối cùng tính được từ các ẩn trước (như
c = S - a - b), hãy bỏ hẳn vòng lặp của ẩn đó để giảm một bậc độ phức tạp. - Luôn giữ lại hàm vét cạn để làm bộ đối chiếu sinh test tự động (stress test) giúp bắt lỗi thuật toán tối ưu.
Thuật toán vét cạn (Brute Force) là gì và vì sao không được coi thường?
Vét cạn (Brute Force) — còn gọi là duyệt toàn bộ (Complete Search) — là chiến lược giải quyết bài toán theo nguyên tắc: “Thử tất cả các khả năng hợp lệ, phương án nào thỏa mãn điều kiện đề bài (hoặc tốt nhất) thì lấy.”
Ví dụ ngoài đời thực là chiếc khóa số 3 vòng (mỗi vòng từ 0 đến 9). Nếu quên mật khẩu, bạn xoay lần lượt từ 000 đến 999. Vì chỉ có 10 × 10 × 10 = 1.000 trạng thái, bạn có thể liệt kê hết các mã; thời gian thực tế tùy tốc độ thao tác. Trong các kỳ thi lập trình và phỏng vấn, vét cạn giữ 3 vai trò chiến lược:
- Lấy chắc điểm từng phần (Subtask): Nếu đề có nhóm điểm cho giới hạn nhỏ, vét cạn có thể giải được nhóm đó. Hãy đọc đúng bảng subtasks; không phải đề nào cũng chia điểm như nhau.
- Làm “máy dò lỗi” (Stress Testing) cho thuật toán tối ưu: Khi viết xong một thuật toán
O(N log N)phức tạp, bạn có thể sinh ngẫu nhiên hàng nghìn bộ dữ liệu nhỏ (N ≤ 15) rồi cho chạy đối chiếu giữa hàm vét cạn và hàm tối ưu để tăng khả năng tìm phản ví dụ; kiểm ngẫu nhiên không chứng minh thuật toán đúng trên mọi đầu vào. - Vét cạn thông minh đủ sức qua 100% test: Nhiều bài toán khi kết hợp khử biến hoặc thu hẹp miền giá trị sẽ co không gian tìm kiếm xuống còn vài triệu bước và đạt điểm tối đa (
AC).
Nhìn giới hạn N để ước lượng số cấu hình
Giới hạn thời gian thay đổi theo đề; chi phí một bước cũng tùy ngôn ngữ, phép toán và máy chấm. Không có quy đổi cố định từ số bước sang một giây. Bảng dưới chỉ giúp so sánh quy mô tìm kiếm, chưa phải kết quả đo thời gian. Trước khi gõ code (bước 3 trong 7 bước tư duy giải quyết mọi bài toán), hãy đối chiếu giới hạn N với bảng sau:
Giới hạn của N trong đề bài |
Độ phức tạp và quy mô cần cân nhắc | Dạng cấu trúc duyệt toàn bộ tương ứng |
|---|---|---|
N ≤ 10 .. 11 |
O(N!) (10! = 3.628.800) |
Duyệt mọi hoán vị của N phần tử (next_permutation / itertools.permutations) |
N ≤ 20 |
O(2ⁿ) (2²⁰ ≈ 10⁶) |
Duyệt mọi tập con của N phần tử (đệ quy quay lui hoặc mặt nạ bit) |
N ≤ 400 |
O(N³) (400³ = 6,4 × 10⁷) |
Ba vòng lặp for lồng nhau (duyệt mọi bộ ba chỉ số (i, j, k)) |
N ≤ 5.000 |
O(N²) (5000² = 2,5 × 10⁷) |
Hai vòng lặp for lồng nhau (duyệt mọi cặp (i, j) hoặc mọi đoạn con [L, R]) |
N ≤ 10⁶ .. 10⁷ |
O(N) | Một vòng lặp duy nhất (Tìm kiếm tuyến tính) |
N ≤ 10¹² .. 10¹⁴ |
O(√N) (√10¹² = 10⁶) |
Vòng lặp chỉ chạy tới căn bậc hai (i * i <= N, như liệt kê ước số) |
Nếu đề bài cho N ≤ 2.500 mà bạn dùng 3 vòng lặp lồng nhau O(N³) (2500³ ≈ 1,56 × 10¹⁰ bước), số cấu hình rất lớn, có nguy cơ quá thời gian (TLE) và bạn cần giảm đi ít nhất 1 vòng lặp.
Kỹ thuật 1: Khử vòng lặp thừa bằng biến đổi đại số — Từ O(N³) xuống O(N²)
Xét bài toán kinh điển sau:
Bài toán 1: Cho hai số nguyên không âm
NvàS(N ≤ 2.500). Hãy đếm số bộ ba số nguyên(a, b, c)thỏa mãn0 ≤ a, b, c ≤ Nvàa + b + c = S.
- Cách 1 — Vét cạn ngây thơ 3 vòng lặp
O(N³): Cho 3 vòng lặpa,b,cchạy từ0đếnN, kiểm traif (a + b + c == S). VớiN = 2.500, phải xét(2500 + 1)³ = 15.643.757.501bộ ba, thường quá lớn cho một lời giải thi đấu. - Cách 2 — Khử vòng lặp
cbằng đại sốO(N²): Khi hai vòng lặp ngoài đã cố địnhavàb, biếnckhông còn tự do nữa mà được xác định duy nhất bởi: \[c = S - a - b\] Ta bỏ hẳn vòng lặpc, tính thẳngint c = S - a - b;trongO(1)rồi chỉ cần kiểm tra xemccó nằm trong đoạn hợp lệ0 ≤ c ≤ Nhay không!
Với N = 2.500, hai vòng lặp xét đúng (N + 1)² = 6.255.001 cặp (a, b). Bài không công bố thời gian chạy vì chưa có phép đo trên máy chấm:
#include <iostream>
using namespace std;
// Cách 1: Vét cạn ngây thơ 3 vòng lặp lồng nhau O(N^3)
long long dem_bo_ba_3_vong(int n, int s) {
long long dem = 0;
for (int a = 0; a <= n; ++a) {
for (int b = 0; b <= n; ++b) {
for (int c = 0; c <= n; ++c) {
if (a + b + c == s) {
++dem;
}
}
}
}
return dem;
}
// Cách 2: Khử vòng lặp c bằng biến đổi đại số c = s - a - b -> giảm còn O(N^2)
long long dem_bo_ba_2_vong(int n, int s) {
long long dem = 0;
for (int a = 0; a <= n; ++a) {
for (int b = 0; b <= n; ++b) {
int c = s - a - b;
if (0 <= c && c <= n) {
++dem;
}
}
}
return dem;
}
int main() {
// Test 1: N = 2, S = 2 -> 6 bộ: (0,0,2), (0,1,1), (0,2,0), (1,0,1), (1,1,0), (2,0,0)
cout << "N=2, S=2 -> 3vong: " << dem_bo_ba_3_vong(2, 2)
<< " | 2vong: " << dem_bo_ba_2_vong(2, 2) << "\n";
// Test 2: N = 10, S = 15 -> 91 bộ
cout << "N=10, S=15 -> 3vong: " << dem_bo_ba_3_vong(10, 15)
<< " | 2vong: " << dem_bo_ba_2_vong(10, 15) << "\n";
// Test 3: N = 50, S = 75 -> 1951 bộ
cout << "N=50, S=75 -> 3vong: " << dem_bo_ba_3_vong(50, 75)
<< " | 2vong: " << dem_bo_ba_2_vong(50, 75) << "\n";
return 0;
}
def dem_bo_ba_3_vong(n: int, s: int) -> int:
"""Cách 1: Vét cạn ngây thơ 3 vòng lặp lồng nhau O(N^3)."""
dem = 0
for a in range(n + 1):
for b in range(n + 1):
for c in range(n + 1):
if a + b + c == s:
dem += 1
return dem
def dem_bo_ba_2_vong(n: int, s: int) -> int:
"""Cách 2: Khử vòng lặp c bằng biến đổi đại số c = s - a - b -> O(N^2)."""
dem = 0
for a in range(n + 1):
for b in range(n + 1):
c = s - a - b
if 0 <= c <= n:
dem += 1
return dem
if __name__ == "__main__":
# Test 1: N = 2, S = 2 -> 6 bộ
print(f"N=2, S=2 -> 3vong: {dem_bo_ba_3_vong(2, 2)} | 2vong: {dem_bo_ba_2_vong(2, 2)}")
# Test 2: N = 10, S = 15 -> 91 bộ
print(f"N=10, S=15 -> 3vong: {dem_bo_ba_3_vong(10, 15)} | 2vong: {dem_bo_ba_2_vong(10, 15)}")
# Test 3: N = 50, S = 75 -> 1951 bộ
print(f"N=50, S=75 -> 3vong: {dem_bo_ba_3_vong(50, 75)} | 2vong: {dem_bo_ba_2_vong(50, 75)}")
Kỹ thuật 2: Thu hẹp miền duyệt bằng bất đẳng thức — Từ O(N²) xuống O(√N)
Ngoài việc giảm số vòng lặp, ta còn có thể cắt ngắn chiều dài vòng lặp nhờ bất đẳng thức. Xét bài toán thứ hai:
Bài toán 2: Cho số nguyên dương
Nlớn tới10¹². Hãy liệt kê mọi cặp số nguyên dương(x, y)với1 ≤ x ≤ ysao chox² + y² = N.
Quá trình thu hẹp miền duyệt diễn ra qua 3 cấp độ:
- Cấp 1 — Duyệt 2 vòng
x, ytừ1 .. N(O(N²)): Tốn10²⁴bước, bất khả thi. - Cấp 2 — Khử
ybằng căn bậc hai (O(N)): Tínhy = √(N - x²)và kiểm tray * y == N - x². Nhưngxchạy tới10¹²vẫn bịTLE. - Cấp 3 — Chặn cận trên của
x(O(√N)): Vìx ≤ ynênx² ≤ y², suy ra2x² ≤ x² + y² = N. Vậy vòng lặpxchỉ cần chạy chừng nào2 * x * x <= N! VớiN = 10¹²,xchỉ chạy tối đa707.106bước; đây là cận số vòng lặp, không phải số đo thời gian.
Phương pháp duyệt trên N = 10¹² |
Độ phức tạp | Tổng số bước lặp | Nhận xét |
|---|---|---|---|
Duyệt 2 vòng x, y từ 1 .. N |
O(N²) |
10²⁴ bước |
Không thể chạy |
Duyệt x từ 1 .. N, tính y = √(N - x²) |
O(N) |
10¹² bước |
Quá thời gian (TLE) |
Duyệt x thỏa 2x² ≤ N, tính y = √(N - x²) |
O(√N) | ≈ 707.106 bước |
Cần chạy thử theo giới hạn đề |
#include <cmath>
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
// Tìm mọi cặp nguyên dương (x, y) với 1 <= x <= y sao cho x^2 + y^2 = n
// Thu hẹp miền duyệt: vì x <= y nên 2 * x^2 <= n -> chỉ tốn O(sqrt(n)) bước
vector<pair<long long, long long>> tim_cap_binh_phuong(long long n) {
vector<pair<long long, long long>> ket_qua;
for (long long x = 1; 2 * x * x <= n; ++x) {
long long phan_con = n - x * x;
long long y = (long long)round(sqrt((double)phan_con));
if (y >= x && y * y == phan_con) {
ket_qua.push_back({x, y});
}
}
return ket_qua;
}
void in_nghiem(long long n) {
auto ds = tim_cap_binh_phuong(n);
cout << "N=" << n << ": ";
if (ds.empty()) {
cout << "NONE\n";
return;
}
for (size_t i = 0; i < ds.size(); ++i) {
cout << "(" << ds[i].first << "," << ds[i].second << ")"
<< (i + 1 == ds.size() ? "\n" : " ");
}
}
int main() {
in_nghiem(25); // N=25: (3,4)
in_nghiem(50); // N=50: (1,7) (5,5)
in_nghiem(65); // N=65: (1,8) (4,7)
in_nghiem(7); // N=7: NONE
return 0;
}
import math
def tim_cap_binh_phuong(n: int) -> list[tuple[int, int]]:
"""Tìm mọi cặp nguyên dương (x, y) với 1 <= x <= y sao cho x^2 + y^2 = n trong O(sqrt(n))."""
ket_qua: list[tuple[int, int]] = []
x = 1
while 2 * x * x <= n:
phan_con = n - x * x
y = math.isqrt(phan_con)
if y >= x and y * y == phan_con:
ket_qua.append((x, y))
x += 1
return ket_qua
def in_nghiem(n: int) -> None:
ds = tim_cap_binh_phuong(n)
if not ds:
print(f"N={n}: NONE")
else:
chuoi = " ".join(f"({x},{y})" for x, y in ds)
print(f"N={n}: {chuoi}")
if __name__ == "__main__":
in_nghiem(25) # N=25: (3,4)
in_nghiem(50) # N=50: (1,7) (5,5)
in_nghiem(65) # N=65: (1,8) (4,7)
in_nghiem(7) # N=7: NONE
Lỗi hay gặp khi viết thuật toán Vét cạn
- Quên kiểm tra miền hợp lệ của biến được suy ra từ công thức. Khi tính
c = S - a - b, nhiều bạn tăng biến đếm ngay mà quên kiểm traif (0 <= c && c <= N), dẫn tới đếm nhầm cả nghiệmcâm hoặccvượt quáN. - Tràn số
intở điều kiện dừngx * x <= N. KhiN ≈ 2 × 10⁹màxkhai báo kiểuint32-bit, tíchx * xsẽ tràn thành số âm khixvượt46.340, gây lặp vô hạn. Hãy luôn khai báolong long x. - Đếm trùng cặp khi thứ tự không quan trọng. Khi duyệt các cặp chỉ số phân biệt
{i, j}, hãy cho vòng trong bắt đầu từfor (int j = i + 1; j < n; ++j)thay vìj = 0.
Luyện ngay trên Big-O Coding
Thực hành ngay kỹ năng duyệt toàn bộ và rút gọn vòng lặp qua các bài tập trên hệ thống Big-O Coding:
Câu hỏi thường gặp
Vét cạn (Brute Force) khác gì với Quay lui (Backtracking)?
Vét cạn là chiến lược xét toàn bộ ứng viên, có thể cài bằng vòng lặp hoặc đệ quy. Quay lui xây dựng nghiệm từng bước, thường bằng đệ quy, và có thể cắt nhánh khi biết nhánh đó không tạo được nghiệm hợp lệ.
Khi nào cần chuyển từ Vét cạn sang Chia để trị hoặc Tìm kiếm nhị phân?
Khi đã khử hết biến phụ thuộc mà độ phức tạp O(N²) vẫn tạo ra quá nhiều bước so với giới hạn của đề (ví dụ N = 100.000). Lúc đó hãy xem cấu trúc bài toán có cho phép dùng Tìm kiếm nhị phân, Chia để trị hoặc Quy hoạch động để giảm số bước hay không; không phải bài nào cũng có lời giải O(N log N) hoặc O(N).
Trong Python, vì sao nên dùng math.isqrt(n) thay cho int(math.sqrt(n))?
Với số nguyên lớn trên 10¹⁶, kiểu số thực float trong math.sqrt(n) bị mất độ chính xác các chữ số cuối. Hàm math.isqrt(n) tính căn bậc hai hoàn toàn trên số nguyên nên chính xác tuyệt đối với mọi số lớn.
Khóa Green cho người mới, sinh viên năm 1–2: xây nền tảng lập trình và thuật toán vững chắc, biết dùng AI hỗ trợ. 16 buổi online, học thử buổi 1 miễn phí.


