Hãy xếp các số thành một hàng sao cho hai số kề nhau không hơn kém đúng 1. Bạn thử vài lần là ra, ví dụ 3 1 4 2. Bài xây dựng (constructive) là loại bài như thế: không hỏi một con số mà đòi in ra một đối tượng (dãy, hoán vị, ma trận, xâu) thỏa các điều kiện, hoặc báo không tồn tại.
Thường có rất nhiều đáp án đúng và trình chấm kiểm từng điều kiện. Không có thuật toán chung, nhưng có một quy trình: tìm điều kiện cần để biết khi nào vô nghiệm, đoán một mẫu đơn giản cho các trường hợp còn lại, rồi chứng minh mẫu đó thỏa từng điều kiện.
Điều kiện cần
Trước khi cố xây, hỏi: nếu có đáp án thì nó phải thỏa gì? Có thể là tính chẵn lẻ của một tổng, đại lượng không đổi qua các phép, giới hạn trên của một đại lượng, hay đơn giản là thử bằng tay.
Điều kiện cần tìm được thường chính là điều kiện đủ. Khi đó nửa còn lại của bài là xây một đáp án cho mọi trường hợp thỏa điều kiện. Nếu không xây được cho một trường hợp, hoặc điều kiện chưa đủ, hoặc mẫu chưa đúng: thử trường hợp đó bằng vét cạn.
Hỏi nhanh
Cần số chẵn và số lẻ có tổng hai nhóm bằng nhau. Với , vì sao chắc chắn không có đáp án?
Tổng của một số lẻ các số lẻ là số lẻ, không bao giờ bằng một tổng chẵn. Vậy phải chẵn, tức chia hết cho 4; đó là điều kiện cần, và mục sau cho thấy nó cũng đủ.
Ví dụHai Nửa Cân Bằng
Đề. Với chẵn (), in số dương khác nhau: số chẵn trước, số lẻ sau, hai nửa có tổng bằng nhau; không được thì in NO.
Lời giải. Đặt . lẻ thì vô nghiệm như câu hỏi trên. chẵn: lấy các số chẵn , tổng , và các số lẻ , tổng . Số lẻ cuối cùng bù phần thiếu: . Nó lẻ vì chẵn, và lớn hơn nên không trùng số nào. Với , , số cuối là 17.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll n;
cin >> n;
ll h = n / 2;
if (h % 2) {
cout << "NO\n";
continue;
}
cout << "YES\n";
for (ll i = 1; i <= h; i++) cout << 2 * i << ' ';
for (ll i = 1; i < h; i++) cout << 2 * i - 1 << ' ';
cout << 3 * h - 1 << '\n';
}
}
3
12
14
16
YES
2 4 6 8 10 12 1 3 5 7 9 17
NO
YES
2 4 6 8 10 12 14 16 1 3 5 7 9 11 13 23
Một công thức
Khi đáp án là vài con số, hãy thử biểu diễn chúng qua dữ liệu vào bằng một công thức, thường là những bội đơn giản. Viết công thức ra rồi kiểm từng điều kiện của đề một cách riêng rẽ: đẳng thức, chia hết, khác nhau, nằm trong miền. Điều kiện nào hỏng ở trường hợp biên (một tham số bằng 1, hai số trùng nhau) thì hoặc đó là trường hợp vô nghiệm, hoặc phải tách riêng.
Ví dụBộ Ba Chia Hết
Đề. Số “loại I” chia hết cho , số “loại II” chia hết cho nhưng không chia hết cho . Tìm ba số dương khác nhau , đúng một số loại I và hai số loại II; không có thì in NO. .
Lời giải. thì mọi bội của đều chia hết cho , không có số loại II: vô nghiệm. : lấy , , . Đẳng thức đúng. loại I. và chia hết cho ; và không chia hết cho khi , nên chúng là loại II. Ba số khác nhau vì . Mọi số không quá .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll a, b;
cin >> a >> b;
if (b == 1) cout << "NO\n";
else cout << "YES\n" << a * b << ' ' << a * (b - 1) << ' ' << a * (2 * b - 1) << '\n';
}
}
3
9 1
6 4
11 7
NO
YES
24 18 42
YES
77 66 143
Xếp theo lớp
Điều kiện kiểu “hai phần tử kề nhau không hơn kém đúng 1” hay “hiệu hai số kề nằm trong ” gợi ý chia các số thành lớp theo chẵn lẻ (hay theo số dư): trong cùng một lớp, hai số liên tiếp đã cách nhau 2.
Xếp hết một lớp rồi tới lớp kia. Việc còn lại là kiểm chỗ nối giữa hai lớp, thường cần chọn khéo hai đầu nối. Trường hợp nhỏ không đủ chỗ cho chỗ nối thì kiểm bằng tay hay vét cạn: chúng thường là các trường hợp vô nghiệm.
Ví dụMa Trận Không Kề Nhau
Đề. Điền vào ma trận () sao cho hai ô chung cạnh không chứa hai số liên tiếp; không được thì in .
Lời giải. : mỗi ô kề hai ô khác, số 2 có hai láng giềng phải tránh 1 và 3 nhưng chỉ còn ô chéo, vô nghiệm. Với , điền theo từng hàng các số lẻ rồi tiếp các số chẵn . Hai ô kề ngang cùng lớp hơn kém 2; hai ô kề dọc cùng lớp hơn kém . Ô lẻ cạnh ô chẵn chỉ xảy ra ở chỗ nối, nơi số lẻ gần còn số chẵn gần 2, hơn kém nhiều khi .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll n;
cin >> n;
if (n == 2) {
cout << -1 << '\n';
continue;
}
ll cur = 1;
for (ll i = 0; i < n; i++) {
for (ll j = 0; j < n; j++) {
cout << cur << ' ';
cur += 2;
if (cur > n * n) cur = 2;
}
cout << '\n';
}
}
}
4
1
2
3
4
1
-1
1 3 5
7 9 2
4 6 8
1 3 5 7
9 11 13 15
2 4 6 8
10 12 14 16
Bất biến
Có điều kiện nhìn phức tạp nhưng tương đương một điều kiện toàn cục đơn giản. “XOR các vị trí lẻ bằng XOR các vị trí chẵn” tương đương “XOR cả mảng bằng 0”, vì khi và chỉ khi . “Tổng hai nửa bằng nhau” là “tổng có dấu bằng 0”.
Khi đó chọn tự do gần hết các phần tử, rồi dùng vài phần tử cuối để bù cho bất biến đúng. Phần khó là bảo đảm phần tử bù không trùng các phần tử đã chọn, thường bằng cách cho nó mang những bit cao mà các phần tử khác không có.
Hỏi nhanh
Vì sao “XOR các phần tử ở vị trí lẻ bằng XOR các phần tử ở vị trí chẵn” tương đương “XOR cả mảng bằng 0”?
Gọi , là XOR hai nhóm vị trí. XOR một số với chính nó bằng 0, và chỉ khi hai số bằng nhau; XOR cả mảng là vì XOR giao hoán và kết hợp. Điều này đúng với mọi , chẵn hay lẻ.
Ví dụCân Bằng XOR Theo Vị Trí
Đề. Với , in số không âm khác nhau nhỏ hơn có XOR các vị trí lẻ bằng XOR các vị trí chẵn.
Lời giải. Cần XOR cả mảng bằng 0. Lấy , tất cả nhỏ hơn ; thêm , , và phần tử cuối là XOR của mọi số đã có. Phần tử cuối có cả bit 20 lẫn bit 21, nên khác mọi số trước: các số nhỏ không có bit 20, còn và chỉ có một trong hai bit. XOR cả mảng bằng 0 theo cách chọn phần tử cuối.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll n, x = 0;
cin >> n;
for (ll i = 1; i <= n - 3; i++) {
cout << i << ' ';
x ^= i;
}
ll p = 1 << 20, q = 1 << 21;
cout << p << ' ' << q << ' ' << (x ^ p ^ q) << '\n';
}
}
3
3
4
6
1048576 2097152 3145728
1 1048576 2097152 3145729
1 2 3 1048576 2097152 3145728
Đẩy về cực đại
Khi đề cho phép tăng (hay thêm, hay bật) tùy ý, hãy xét đáp án cực đại: tăng mọi thứ tới mức lớn nhất còn có thể hợp lệ. Thường đáp án cực đại hoặc đúng, hoặc nếu nó sai thì mọi đáp án khác cũng sai.
Chẳng hạn trên lưới, ô có giá trị cần láng giềng dương, nên không vượt số ô chung cạnh của nó. Có ô vượt thì vô nghiệm. Không có ô nào vượt thì đặt mọi ô bằng đúng số ô chung cạnh: mọi ô đều dương nên điều kiện đúng với dấu bằng.
Vét cạn tìm quy luật
Khi không đoán được mẫu, viết một chương trình vét cạn trên kích thước nhỏ (, xâu dài ), in ra mọi đáp án hay đáp án tốt nhất, rồi nhìn. Các đáp án có chung hình dạng gì? Đại lượng tối ưu tăng theo quy luật nào?
Từ đó phát biểu một mẫu, chứng minh nó, và giữ chương trình vét cạn để so với lời giải trên nhiều test ngẫu nhiên. Một quy luật hay gặp: tích của nhiều thừa số lớn nhất khi tổng cố định thì các thừa số đều nhau, hơn kém nhau không quá 1.
Ví dụXâu Ngắn Nhất Nhiều Dãy Con
Đề. Cho . In xâu ngắn nhất có ít nhất dãy con bằng codeforces.
Lời giải. Vét cạn trên nhỏ cho thấy đáp án tốt nhất luôn có dạng: chữ thứ của codeforces lặp lần liền nhau, theo đúng thứ tự. Số dãy con khi đó là , độ dài là tổng các . Với tổng cố định, tích lớn nhất khi các hơn kém nhau không quá 1; nên bắt đầu với mọi và lần lượt tăng vòng tròn tới khi tích đạt . Mỗi lần tăng thêm 1 ký tự và tích tăng nhanh nhất có thể. Với , tăng bốn chữ đầu cho tích 16.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll k, c[10];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> k;
string s = "codeforces";
for (ll i = 0; i < 10; i++) c[i] = 1;
for (ll i = 0;; i = (i + 1) % 10) {
ll prod = 1;
for (ll j = 0; j < 10; j++) prod *= c[j];
if (prod >= k) break;
c[i]++;
}
for (ll i = 0; i < 10; i++) cout << string(c[i], s[i]);
cout << '\n';
}
12
ccooddeeforces
Khi đi thi
- Thử tay trước tiên: các trường hợp vô nghiệm thường nằm ở đó, và đáp án của chúng gợi ý mẫu chung.
- Viết một hàm kiểm đáp án theo đúng điều kiện của đề và một chương trình vét cạn nhỏ; chạy mẫu xây dựng trên mọi nhỏ trước khi nộp.
- In đáp án nhanh: tổng kích thước đầu ra có thể tới hay số, dùng
'\n'thay choendlvà tắt đồng bộ luồng.
Tóm tắt
- Điều kiện cần (chẵn lẻ, tổng, kích thước nhỏ) loại các trường hợp vô nghiệm và thường cũng là điều kiện đủ.
- Một công thức hay một mẫu xếp duy nhất cho mọi trường hợp còn lại, kiểm từng điều kiện riêng rẽ.
- Chia theo lớp chẵn lẻ hay số dư, rồi chỉ lo chỗ nối giữa các lớp.
- Đưa điều kiện về một bất biến toàn cục rồi dùng vài phần tử cuối để bù.
- Đáp án cực đại thường đúng khi đề cho phép tăng tùy ý; vét cạn nhỏ giúp tìm và kiểm quy luật.
Bài tập
Đọc thêm
- 构造OI Wiki
- Constructive AlgorithmsCodeforces problemset