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

浙公网安备 33010602011771号