Interactive Graph (Simple Version)

Interactive Graph (Simple Version)

二分法

问题概述

给定一个有向无环图,\(n\) 个顶点,边数 \(m\) 未知,\(n \le 15\)。可以询问第 \(k\) 条字典序路径,最多询问 \(32 \cdot (n+m)\) 次。需要确定图中所有边。

核心观察

所有路径按字典序排序。字典序首先比较第一个顶点,因此所有以顶点 \(u\) 开头的路径在列表中连续排列。对于每个顶点 \(u\),单点路径 \((u)\) 是所有以 \(u\) 开头路径中字典序最小的,因为它是其他以 \(u\) 开头路径的前缀。

设 \(pos(u)\) 为路径 \((u)\) 在总列表中的位置。则以 \(u\) 开头的路径占据区间:

\[[pos(u), \; pos(u+1)-1] \]

其中 \(pos(n+1) = T+1\),\(T\) 为总路径数。

算法步骤

1. 确定每个顶点的单点路径位置 \(pos(i)\)

  • \(pos(1) = 1\)。
  • 对于 \(i = 2 \dots n\),在区间 \([pos(i-1)+1, 2^{30}]\) 中二分查找第一个满足“第一个顶点 \(\ge i\)”的位置。
    • 查询中间位置 \(mid\),读取返回路径。
    • 若路径为空(\(len = 0\))或第一个顶点 \(\ge i\),则 \(right = mid\);否则 \(left = mid + 1\)。
    • 最终 \(pos(i) = left\)。

2. 确定总路径数边界 \(pos(n+1) = T+1\)

  • 在区间 \([pos(n)+1, 2^{30}]\) 中二分查找第一个返回空路径的位置。
    • 查询 \(mid\),若 \(len = 0\),则 \(right = mid\);否则 \(left = mid + 1\)。
    • 最终 \(pos(n+1) = left\)。

3. 枚举每个顶点 \(i\) 的所有出边

对于顶点 \(i\),其区间为 \([pos(i), \; pos(i+1)-1]\)。跳过第一条单点路径 \((i)\),从 \(l = pos(i)+1\) 开始,\(r = pos(i+1)-1\)。

当 \(l \le r\) 时循环:

  1. 查询第 \(l\) 条路径,得到第二个顶点 \(v\),记录边 \(i \to v\)。
  2. 二分查找第一个第二个顶点 \(> v\) 的位置:
    • 令 \(low = l\),\(high = r + 1\)。
    • 当 \(low < high\):
      • \(mid = (low + high) / 2\)。
      • 查询第 \(mid\) 条路径,读取第二个顶点。
      • 若第二个顶点 \(> v\),则 \(high = mid\);否则 \(low = mid + 1\)。
    • 最后 \(l = low\)。
  3. 重复直到 \(l > r\)。

关键细节

  • 二分右边界必须为 \(r+1\):内层二分是在 \([l, r+1)\) 中查找第一个满足条件的位置。若不存在,则返回 \(r+1\),使 \(l\) 跳到 \(r+1\),外层循环结束。若写成 \(high = r\),当所有元素都不满足时,\(low\) 停在 \(r\),导致死循环。
  • 外层循环条件必须为 \(l \le r\):保证处理区间内最后一个位置,避免漏边。
  • 查询 \(k\) 的范围:\(1 \le k \le 2^{30}\),代码中右边界使用 1LL << 30。
  • 每次查询后刷新输出:cout.flush()。
  • 若读到 \(-1\) 立即退出。

查询次数分析

  • 确定 \(pos(2..n)\):每个顶点二分约 \(30\) 次,共 \((n-1) \times 30\) 次。
  • 确定 \(pos(n+1)\):约 \(30\) 次。
  • 枚举出边:每条边需要一次查询获取 \(v\),外加约 \(30\) 次二分定位下一个不同出边,共约 \(31m\) 次。
  • 总查询次数约 \(30n + 31m\)。对于 \(n \le 15, m \le 105\),最坏约 \(30 \times 15 + 31 \times 105 = 450 + 3255 = 3705\) 次,小于限制 \(32 \times (15+105) = 3840\) 次。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

vector<ll> query(ll k, ll &l) {
    cout << "? " << k << "\n";
    cout.flush();

    int len; cin >> len;
    vector<ll> a(len + 1);
    for(int i = 1; i <= len; i ++) cin >> a[i];
    l = len;
    return a;
}

void solve() {
    int n; cin >> n;
    vector<ll> pos(n + 2, 0); pos[1] = 1;
    ll len;
    
    //确定pos的位置
    for(int i = 2; i <= n; i ++) {
        ll left = pos[i-1] + 1, right = (1LL << 30);
        while(left < right) {
            ll mid = (left + right) / 2;
            vector<ll> reply = query(mid, len);
            if(len == 0 || reply[1] >= i) right = mid;
            else left = mid + 1;
        }
        pos[i] = left;
    }
    //确定边界的位置
    ll left = pos[n] + 1, right = (1LL << 30);
    while(left < right) {
        ll mid = (left + right) / 2;
        vector<ll> reply = query(mid, len);
        if(len == 0) right = mid;
        else left = mid + 1;
    }
    pos[n+1] = left;

    ll edge = 0;
    vector<vector<int>> g(n + 1);
    for(int i = 1; i <= n; i ++) {
        ll l = pos[i] + 1, r = pos[i+1] - 1;

        while(l <= r) {
            vector<ll> path = query(l, len);
            int v = path[2];
            g[i].push_back(path[2]); edge ++;

            ll low = l, high = r + 1;
            while(low < high) {
                ll mid = (low + high) / 2;
                vector<ll> reply = query(mid, len);
                if(reply[2] > v) high = mid;
                else low = mid + 1;
            }
            l = low;
        }
    }
    cout << "! " << edge << "\n";
    for(int i = 1; i <= n; i ++) {
        for(int v : g[i]) cout << i << " " << v << "\n";
    }
    cout.flush();
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int _ = 1; cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-10-11 21:03  thenrify  阅读(3)  评论(0)    收藏  举报