Chương 2·Bài 28

Interactive Problems

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 nn.
  • 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ả −1-1 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ỗ.

Giao thức tương tác đi theo hai chiều: lời giải hỏi, interactor trả lời, rồi lời giải suy luận.Lời giải? i jInteractora[i]+a[j]Lời giảicập nhật alặp lại tới khi đủ thông tin; flush trước khi chờ phản hồi
Lời giải gửi câu hỏi và đẩy bộ đệm, trình tương tác trả lời, lời giải cập nhật điều đã biết.

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 (nn 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?

Ví dụDãy Sau Những Tổng Đôi

Đề. Dãy ẩn nn số nguyên dương, 3≤n≤50003 \le n \le 5000. Câu hỏi ? i j nhận ai+aja_i + a_j; được hỏi không quá nn câu, rồi in ! và cả dãy.

Lời giải. Ba câu a1+a2a_1 + a_2, a2+a3a_2 + a_3, a1+a3a_1 + a_3 cho ba phương trình ba ẩn: a1=(a1+a2)+(a1+a3)−(a2+a3)2a_1 = \frac{(a_1 + a_2) + (a_1 + a_3) - (a_2 + a_3)}{2}, rồi suy ra a2,a3a_2, a_3. Mỗi aia_i với i≥4i \ge 4 tốn đúng một câu ai−1+aia_{i-1} + a_i. Tổng 3+(n−3)=n3 + (n - 3) = n câu, đúng ngân sách. Hỏi các cặp (1,i)(1, i) cũng được; điều cần là mỗi câu thêm đúng một ẩn mới.

C++
#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ó cc kiểu trả lời thì qq câu phân biệt được tối đa cqc^q khả năng; cần phân biệt KK khả năng thì phải hỏi ít nhất ⌈log⁡cK⌉\lceil \log_c K \rceil câu. Câu hỏi có/không (c=2c = 2) cho vị trí trong n=2⋅105n = 2 \cdot 10^5 ô 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 nn 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 xx trong [2,100][2, 100]. Câu hỏi là một số q∈[2,100]q \in [2, 100], nhận yes nếu qq chia hết xx. 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 4,9,25,494, 9, 25, 49, tổng 19 câu; đếm số câu yes. Nếu xx là hợp số, viết x=pqx = pq với p≤qp \le q là hai thừa số, pp nguyên tố. Khi q≠pq \ne p có một ước nguyên tố khác pp không quá 5050, tức không quá 47, nên ít nhất hai số nguyên tố trong danh sách chia hết xx. Khi xx chỉ có một ước nguyên tố pp thì p2∣xp^2 \mid x và p2≤100p^2 \le 100, nên p≤7p \le 7 và cả pp lẫn p2p^2 đều trả yes. Nếu xx nguyên tố thì chỉ có thể chính xx 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.

C++
#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, ⌈log⁡2n⌉\lceil \log_2 n \rceil 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ố [1,m][1, m] là mm trừ tổng, tăng theo mm. 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 n≤2⋅105n \le 2 \cdot 10^5; đọc nn, t=1t = 1, rồi kk. Câu hỏi ? l r nhận tổng các bit trên [l,r][l, r]; hỏi không quá 20 câu rồi in ! x, vị trí của số 0 thứ kk.

Lời giải. Số 0 trong [1,m][1, m] là m−sum(1,m)m - \mathrm{sum}(1, m), không giảm theo mm. Vị trí cần tìm là mm nhỏ nhất có ít nhất kk số 0 trong [1,m][1, m]: tìm kiếm nhị phân trên mm, mỗi bước hỏi ? 1 m. Có ⌈log⁡2(2⋅105)⌉=18\lceil \log_2 (2 \cdot 10^5) \rceil = 18 câu.

Mảng bị giấu; bên dưới là cuộc hỏi đáp giữa lời giải và trình tương tác.
C++
#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 n≤105n \le 10^5 số khác nhau. Câu hỏi ? l r với l<rl < r nhận chỉ số của phần tử lớn thứ hai trong [l,r][l, r]. 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 ss, vị trí lớn thứ hai. Một đoạn chứa ss trả lời đúng ss khi và chỉ khi nó cũng chứa phần tử lớn nhất pp. Hỏi ? 1 s (khi s>1s > 1) để biết pp nằm bên trái hay bên phải ss. Bên trái thì ? m s trả ss khi và chỉ khi m≤pm \le p: tìm mm lớn nhất như vậy. Bên phải thì ? s m trả ss khi và chỉ khi m≥pm \ge p: tìm mm nhỏ nhất. Tổng 2+17=192 + 17 = 19 câu.

C++
#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ỡ 2n2n 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 u<vu < v: u mod v=uu \bmod v = u, còn v mod u<uv \bmod u < u. 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àng trên là hoán vị bị giấu; hàng dưới là những gì lời giải đã chốt. Ô cam là hai vị trí đang được hỏi.

Hỏi nhanh

Hoán vị ẩn, hai vị trí i,ji, j có pi mod pj=3p_i \bmod p_j = 3 và pj mod pi=7p_j \bmod p_i = 7. Ta biết gì?

Ví dụHoán Vị Qua Phần Dư

Đề. Hoán vị ẩn pp của 1..n1..n, n≤104n \le 10^4. Câu hỏi ? i j nhận pi mod pjp_i \bmod p_j. Hỏi không quá 2n2n 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 j=2..nj = 2..n: hỏi x=pmx mod pjx = p_{mx} \bmod p_j và y=pj mod pmxy = p_j \bmod p_{mx}. Nếu x>yx > y thì pmx=xp_{mx} = x và jj thành ứng viên mới; ngược lại pj=yp_j = y. Cuối cùng pmx=np_{mx} = n. Tổng 2(n−1)2(n - 1) câu; n=1n = 1 thì in luôn.

C++
#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 −1-1 thì thoát ngay.
  • Ngân sách gợi ý thuật toán: log⁡2n\log_2 n là tìm kiếm nhị phân, nn là mỗi câu một ẩn, 2n2n là hỏi theo cặp, n+cn + c 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.
  • qq câu hỏi có cc kiểu trả lời tách được tối đa cqc^q 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, ⌈log⁡2n⌉\lceil \log_2 n \rceil 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

1
Sáu Mã Sốcanuc80k2802 · Tương tácHỏi bốn tích a1a2,a2a3,a3a4,a4a5a_1 a_2, a_2 a_3, a_3 a_4, a_4 a_5; trong sáu số cố định, mọi tích hai số khác nhau là phân biệt, nên thử mọi hoán vị khớp bốn tích.
LQDOJ
2
Hai Vị Trí Cực Trịcanuc80k2803 · Tương tácSo từng cặp phần tử với nhau trước, số lớn của cặp thi max, số nhỏ thi min: khoảng ⌈3n/2⌉−2\lceil 3n/2 \rceil - 2 phép so.
LQDOJ
3
Chồng Đá Khác Thườngcanuc80k2805 · Tương tácTổng tiền tố của số đá biết trước; hỏi tổng khối lượng một nửa các đống và so với tổng số đá của nửa đó để tìm kiếm nhị phân đống nặng.
LQDOJ
4
Điểm Cố Định Duy Nhấtcanuc80k2808 · Tương tácHỏi một đoạn [1,m][1, m], đếm các giá trị trả về nằm trong [1,m][1, m]; số đó lẻ thì điểm cố định ở nửa trái, tìm kiếm nhị phân.
LQDOJ
5
Ô Vua Bí Mậtcanuc80k2809 · Tương tácHỏi góc (1,1)(1, 1) được dd: vua nằm trên hàng d+1d + 1 hoặc cột d+1d + 1; hỏi thêm một ô trên hàng đó và một ô trên cột đó (cắt theo n,mn, m) để chốt.
LQDOJ
6
Đáy Cục Bộcanuc80k2811 · Tương tácTìm kiếm nhị phân trên đoạn [l,r][l, r] luôn giữ al−1>ala_{l-1} > a_l và ar<ar+1a_r < a_{r+1}: so ama_m với am+1a_{m+1} để giữ nửa còn bất biến.
LQDOJ
7
Kênh Phản Hồi Luân Phiêncanuc80k2812 · Tương tácnn câu đầu đều hỏi số 1: đáp đúng chắc chắn là 1 (hoặc 0 thì xong), nên đọc ra từng pip_i; sau đó tìm kiếm nhị phân và đảo dấu theo chu kỳ.
LQDOJ
8
Chuỗi Con Bí Mậtcanuc80k2813 · Tương tácDựng xâu bằng cách thử nối 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á 2n2n câu.
LQDOJ
9
Chu Trình Của Hoán Vị Ẩncanuc80k2816 · Tương tácHỏi lặp cùng một chỉ số ii: sau tt lần cập nhật qi=pt(i)q_i = p^t(i), nên câu sau là pp của câu trước; mỗi chu trình tốn độ dài cộng một câu.
LQDOJ
10
Ba Tổng Bitcanuc80k2818 · Tương tácXOR của a1a_1 với mọi phần tử tốn n−1n - 1 câu; ba câu AND trên ba chỉ số đầu cùng a+b=(a⊕b)+2(a AND b)a + b = (a \oplus b) + 2(a \text{ AND } b) cho hệ ba phương trình tính a1a_1.
LQDOJ
11
Thống Kê Thứ K Từ AND Và ORcanuc80k2819 · Tương táca+b=(a OR b)+(a AND b)a + b = (a \text{ OR } b) + (a \text{ AND } b): tổng ba cặp đầu cho ba số đầu, mỗi số sau thêm hai câu; sắp lại và lấy số thứ kk.
LQDOJ
12
Hai Mảnh Của Số Ẩncanuc80k2820 · Tương tácHai câu hỏi, mỗi câu 100 số: câu đầu các số có 7 bit cao bằng 0, câu sau 7 bit thấp bằng 0; XOR với đáp án cho từng nửa bit của xx.
LQDOJ
13
Hai Số Sau Mặt Nạ XORcanuc80k2822 · Tương tácĐi từ bit cao xuống, giữ dấu so sánh của phần bit thấp còn lại; hai câu bật bit đó ở cc hoặc ở dd: kết quả khác nhau thì hai bit bằng nhau, giống nhau thì bit khác nhau và số đang lớn hơn mang bit 1.
LQDOJ
14
Cực Đại Ngoài Mỗi Nhómcanuc80k2825 · Tương tácMột câu hỏi cả mảng rồi tìm kiếm nhị phân trên tiền tố chỉ số để tìm vị trí max, khoảng 10 câu; chỉ tập chứa vị trí đó cần hỏi riêng phần bù, các tập khác nhận max toàn mảng.
LQDOJ

Đọ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.