Đoán cây nhị phân

Xem dạng PDF

Gửi bài giải


Điểm: 0,01 (OI)
Giới hạn thời gian: 4.0s
Giới hạn bộ nhớ: 256M
Input: stdin
Output: stdout

Dạng bài
Ngôn ngữ cho phép
C, C++, Go, Java, Kotlin, Pascal, PyPy, Python, Rust, Scratch

Đây là một bài toán tương tác (interactive)

Cho một cây nhị phân gồm ~2\cdot n - 1~ đỉnh được đánh số từ ~1~ đến ~2\cdot n - 1~. Gốc cây là đỉnh ~2\cdot n - 1~. Trên cây, các đỉnh ~1, 2, \ldots, n~ là các đỉnh . Các đỉnh ~p~ (~n < p < 2\cdot n~) còn lại đều thoả mãn tính chất sau:

  • đỉnh ~p~ có chính xác hai đỉnh con, một đỉnh con bên trái ~a_p~ và một đỉnh con bên phải ~b_p~;

  • với cặp đỉnh ~u~, ~v~ (~1 \le u, v \le n~) bất kì mà ~a_p~ là tổ tiên của ~u~ và ~b_p~ là tổ tiên của ~v~, thì ~u < v~.

Với ~1 \le l \le r \le n~, định nghĩa ~f(l, r)~ là số lượng phần tử của tập hợp các đỉnh ~S~ thoả mãn các điều kiện sau:

  • với ~l \le u \le r~, tồn tại một tổ tiên của ~u~ trong ~S~;

  • với ~1 \le u < l~ hoặc ~r < u \le n~, không tồn tại tổ tiên nào của ~u~ trong ~S~;

  • trong tất cả các tập hợp đỉnh thoả mãn hai điều kiện trên, tập ~S~ là tập có ít phần tử nhất.

Bạn được biết số nguyên ~n~, nhưng không biết trước cấu trúc của cây. Bạn cần tương tác với chương trình của ban tổ chức (BTC) để xác định cấu trúc của cây nhị phân. Bạn được phép đưa ra các truy vấn có dạng ~l, r~ (~1 \le l \le r \le n~), và phản hồi bạn nhận được là ~f(l, r)~.

Khi đã xác định được cấu trúc cây, bạn cần đưa ra danh sách các đỉnh con ~a_p~ và ~b_p~ cho các đỉnh ~n < p < 2 \cdot n~. Đáp án của bạn được coi là đúng khi các điều kiện sau thoả mãn:

  • bạn đưa ra không quá ~n^2~ truy vấn;

  • cấu trúc cây thoả mãn các tính chất đã nêu trên;

  • với mọi cặp số ~l, r~ (~1 \le l \le r \le n~), ~f(l, r)~ tính từ cây của bạn phải bằng với ~f(l, r)~ tính từ cây của BTC.

Nếu có nhiều đáp án thoả mãn các điều kiện trên, hãy đưa ra đáp án thoả mãn bất kì.

Trong bài toán này, điểm số bạn nhận được phụ thuộc vào số lượng truy vấn bạn đưa ra. Chi tiết xem thêm phần scoring.


Đỉnh ~p~ được gọi là tổ tiên của ~u~ nếu ~p = u~, hoặc ~p~ không phải là lá và một đỉnh con của ~p~ là tổ tiên của ~u~.

Interaction

Đầu tiên, bạn cần đọc số nguyên ~t~ (~1 \le t \le 150~)  – số test case. Với mỗi test case, quá trình tương tác diễn ra như sau:

  • Đầu tiên, hãy đọc vào một số nguyên ~n~ (~2 \le n \le 150~) – số lượng đỉnh lá của cây.

  • Để đưa ra truy vấn, hãy in ra ? ~l \ r~ (~1 \le l \le r \le n~), sau đó đọc vào một số nguyên ~f(l, r)~.

  • Khi đã xác định được cấu trúc của cây, hãy in ra trên một dòng kí tự !, tiếp đó in ra ~2n - 2~ số nguyên ~a_{n+1}, b_{n+1}, a_{n+2}, b_{n+2}, \ldots, a_{2n-1}, b_{2n-1}~ – danh sách các đỉnh con của các đỉnh ~n + 1, n + 2, \ldots 2 \cdot n - 1~.

    Nếu có nhiều đáp án đúng, hãy in ra đáp án bất kì.

