Vét cạn (brute force, còn gọi là duyệt toàn bộ) giải bài toán theo cách thẳng nhất: liệt kê mọi ứng viên có thể là đáp án, kiểm từng ứng viên, rồi giữ cái tốt nhất hoặc đếm những cái thỏa. Liệt kê đủ và kiểm đúng thì kết quả đúng, không cần thêm ý tưởng nào. Lời giải vét cạn chỉ còn một cách thất bại là thời gian: số ứng viên thường tăng rất nhanh theo , và ước lượng sai là quá hạn.
Bài này dạy hai kỹ năng. Một là liệt kê các loại ứng viên hay gặp (cặp, bộ ba, đoạn con, tập con, hoán vị, chính đáp án) mà không sót, không trùng. Hai là tính trước số phép tính để biết vét cạn có kịp không, chưa kịp thì bớt ở đâu. Với đề hay , vét cạn chính là lời giải; với đề lớn hơn, nó là bộ kiểm cho lời giải nhanh.
Vét cạn là gì, và khi nào đủ nhanh
Mọi lời giải vét cạn có hai phần. Không gian ứng viên là tập những thứ ta sẽ thử, chọn sao cho mỗi đáp án thật ứng với đúng một ứng viên (khi đề yêu cầu đếm) hoặc ít nhất một ứng viên (khi chỉ cần tối ưu). Phép kiểm nhìn một ứng viên và cho biết nó có hợp lệ không, giá trị bao nhiêu. Lời giải đúng khi trả lời được ba câu: ứng viên có phủ hết đáp án không, mỗi đáp án có bị đếm đúng một lần không, phép kiểm có chính xác không.
Phần còn lại là tốc độ. Máy chấm làm được cỡ phép đơn giản mỗi giây: cộng, so sánh, đọc ghi các ô liền nhau của mảng. Phép chia, map, truy cập mảng lớn lộn xộn đều chậm hơn nhiều lần. Quy tắc thực dụng là
và chỉ cỡ vài nếu phép kiểm nặng. Các mốc quen thuộc:
| Giới hạn | Duyệt được | Số ứng viên |
|---|---|---|
| mọi hoán vị | ||
| mọi tập con | ||
| mọi bộ ba | ||
| mọi cặp, mọi đoạn con | ||
| không duyệt trực tiếp được |
Bảng chỉ là mốc thô; luôn thay số của chính đề, và đừng quên chi phí kiểm. Duyệt tập con chỉ là ứng viên, nhưng nếu mỗi tập phải cộng lại phần tử thì thành phép, và với là , hết ngân sách.
Để nói gọn, ta chỉ giữ phần tăng nhanh nhất theo và bỏ hằng số nhân: duyệt mọi cặp tốn phép, viết là , đọc là “bậc ”; duyệt mọi tập con rồi cộng lại tốn ; một việc chỉ tốn vài phép, không phụ thuộc , viết là . Ký hiệu này gọi là độ phức tạp thời gian, và từ đây mỗi lời giải đều kèm một câu như vậy.
Hỏi nhanh
Đề cho số và hỏi có bao nhiêu tập con có tổng bằng . Một bạn duyệt mọi tập con, với mỗi tập cộng lại từ đầu các phần tử được chọn (tốn khoảng phép). Ước lượng nào đúng?
Có tập con, mỗi tập cộng lại tới phần tử, tổng là . Đáp án đầu chỉ đếm số tập con mà quên chi phí kiểm; đáp án thứ ba lẫn tập con với hoán vị; đáp án cuối lẫn số phần tử với số ứng viên. Khi tổng mỗi tập được suy ra từ một tập nhỏ hơn trong (xem mục tập con), cả bài còn khoảng phép.
Ví dụĐiểm Hẹn Ít Mệt Nhất
Đề. người ở các vị trí nguyên . Chọn điểm hẹn nguyên để tổng nhỏ nhất, in tổng đó.
Lời giải. Ứng viên là điểm hẹn . Điểm hẹn ngoài đoạn không bao giờ tốt hơn điểm biên gần nó, vì kéo về phía mọi người làm mọi khoảng cách nhỏ đi, nên chỉ cần thử . Phép kiểm cộng bình phương. Cả bài phép, không cần nghĩ thêm công thức nào.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, x[105];
signed main() {
cin >> n;
for (ll i = 1; i <= n; i++) cin >> x[i];
ll ans = 1e18;
for (ll p = 1; p <= 100; p++) {
ll cost = 0;
for (ll i = 1; i <= n; i++) cost += (x[i] - p) * (x[i] - p);
ans = min(ans, cost);
}
cout << ans << '\n';
}
3
1 4 7
18
Duyệt cặp, bộ ba và suy ra biến còn lại
Ứng viên đơn giản nhất là một cặp hay một bộ ba, duyệt bằng các vòng for lồng nhau. Đếm các cặp chỉ số có :
for (ll i = 1; i <= n; i++)
for (ll j = i + 1; j <= n; j++)
if (a[i] + a[j] == k) cnt++;
Điểm đáng nói là cận j = i + 1. Cho chạy từ đầu thì mỗi cặp bị gặp hai lần dưới dạng và , lại thêm các cặp . Cận liệt kê đúng một lần mỗi cặp không thứ tự, tức cặp. Nếu đề hỏi cặp có thứ tự thì cho cả hai vòng chạy đủ rồi bỏ . Đọc kỹ xem và là một hay hai, đây là lỗi hay gặp nhất ở bài đếm cặp.
Khi các biến ràng buộc nhau bằng một đẳng thức, có cách khéo hơn lồng thêm vòng. Đếm bộ nguyên dương với và : ba vòng lặp có cỡ ứng viên, nhưng khi đã chọn , thì buộc phải bằng . Ta chỉ duyệt , , suy ra rồi kiểm có nguyên và không vượt không. Quy tắc chung: số vòng lặp bằng số biến thật sự tự do; biến còn lại tính từ các biến đã chọn, rồi kiểm đủ điều kiện của nó.
Hỏi nhanh
Sau khi chọn , , ta tính là phần nguyên của bằng hàm căn nguyên đúng. Ta cộng 1 vào đáp án khi nào?
Phần nguyên của căn chỉ cho biết ; chỉ khi bằng đúng thì tổng đó mới là bình phương của một số nguyên. Ví dụ , cho tổng và , nhưng . Hai đáp án đầu bỏ qua phép so sánh này. Đáp án cuối đúng với mọi cặp vì , nên không lọc được gì.
Ví dụBa Số Chạm Tổng
Đề. Cho và . Đếm bộ ba có thứ tự với và .
Lời giải. Ba vòng lặp có ứng viên, quá chậm. Nhưng bị xác định khi đã chọn : chỉ duyệt hai vòng, tính , và đếm nếu . Còn phép. Bộ ba có thứ tự nên mọi cặp đều được xét, không cần cận .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll k, s, ans;
signed main() {
cin >> k >> s;
for (ll x = 0; x <= k; x++)
for (ll y = 0; y <= k; y++) {
ll z = s - x - y;
if (z >= 0 && z <= k) ans++;
}
cout << ans << '\n';
}
2 2
6
Duyệt đoạn con và tính tăng dần
Một đoạn con liên tiếp là một cặp với , có đoạn. Nếu với mỗi đoạn ta cộng lại từ tới , số phép cộng là tổng độ dài mọi đoạn, cỡ ; với đó là , vượt xa .
Sự lãng phí lộ ra khi nhìn hai đoạn kề nhau: và chỉ khác đúng phần tử , vậy mà ta cộng lại từ đầu. Giữ cố định, cho tăng dần và mang theo tổng chạy: tổng bằng tổng cộng . Mỗi đoạn tốn , cả bài phép.
Nguyên tắc này rộng hơn phép cộng: khi hai ứng viên kề nhau trong thứ tự duyệt chỉ khác nhau một chút, hãy cập nhật trạng thái thay vì dựng lại từ đầu. Trạng thái có thể là tổng, tọa độ, một bảng đếm chữ cái. Nếu trạng thái chỉ xấu đi khi đoạn dài thêm (ví dụ số chữ khác nhau vượt ngưỡng), còn được break khỏi vòng .
Hỏi nhanh
Dãy có phần tử. Một chương trình duyệt mọi đoạn và với mỗi đoạn cộng lại từ đầu các phần tử từ tới . Số phép cộng gần với con số nào nhất?
Có đoạn, nhưng độ dài trung bình của một đoạn chỉ cỡ . Số phép cộng là tích hai số đó, cỡ . Đáp án coi mọi đoạn dài và có đoạn; đáp án coi mọi đoạn dài ; đáp án nhỏ nhất quên vòng cộng bên trong.
Ví dụĐoạn Lệnh Khép Kín
Đề. Xâu lệnh dài gồm U, D, L, R, mỗi lệnh dịch một ô. Đếm số đoạn liên tiếp không rỗng mà thực hiện riêng đoạn đó thì điểm quay về chỗ cũ.
Lời giải. Ứng viên là đoạn , phép kiểm là độ dời của đoạn có bằng không. Cố định , cho tăng và giữ tọa độ của đoạn : thêm lệnh chỉ sửa một tọa độ. Có đoạn, mỗi đoạn . Nhớ đặt lại mỗi khi đổi .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, ans;
string s;
signed main() {
cin >> n >> s;
for (ll l = 0; l < n; l++) {
ll x = 0, y = 0;
for (ll r = l; r < n; r++) {
if (s[r] == 'U') y++;
else if (s[r] == 'D') y--;
else if (s[r] == 'L') x--;
else x++;
if (x == 0 && y == 0) ans++;
}
}
cout << ans << '\n';
}
8
URDLRLRL
9
Duyệt mọi tập con bằng bitmask
Mỗi phần tử có hai lựa chọn, được chọn hoặc không, nên phần tử có tập con. Cách duyệt gọn nhất là đếm từ tới rồi đọc mỗi số dưới dạng chữ số nhị phân: bit bằng 1 nghĩa là phần tử được chọn. Mỗi tập con ứng với đúng một số, gọi là mask. Mục này đánh số phần tử từ 0 để phần tử ứng với bit (chữ số nhị phân thứ , đếm từ phải sang, bắt đầu từ 0).
Ba phép trên bit đủ cho mục này: 1 << n là ; mask >> i dịch các chữ số nhị phân của mask sang phải chỗ, đưa bit về hàng cuối; x & 1 giữ lại đúng chữ số cuối của x. Chẳng hạn : 13 >> 2 là , rồi 3 & 1 bằng 1, nên bit 2 của 13 đang bật.
for (ll mask = 0; mask < (1 << n); mask++) {
ll sum = 0;
for (ll i = 0; i < n; i++)
if (mask >> i & 1) sum += a[i];
if (sum == s) ways++;
}
Có mask, mỗi mask cộng tối đa phần tử, tốn : với là khoảng phép. Biểu thức mask >> i & 1 đúng vì >> ưu tiên cao hơn &. Chỗ dễ sai là các cách viết gần giống: == ưu tiên cao hơn &, nên mask & (1 << i) == 1 được hiểu là mask & ((1 << i) == 1).
Hỏi nhanh
Trong C++, biểu thức nào kiểm đúng bit của mask đang bật, với mọi ?
Dịch phải chỗ đưa bit về vị trí thấp nhất, rồi & 1 giữ lại đúng bit đó. Đáp án thứ hai sai vì == ưu tiên cao hơn &. Đáp án thứ ba có đủ ngoặc nhưng mask & (1 << i) bằng khi bit bật, chỉ so được với khi . Đáp án cuối kiểm các bit của chính số .
Khi gần giới hạn, phép kiểm tính từ đầu là phần đắt nhất. Tổng của mask suy ra được từ tổng của mask đã tắt bit 1 thấp nhất, mask đó nhỏ hơn nên đã tính xong. Trong dòng dưới, mask & (mask - 1) chính là mask đã tắt bit 1 thấp nhất, còn __builtin_ctz(mask) cho vị trí của bit đó:
for (ll mask = 1; mask < (1 << n); mask++) sum[mask] = sum[mask & (mask - 1)] + a[__builtin_ctz(mask)];
Mỗi mask còn , đổi lại ô nhớ. Vài biến thể: mỗi phần tử có ba lựa chọn thì có cấu hình, duyệt bằng số viết ở cơ số 3; muốn tập đúng phần tử thì lọc bằng __builtin_popcount(mask) == k (hàm đếm số bit 1); khi thì chia đôi tập rồi ghép lại (bài 10).
Ví dụChia Táo Thành Hai Nhóm
Đề. quả táo nặng . Chia hết táo thành hai nhóm (một nhóm có thể rỗng) sao cho chênh lệch tổng hai nhóm nhỏ nhất.
Lời giải. Mỗi cách chia ứng với tập các quả thuộc nhóm 1, tức một mask. Gọi là tổng mọi quả và là tổng nhóm 1 thì chênh lệch là . Hai cách chia và cho cùng chênh lệch, nên ghim quả cuối vào nhóm 2 và chỉ duyệt mask của quả đầu: phép. Tổng tới nên dùng long long.
Tham lam “xếp quả nặng nhất còn lại vào nhóm đang nhẹ hơn” không đúng: với nó cho và , trong khi và cùng tổng . Vét cạn không phụ thuộc vào cảm giác như vậy.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, p[25], tot;
signed main() {
cin >> n;
for (ll i = 0; i < n; i++) {
cin >> p[i];
tot += p[i];
}
ll ans = 1e18;
for (ll mask = 0; mask < (1 << (n - 1)); mask++) {
ll s = 0;
for (ll i = 0; i < n - 1; i++)
if (mask >> i & 1) s += p[i];
ans = min(ans, abs(tot - 2 * s));
}
cout << ans << '\n';
}
5
3 2 7 4 1
1
Duyệt mọi hoán vị
Khi ứng viên là một thứ tự (thứ tự thăm các thành phố, cách xếp người vào ghế) thì có ứng viên, tăng nhanh hơn rất nhiều: , , . Vét cạn hoán vị chỉ dùng được khi hoặc , và phép kiểm phải rẻ.
C++ có sẵn next_permutation(first, last): biến dãy thành hoán vị đứng ngay sau nó theo thứ tự từ điển và trả về true; nếu dãy đã giảm dần thì trả về false. Bắt đầu từ dãy tăng dần rồi gọi liên tục sẽ đi qua mọi hoán vị đúng một lần. Dãy có phần tử trùng thì hàm chỉ đi qua các hoán vị phân biệt.
Hàm làm ba việc: tìm vị trí lớn nhất có (điểm gãy), đổi với số nhỏ nhất lớn hơn nó trong phần đuôi, rồi đảo phần đuôi cho tăng dần. Đuôi sau điểm gãy giảm dần, tức đã là cách xếp lớn nhất của nó; muốn lớn hơn ít nhất thì phải tăng lên giá trị nhỏ nhất có thể rồi xếp phần còn lại nhỏ nhất có thể.
Hai quy tắc khi dùng: phải sắp tăng dần trước, vì hàm chỉ đi tiếp từ vị trí hiện tại; và viết do { ... } while (next_permutation(...)), vì vòng while đặt điều kiện trước sẽ bỏ qua hoán vị đầu tiên.
Hỏi nhanh
Một bạn khai báo p = {3, 1, 2} rồi chạy do { ... } while (next_permutation(p, p + 3)); mà không sắp xếp. Phần thân vòng lặp chạy mấy lần?
Hàm chỉ đi tới hoán vị kế tiếp theo thứ tự từ điển. Sau là ; đã lớn nhất nên lần gọi tiếp trả về false. Bốn hoán vị , , , đứng trước nên không bao giờ được thăm.
Ví dụHoán Vị Cột Số
Đề. xâu, mỗi xâu đúng chữ số. Chọn một hoán vị các vị trí và áp dụng cho mọi xâu, rồi đọc mỗi xâu như một số. Tìm hiệu nhỏ nhất giữa số lớn nhất và số nhỏ nhất.
Lời giải. Ứng viên là hoán vị của cột, có cái. Với mỗi hoán vị, dựng lại số rồi lấy lớn nhất trừ nhỏ nhất: phép. Cả bài khoảng phép. Số có tới 8 chữ số, vẫn vừa int, nhưng dùng ll cho chắc.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, k, p[10];
string s[10];
signed main() {
cin >> n >> k;
for (ll i = 0; i < n; i++) cin >> s[i];
for (ll j = 0; j < k; j++) p[j] = j;
ll ans = 1e18;
do {
ll lo = 1e18, hi = -1e18;
for (ll i = 0; i < n; i++) {
ll v = 0;
for (ll j = 0; j < k; j++) v = v * 10 + s[i][p[j]] - '0';
lo = min(lo, v);
hi = max(hi, v);
}
ans = min(ans, hi - lo);
} while (next_permutation(p, p + k));
cout << ans << '\n';
}
2 2
01
10
9
Duyệt theo đáp án và sinh ứng viên
Đôi khi ứng viên tốt nhất lại chính là đáp án. Có hai cách dùng.
Thử đáp án tăng dần. Tìm số nhỏ nhất có tổng chữ số chia hết cho 4: cho chạy từ lên tới khi thỏa. Cách này chỉ nhanh khi chứng minh được đáp án nằm sát . Ở đây mười số cùng phần đầu, khác nhau ở chữ số cuối, có tổng chữ số là mười số nguyên liên tiếp, nên đáp án luôn cách không quá đơn vị. Cận này cũng có thể tìm ra bằng cách cho vét cạn chạy trên mọi nhỏ rồi lấy khoảng cách lớn nhất.
Sinh ứng viên thay vì lọc. Đếm các số không vượt chỉ gồm chữ số và . Duyệt từng số rồi kiểm chữ số mất cỡ phép. Nhưng số như vậy rất ít: dưới chỉ có số. Thay vì lọc cả miền giá trị, ta sinh trực tiếp: từ một số đã dựng, nối thêm chữ số hoặc . Khi số vừa dựng đã vượt thì nối thêm chỉ làm nó lớn hơn, nên bỏ cả nhánh. Số ứng viên giờ bằng số đối tượng hợp lệ, không phải độ lớn của miền giá trị. Hàm tự gọi lại chính nó, đây là hình hài đầu tiên của quay lui (bài 07).
Ví dụSố May Mắn Cân Bằng Gần Nhất
Đề. Số may mắn chỉ gồm chữ số và , cân bằng nếu số chữ số bằng số chữ số . Với , tìm số cân bằng nhỏ nhất không nhỏ hơn .
Lời giải. Sinh mọi số gồm và có tới chữ số, giữ số cân bằng và không nhỏ hơn , lấy nhỏ nhất. Mười chữ số là đủ vì . Có số, rất ít. Đáp án có thể có 10 chữ số, vượt int.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, ans = 1e18;
void gen(ll x, ll c4, ll c7) {
if (x > 1e10) return;
if (c4 == c7 && x >= n) ans = min(ans, x);
gen(x * 10 + 4, c4 + 1, c7);
gen(x * 10 + 7, c4, c7 + 1);
}
signed main() {
cin >> n;
gen(0, 0, 0);
cout << ans << '\n';
}
4500
4747
Ở lời gọi đầu cũng “cân bằng” nhưng không bao giờ được chọn vì .
Thu hẹp số ứng viên
Lời giải vét cạn đúng rồi mới tới chuyện nhanh. Có bốn cách bớt việc, phần lớn đã gặp ở trên.
Cận chặt và break đúng chỗ. Cho bắt đầu từ , thoát vòng khi . Mỗi break phải dựa trên một tính đơn điệu có thật; một break sai bỏ sót đáp án một cách lặng lẽ.
Dừng sớm. Khi chỉ cần một đáp án, hay đáp án đầu tiên theo một thứ tự, duyệt theo thứ tự ấy và dừng ngay khi gặp.
Gộp ứng viên đối xứng. Hai ứng viên luôn cho cùng kết quả thì chỉ duyệt một, như ghim một quả táo vào nhóm 2 để giảm từ xuống .
Làm phép kiểm rẻ. Kiểm “tổng hai số có nguyên tố không” bằng chia thử tốn cỡ phép; một sàng Eratosthenes (bài 08) dựng trước cho câu trả lời bằng một lần đọc mảng.
Ngoài bốn cách trên còn một cách mạnh hơn, không có công thức chung: dùng một nhận xét toán học để chứng minh chỉ một phần nhỏ ứng viên có thể là đáp án, rồi chỉ duyệt phần đó.
Ví dụBa Số Có Tận Cùng Bằng 3
Đề. Mảng số nguyên dương. Có chọn được ba phần tử ở ba vị trí khác nhau mà tổng có chữ số hàng đơn vị là không? Tổng qua các test không quá .
Lời giải. Duyệt bộ ba trên cả mảng là , không thể. Nhận xét: chữ số cuối của tổng chỉ phụ thuộc chữ số cuối của từng số, và một bộ ba dùng mỗi chữ số cuối nhiều nhất ba lần. Vậy với mỗi chữ số cuối chỉ cần giữ lại tối đa ba phần tử, còn lại không quá số. Duyệt mọi bộ ba của số là phép mỗi test. Đây là vét cạn trên một không gian đã được thu hẹp có chứng minh.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll t, n, cnt[10], v[35], m;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n;
memset(cnt, 0, sizeof cnt);
m = 0;
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
if (cnt[x % 10] < 3) {
cnt[x % 10]++;
v[++m] = x % 10;
}
}
bool ok = false;
for (ll i = 1; i <= m; i++)
for (ll j = i + 1; j <= m; j++)
for (ll k = j + 1; k <= m; k++)
if ((v[i] + v[j] + v[k]) % 10 == 3) ok = true;
cout << (ok ? "YES" : "NO") << '\n';
}
}
3
5
10 21 32 44 55
4
10 20 40 80
3
1 1 1
YES
NO
YES
Vét cạn làm bộ kiểm
Lời giải nhanh thường tinh vi và dễ sai; vét cạn thì đơn giản tới mức khó sai. Hai chương trình cùng giải một đề phải cho cùng đáp án trên mọi đầu vào, nên chỉ cần một bộ sinh test nhỏ ngẫu nhiên và một phép so sánh:
- Viết
bruteđơn giản nhất có thể. - Sinh test với và giá trị đều nhỏ, vì lỗi thường lộ ngay ở dữ liệu nhỏ: , số âm, số bằng nhau.
- Chạy hàng nghìn test, dừng ở test đầu tiên hai bên khác nhau.
- Đọc test ấy bằng mắt: nó nhỏ nên lỗi thường hiện ra ngay.
Vét cạn còn hai việc nữa. Một là lấy điểm: nhiều đề chia subtask, subtask đầu có giới hạn nhỏ chính là dành cho vét cạn. Hai là tìm quy luật: chạy brute trên nhiều đầu vào nhỏ rồi nhìn kết quả để đoán công thức hay một cận, như cận ở mục trước.
Ví dụĐoạn Có Tổng Lớn Nhất
Đề. Dãy số nguyên, . Tìm tổng lớn nhất của một đoạn con liên tiếp không rỗng.
Lời giải. Vét cạn mọi đoạn là , quá chậm. Cách : duyệt dãy, giữ cur là tổng lớn nhất của đoạn kết thúc tại vị trí hiện tại (nếu cur trước đó âm thì bỏ nó đi, bắt đầu lại từ phần tử này), và ans là giá trị lớn nhất của các cur. Khởi tạo ans phải là chứ không phải , vì đoạn không được rỗng: dãy toàn số âm có đáp án âm.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll cur = 0, ans = -1e18;
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
cur = max(cur, 0LL) + x;
ans = max(ans, cur);
}
cout << ans << '\n';
}
8
-5 4 -1 2 1 -7 6 3
9
Lỗi khởi tạo ans = 0 không lộ ra ở test mẫu, nhưng bộ kiểm tìm ra ngay. Chương trình dưới chạy brute (mọi đoạn) và fast (cách trên) trên 20000 dãy ngắn ngẫu nhiên, lần lượt với hai giá trị khởi tạo:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, a[10];
ll brute() {
ll best = -1e18;
for (ll l = 1; l <= n; l++) {
ll sum = 0;
for (ll r = l; r <= n; r++) {
sum += a[r];
best = max(best, sum);
}
}
return best;
}
ll fast(ll init) {
ll cur = 0, best = init;
for (ll i = 1; i <= n; i++) {
cur = max(cur, 0LL) + a[i];
best = max(best, cur);
}
return best;
}
signed main() {
mt19937 rng(2024);
for (ll init : {0LL, (ll)-1e18}) {
bool ok = true;
for (ll t = 1; t <= 20000 && ok; t++) {
n = rng() % 6 + 1;
for (ll i = 1; i <= n; i++) a[i] = (ll)(rng() % 11) - 5;
if (brute() != fast(init)) {
cout << "ans = " << init << ": sai ở test " << t << ", dãy";
for (ll i = 1; i <= n; i++) cout << ' ' << a[i];
cout << ", đúng là " << brute() << ", in ra " << fast(init) << '\n';
ok = false;
}
}
if (ok) cout << "ans = " << init << ": 20000 test đều khớp\n";
}
}
ans = 0: sai ở test 4, dãy -3, đúng là -3, in ra 0
ans = -1000000000000000000: 20000 test đều khớp
Phản ví dụ nhỏ hết mức: dãy một phần tử . Hạt giống 2024 cố định nên lần chạy nào cũng cho cùng dãy test, lỗi tái hiện được. Khi bài lớn hơn, người ta tách thành ba chương trình (sinh test, brute, fast) và một script so đầu ra, nhưng nguyên tắc vẫn như trên.
Khi đi thi
- Đọc giới hạn trước khi nghĩ cách làm: gợi hoán vị, gợi tập con, vài nghìn gợi cặp và đoạn con. Nhân số ứng viên với chi phí kiểm rồi so với trước khi viết dòng code đầu tiên.
- Gặp ràng buộc đẳng thức thì chỉ duyệt các biến tự do và suy ra biến còn lại, nhớ kiểm biến suy ra có hợp lệ; tổng và tích dùng
long long. - Viết vét cạn trước để lấy điểm subtask nhỏ, giữ nó làm bộ kiểm, và chưa nộp lời giải nhanh nào khi chưa so nó với vét cạn trên các test nhỏ ngẫu nhiên.
Tóm tắt
- Vét cạn là một không gian ứng viên cùng một phép kiểm; đúng khi ứng viên không sót, không đếm trùng và phép kiểm chính xác.
- Thời gian ước lượng bằng số ứng viên nhân chi phí kiểm, ngưỡng khoảng : tới , tới , bộ ba tới vài trăm, cặp tới vài nghìn.
- Cặp dùng cận để không trùng; ràng buộc đẳng thức cho phép bỏ một vòng lặp bằng cách suy ra biến còn lại.
- Ứng viên kề nhau chỉ khác một chút thì cập nhật tăng dần: tổng đoạn từ đoạn ngắn hơn một phần tử, tổng mask từ mask đã tắt bit thấp nhất.
- Tập con duyệt bằng mask từ tới , hoán vị bằng
next_permutation(sắp tăng dần trước, dùngdo ... while). - Vét cạn là bộ kiểm: so với lời giải nhanh trên nhiều test nhỏ ngẫu nhiên để lấy phản ví dụ nhỏ nhất.
Bài tập
long long.Đọc thêm
- Complete SearchUSACO GuideBài mẫu duyệt mọi cặp điểm và một bảng khoảng hai mươi bài vét cạn gắn nhãn độ khó.
- Phép toán bitVNOI WikiBitmask biểu diễn tập hợp, duyệt tập con và các phép toán tập hợp trên mask.
- Enumerating submasks of a bitmaskcp-algorithmsDuyệt tập con của một mask và vì sao duyệt mọi cặp lồng nhau chỉ tốn .
- 枚举OI WikiChọn đối tượng duyệt, thu hẹp phạm vi và dùng đối xứng để bớt một nửa.
- Competitive Programmer's Handbook, chương 5Sinh tập con và hoán vị bằng đệ quy lẫn bằng bit, rồi quay lui và cắt tỉa.