Trong bài tương tác, dữ liệu không nằm sẵn trong input. Một chương trình của ban tổ chức, gọi là trình tương tác (interactor), giữ một trạng thái bí mật; lời giải gửi câu hỏi theo một giao thức cố định, đọc câu trả lời, và cuối cùng in kết luận. Cái khó thường không nằm ở thuật toán mà ở hai chỗ: mỗi câu hỏi phải lấy được đủ thông tin để không vượt ngân sách câu hỏi, và chương trình phải tuân thủ giao thức đến từng dòng.
Giao thức hỏi và đáp
Lời giải và trình tương tác nối với nhau bằng hai đường ống: những gì lời giải in ra là input của trình tương tác và ngược lại. Một phiên thường gồm:
- Đọc dữ liệu công khai, chẳng hạn .
- Lặp: in một dòng câu hỏi đúng định dạng, đẩy bộ đệm, đọc câu trả lời.
- In dòng kết luận, đẩy bộ đệm, kết thúc.
Đẩy bộ đệm (flush) là bắt buộc: cout gom dữ liệu trong bộ nhớ và chỉ thật sự gửi khi đầy; câu hỏi còn kẹt trong bộ đệm thì trình tương tác không thấy, còn lời giải đứng chờ câu trả lời, hai bên chờ nhau tới hết giờ (thường báo Idleness Limit Exceeded hoặc Time Limit Exceeded). endl vừa xuống dòng vừa đẩy bộ đệm, nên dùng endl cho mọi dòng gửi đi. ios::sync_with_stdio(false) và cin.tie(0) vẫn dùng được, miễn là đẩy bộ đệm đúng lúc.
Khi câu hỏi sai hoặc vượt ngân sách, trình tương tác thường trả rồi dừng. Lời giải phải thoát ngay, nếu cứ đọc tiếp có thể nhận một lỗi khó hiểu thay vì Wrong Answer. Gói việc hỏi vào một hàm ask để mọi câu hỏi cùng đi qua một chỗ.
Tự kiểm thử. Đoạn hội thoại mẫu trong đề chỉ là một phiên; không thể dán nó vào input như bài thường. Viết một trình tương tác nhỏ (Python là đủ): sinh trạng thái bí mật ngẫu nhiên, đọc từng dòng của lời giải, kiểm định dạng và ngân sách, trả lời, rồi so kết luận. Chạy vài trăm phiên ngẫu nhiên và vài phiên biên ( nhỏ nhất, phần tử cần tìm ở hai đầu).
Hỏi nhanh
Một lời giải dùng cout << "? " << i << ' ' << j << '\n'; rồi đọc ngay câu trả lời, không có endl hay flush. Chuyện gì dễ xảy ra nhất?
Định dạng dòng vẫn đúng, nhưng dữ liệu chưa rời bộ đệm nên trình tương tác không nhận được câu hỏi. Mặc định cin gắn với cout và tự đẩy trước khi đọc; chính cin.tie(0) đã gỡ việc đó, nên phải tự endl hoặc flush.
Ví dụDãy Sau Những Tổng Đôi
Đề. Dãy ẩn số nguyên dương, . Câu hỏi ? i j nhận ; được hỏi không quá câu, rồi in ! và cả dãy.
Lời giải. Ba câu , , cho ba phương trình ba ẩn: , rồi suy ra . Mỗi với tốn đúng một câu . Tổng câu, đúng ngân sách. Hỏi các cặp cũng được; điều cần là mỗi câu thêm đúng một ẩn mới.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, a[5005];
ll ask(ll i, ll j) {
cout << "? " << i << ' ' << j << endl;
ll x = -1;
cin >> x;
if (x == -1) exit(0);
return x;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll s12 = ask(1, 2), s23 = ask(2, 3), s13 = ask(1, 3);
a[1] = (s12 + s13 - s23) / 2;
a[2] = s12 - a[1];
a[3] = s13 - a[1];
for (ll i = 4; i <= n; i++) a[i] = ask(i - 1, i) - a[i - 1];
cout << '!';
for (ll i = 1; i <= n; i++) cout << ' ' << a[i];
cout << endl;
}
Câu hỏi phải tách được các khả năng
Trước khi tìm cách hỏi, đếm xem cần phân biệt bao nhiêu khả năng. Câu hỏi có kiểu trả lời thì câu phân biệt được tối đa khả năng; cần phân biệt khả năng thì phải hỏi ít nhất câu. Câu hỏi có/không () cho vị trí trong ô cần ít nhất 18 câu, nên ngân sách 20 câu gợi ý tìm kiếm nhị phân; ngân sách câu gợi ý mỗi câu chốt một phần tử.
Không phải mọi bài cần chốt hẳn trạng thái. Bài chỉ hỏi một tính chất (nguyên tố hay hợp số, chẵn hay lẻ) thì chỉ cần câu hỏi tách được hai nhóm khả năng, và nhiều khi một danh sách câu hỏi cố định, hỏi mù không cần nhìn câu trả lời trước, là đủ. Khi đó việc chính là chứng minh danh sách ấy tách được mọi trường hợp.
Ví dụPhân Loại Số Bí Mật
Đề. Số bí mật trong . Câu hỏi là một số , nhận yes nếu chia hết . Hỏi không quá 20 câu rồi in prime hoặc composite.
Lời giải. Hỏi 15 số nguyên tố không quá 47 và bốn bình phương , tổng 19 câu; đếm số câu yes. Nếu là hợp số, viết với là hai thừa số, nguyên tố. Khi có một ước nguyên tố khác không quá , tức không quá 47, nên ít nhất hai số nguyên tố trong danh sách chia hết . Khi chỉ có một ước nguyên tố thì và , nên và cả lẫn đều trả yes. Nếu nguyên tố thì chỉ có thể chính trả yes. Vậy hợp số khi và chỉ khi có ít nhất hai câu yes; đủ hai thì dừng sớm.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll q[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 4, 9, 25, 49};
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll cnt = 0;
for (ll x : q) {
cout << x << endl;
string s;
cin >> s;
if (s == "yes") cnt++;
if (cnt >= 2) break;
}
cout << (cnt >= 2 ? "composite" : "prime") << endl;
}
Tìm kiếm nhị phân bằng câu hỏi
Một câu hỏi có/không đơn điệu theo vị trí (sai, sai, …, đúng, đúng) là khuôn của tìm kiếm nhị phân (bài 05): mỗi câu loại một nửa số khả năng, câu là đủ. Việc cần làm là biến câu trả lời thành điều kiện đơn điệu. Câu hỏi tổng đoạn cho số bit 1 trên tiền tố, nên số bit 0 trên tiền tố là trừ tổng, tăng theo . Câu hỏi “phần tử lớn thứ hai trong đoạn” cho biết phần tử lớn nhất nằm cùng phía với vị trí đó hay không. Mỗi câu chỉ cần thêm một bit thông tin đúng chỗ.
Ví dụSố Không Thứ K
Đề. Mảng nhị phân ẩn dài ; đọc , , rồi . Câu hỏi ? l r nhận tổng các bit trên ; hỏi không quá 20 câu rồi in ! x, vị trí của số 0 thứ .
Lời giải. Số 0 trong là , không giảm theo . Vị trí cần tìm là nhỏ nhất có ít nhất số 0 trong : tìm kiếm nhị phân trên , mỗi bước hỏi ? 1 m. Có câu.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, t, k;
ll ask(ll l, ll r) {
cout << "? " << l << ' ' << r << endl;
ll x = -1;
cin >> x;
if (x == -1) exit(0);
return x;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> t >> k;
ll l = 1, r = n, res = n;
while (l <= r) {
ll mid = (l + r) >> 1;
if (mid - ask(1, mid) >= k) r = (res = mid) - 1;
else l = mid + 1;
}
cout << "! " << res << endl;
}
Ví dụVị Trí Lớn Nhất
Đề. Mảng ẩn số khác nhau. Câu hỏi ? l r với nhận chỉ số của phần tử lớn thứ hai trong . Tìm chỉ số phần tử lớn nhất với không quá 40 câu.
Lời giải. Hỏi cả mảng được , vị trí lớn thứ hai. Một đoạn chứa trả lời đúng khi và chỉ khi nó cũng chứa phần tử lớn nhất . Hỏi ? 1 s (khi ) để biết nằm bên trái hay bên phải . Bên trái thì ? m s trả khi và chỉ khi : tìm lớn nhất như vậy. Bên phải thì ? s m trả khi và chỉ khi : tìm nhỏ nhất. Tổng câu.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n;
ll ask(ll l, ll r) {
cout << "? " << l << ' ' << r << endl;
ll x = -1;
cin >> x;
if (x == -1) exit(0);
return x;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll s = ask(1, n), res;
if (s > 1 && ask(1, s) == s) {
ll l = 1, r = s - 1;
res = 1;
while (l <= r) {
ll mid = (l + r) >> 1;
if (ask(mid, s) == s) l = (res = mid) + 1;
else r = mid - 1;
}
} else {
ll l = s + 1, r = n;
res = n;
while (l <= r) {
ll mid = (l + r) >> 1;
if (ask(s, mid) == s) r = (res = mid) - 1;
else l = mid + 1;
}
}
cout << "! " << res << endl;
}
Hỏi theo cặp để chốt từng phần tử
Khi ngân sách là cỡ câu, hãy tìm một cặp câu hỏi mà dù trả lời thế nào cũng chốt chắc một phần tử. Ý quen thuộc là hỏi một quan hệ không đối xứng theo cả hai chiều. Với phép lấy dư trên hai số dương khác nhau : , còn . Vậy hỏi cả hai chiều, kết quả lớn hơn chính là số nhỏ hơn trong hai số. Duy trì một “ứng viên lớn nhất”, so với từng phần tử mới: số nhỏ hơn của cặp được chốt, số lớn hơn thành ứng viên mới. Ứng viên còn lại cuối cùng là giá trị lớn nhất, suy ra không cần hỏi.
Hỏi nhanh
Hoán vị ẩn, hai vị trí có và . Ta biết gì?
Kết quả lớn hơn, 7, là ; nếu thì , nên . Không thể , vì khi đó , mâu thuẫn với 7. Còn chỉ biết lớn hơn 7.
Ví dụHoán Vị Qua Phần Dư
Đề. Hoán vị ẩn của , . Câu hỏi ? i j nhận . Hỏi không quá câu rồi in cả hoán vị.
Lời giải. Giữ chỉ số ứng viên mx, ban đầu là 1. Với mỗi : hỏi và . Nếu thì và thành ứng viên mới; ngược lại . Cuối cùng . Tổng câu; thì in luôn.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, p[10005];
ll ask(ll i, ll j) {
cout << "? " << i << ' ' << j << endl;
ll x = -1;
cin >> x;
if (x == -1) exit(0);
return x;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll mx = 1;
for (ll j = 2; j <= n; j++) {
ll x = ask(mx, j), y = ask(j, mx);
if (x > y) {
p[mx] = x;
mx = j;
} else p[j] = y;
}
p[mx] = n;
cout << '!';
for (ll i = 1; i <= n; i++) cout << ' ' << p[i];
cout << endl;
}
Khi đi thi
- Đọc kỹ giao thức: định dạng câu hỏi, câu trả lời, dòng kết luận có tính vào ngân sách không, có nhiều test trong một phiên không. Mọi dòng gửi đi dùng
endl; gặp thì thoát ngay. - Ngân sách gợi ý thuật toán: là tìm kiếm nhị phân, là mỗi câu một ẩn, là hỏi theo cặp, là tìm một mốc rồi suy ra phần còn lại.
- Viết một trình tương tác ngẫu nhiên nhỏ và chạy nhiều phiên trước khi nộp; kiểm cả số câu hỏi ở trường hợp xấu nhất, không chỉ kết quả.
Tóm tắt
- Lời giải và trình tương tác nói chuyện qua hai đường ống; mỗi dòng gửi đi phải được đẩy khỏi bộ đệm.
- câu hỏi có kiểu trả lời tách được tối đa khả năng, cho cận dưới số câu cần hỏi.
- Câu trả lời biến được thành điều kiện đơn điệu thì tìm kiếm nhị phân, câu.
- Hỏi một quan hệ không đối xứng theo hai chiều để mỗi cặp câu chốt chắc một phần tử.
Bài tập
0 rồi 1 vào cuối; khi cả hai đều không được thì phần còn lại nằm bên trái, nối vào đầu (thử 0, sai thì là 1); tổng không quá câu.Đọc thêm
- Interactive Problems: Guide for ParticipantsCodeforcesQuy ước đẩy bộ đệm, phản hồi lỗi và Idleness Limit Exceeded.
- 交互题OI WikiGiao thức, đẩy bộ đệm trong nhiều ngôn ngữ và cách tự kiểm thử.
- Binary searchcp-algorithmsTìm kiếm nhị phân trên hàm đơn điệu, khung của phần lớn bài tương tác.