Cây đã được cố định trước quá trình tương tác, và sẽ không thay đổi trong quá trình tương tác.

Dữ liệu vào đảm bảo tổng giá trị ~n~ của các testcase không vượt quá ~150~.

Sau khi in ra một câu lệnh, đừng quên xuống dòng và flush đầu ra chuẩn. Để làm điều này, bạn có thể sử dụng:

  • fflush(stdout) hoặc cout.flush() trong C++;

  • System.out.flush() trong Java;

  • flush(output) trong Pascal;

  • stdout.flush() trong Python;

  • xem tài liệu chuẩn đối với các ngôn ngữ khác.

Scoring

Gọi ~q~ là số câu hỏi bạn đưa ra. Điểm số của bạn cho test case như sau:

Điều kiện Điểm
~2\cdot n \lt q \le n^2~ ~1000~
~q \le 2n~ ~2000~

Điểm số của bộ dữ liệu vào bằng điểm số thấp nhất của các test case. Điểm số của bạn cho bài này bằng điểm số thấp nhất của các bộ dữ liệu vào.

Sample Input 1

2
3

2

1

4

2

2

1

Sample Output 1

? 1 2

? 2 3

! 2 3 1 4

? 1 3

? 2 4

? 1 4

! 1 2 3 4 5 6

Notes

Trong ví dụ đầu tiên, các truy vấn được đưa ra như sau:

  • ~f(1, 2) = 2~. Ở đây ~S = \{ 1, 2 \}~

  • ~f(2, 3) = 1~. Ở đây ~S = \{ 4 \}~

image

Hình vẽ minh hoạ ví dụ thứ nhất

Trong ví dụ thứ hai, các truy vấn được đưa ra như sau:

  • ~f(1, 3) = 2~. Ở đây ~S = \{ 3, 6 \}~

  • ~f(2, 4) = 2~. Ở đây ~S = \{ 2, 5 \}~

  • ~f(1, 4) = 1~. Ở đây ~S = \{ 7 \}~

image

Hình vẽ minh hoạ ví dụ thứ hai


Bình luận

Hãy đọc nội quy trước khi bình luận.



  • 4
    ProTeam15  đã bình luận lúc 17, Tháng 5, 2026, 4:50

    Bản chất thì ~f(l, r)~ là số nút ít nhất cần để phủ hết đoạn từ ~l \rightarrow r~.

    Để dễ tưởng tượng bài này hơn bạn có thể nghĩ đang làm việc với Segment Tree. Ý tưởng sơ khai là mỗi lẫn bạn truy vấn trên ST thì số lượng nút bạn đụng đến không quá ~log(n)~. Tại sao điều đó lại diễn ra??? Lí do là có sự gộp nút ở đây, nếu như đoạn truy vấn có cả nút ~2\times id ~ và ~2\times id + 1~ thì có thể gộp nó lại thành nút ~id~.

    Qua cái nhìn trên mình có thể đưa ra ý tưởng là nếu hiện tại đang biết đoạn từ ~1 \rightarrow i-1~ sau đó mình thêm nút ~i~ vào thì sẽ xảy ra bao nhiêu lần gộp. Và mỗi lần gộp như thế nó sẽ tác động đến ~2~ đỉnh nào?? Và sau khi gộp ~2~ đỉnh đó lại thì nó biến thành đỉnh nào?? Giả sử gộp ~2~ đỉnh ~A, B~ thành một đỉnh mới là ~C~ thì ta có ~L[C] = A~, ~R[C] = B~.

    Để biết nó tác động đến ~2~ đỉnh nào thì ta cần duy trì các đỉnh hiện tại cần để phủ và đặt ra các câu hỏi ~f(1, i)~ để biết rằng có bao nhiêu lần gộp, từ số lần gộp sẽ thấy cái tập đỉnh duy trì để phủ hết sẽ biến đổi như thế nào.

    Giả sử gọi tập đỉnh duy trì để phủ hết là ST:

    • Ta đang phủ hết đoạn ~1 \rightarrow i - 1~, bây giờ thêm đỉnh ~i~ vào. Suy ra ST sẽ thêm ~i~.
    • Sau đó nó sẽ bắt đầu quá trình gộp, thì gộp sẽ tác động lên ~2~ đỉnh cuối trong ST.
    • Số lần gộp là: ~f(1, i - 1) + 1 - f(1, i)~.
    • Sau khi gộp ~2~ đỉnh cuối lại thì nó tạo ra đỉnh nào?? => Đỉnh tiếp theo chưa xuất hiện.

    Vậy chỉ cần tốn tổng cộng ~n - 1~ truy vấn là có thể xây dựng lại cây ban đầu.

    Code tham khảo:

    #include<bits/stdc++.h>
    #define int long long
    #define oo 1000000000000000000
    #define ii pair<int, int>
    #define fi first
    #define se second
    #define all(x) x.begin(), x.end()
    #define uniq(x) x.erase(unique(all(x)), x.end())
    #define pb push_back
    using namespace std;
    
    const int mod = 998244353;
    
    void add(int &x, int y) {
       x += y;
       if(x >= mod) x -= mod;
    }
    void sub(int &x, int y) {
       x -= y;
       if(x < 0) x += mod;
    }
    
    int n;
    int need[1005]; // f[1 -> i]
    int a[1005], b[1005];
    
    int ask(int l, int r) {
       cout << "? " << l << " " << r << endl;
    
       int cnt; cin >> cnt;
       return cnt;
    } 
    
    void solve() {
    
       /*
       Về mặt bản chất f(l, r) là số đỉnh ít nhất để phủ đoạn đỉnh [L -> R]
    
       nếu mình tính đc số lần các nút gộp vào nhau để tạo thành nút lớn hơn => mình sẽ biết đc đó là 2 nút con của thằng cha.
    
       1 -> (i - 1) thêm i thì gộp bao nhiêu lần?
       hỏi f(1, i) trừ đi f(1, i - 1) + 1 -- +1 là do thêm đỉnh mới là i vào
    
       giờ có số lần gộp rồi thì làm gì??
       mấy lần gộp đó tác động lên thằng nào???
       2 thằng cuối, nma 2 thằng cuối gộp lại thì thành gì???? => thành thằng tiếp theo chưa có
       */
    
       cin >> n;
    
       need[1] = 1;
       for(int i = 2; i <= n; i++) {
           need[i] = ask(1, i);
       }
    
       stack<int> st;
       st.push(1);
       int cur = n;
       for(int i = 2; i <= n; i++) {
           st.push(i);
           int gop = need[i - 1] + 1 - need[i];
           for(int j = 1; j <= gop; j++) {
               int y = st.top(); st.pop();
               int x = st.top(); st.pop();
               cur++;
               a[cur] = x;
               b[cur] = y;
               st.push(cur);
           }
       }
    
       cout << "! ";
       for(int i = n + 1; i < 2 * n; i++) {
           cout << a[i] << " " << b[i] << " ";
       }
       cout << endl;
    }
    
    signed main() {
       ios_base::sync_with_stdio(0);
       cin.tie(0); cout.tie(0);
    
       // if(fopen("a.inp", "r")) {
       //     freopen("a.inp", "r", stdin);
       //     freopen("a.out", "w", stdout);
       // }
    
       int t = 1;
       cin >> t;
       while(t--) {
           solve();
       }
    }