Search for a command to run...
Progressive hints first, then the full explanation and implementation when you're ready to cash out.
Review status
AI-generated and still unreviewed. Double-check the details before internalizing them.
Hints
Open only as much as you need to keep the solve alive.
Look at the lexicographic list as a trie of path prefixes. For any fixed prefix ending at vertex , all paths with that prefix form one contiguous block.
The size of that block depends only on the last vertex. Define as the number of paths starting at , including the one-vertex path .
If you queried consecutive paths, their longest common prefix would tell you exactly which new trie edge you just entered. That trie edge is also a real graph edge.
When a vertex disappears from the common prefix between the previous queried path and the current one, its whole block is finished. At that moment you can compute from the already discovered outgoing edges.
Start from the known first path and query . After reading a path, add the edge after the LCP; if the next suffix starts at a vertex with known , jump by , otherwise move to .
Research used: the official Codeforces editorial for Round 1079 shows the accepted C2 code pattern; the official problem statement is the canonical spec; this CSDN explanation independently describes the same prefix-block and path-count reuse idea.
This is still an interactive solution. The hack input format is for the interactor/test data; the submitted program reconstructs the graph by asking ? k and finally printing ! m. Reading m directly would be solving the wrong damn problem.
For any vertex , define as the number of paths that start at , including the length- path. If the outgoing edges of go to vertices , then
The key observation is stronger than it first looks: for any already fixed valid prefix ending at , all complete paths beginning with form one contiguous segment in lexicographic order, and that segment has size exactly . The segment is ordered as: first itself, then for each outgoing neighbor in increasing vertex-label order, the whole segment for that neighbor. So the global path list is basically DFS preorder on the trie of all paths, except identical suffix subproblems repeat whenever the same graph vertex is reached through another prefix.
If we queried every path one by one, two consecutive paths would reveal movement in this DFS preorder. Let prev be the previous path and cur be the current path, and let lcp be their longest common prefix length. If lcp > 0, then cur[lcp - 1] -> cur[lcp] is the edge that enters the next child block, so it is a real graph edge and should be recorded. This is the simple part.
The hard version needs at most queries, so walking every path is impossible; there may be exponentially many. The escape hatch is memoization of . Once the scan leaves a prefix containing some vertex , the whole block for that occurrence of is over forever. At that moment, all outgoing edges of have already been discovered, and because the graph is a DAG, can be computed recursively as . Later, if another queried path reaches the same vertex , we do not need to inspect all paths below it again. We can jump over exactly paths.
Algorithm:
prev = [1] and start with k = 2. Skipping ? 1 is important because the final empty query also costs one.lcp(prev, cur).prev has a vertex immediately after the LCP, that vertex's block just ended, so compute its value from the discovered graph.lcp > 0, add edge cur[lcp - 1] -> cur[lcp].k += F(cur[lcp]); otherwise set k++.prev by cur.Why this finds every edge: consider any real edge . The first time the DFS-style path order enters the child block for after some prefix ending at , the current path differs from the previous path exactly at the position of , so the LCP rule records . If the algorithm later skips a block starting at , then was already known, meaning all edges reachable from were already discovered earlier. No edge is silently lost; skipped work is only repeated suffix work.
Why the query count fits: every non-empty query with lcp > 0 records one new edge, so there are at most such queries. Every non-empty query with lcp = 0 is a singleton path starting a new first vertex; since was known for free, there are at most of those. Then there is exactly one final empty query. Total: . Cute budget design, not a coincidence.
Edge cases are clean. For , ? 2 returns empty and the answer is zero edges. Isolated vertices only create singleton queries. Edges may go from a larger label to a smaller label; nothing assumes labels are topological order. The only structural assumption used is acyclicity, so the recursive path counts terminate.
Local work is tiny: each LCP comparison costs , and there are at most queries, so computation is with memory. The real complexity metric is the query count: at most .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
vector<int> ask(ll k) {
cout << '?' << ' ' << k << endl;
int len;
if (!(cin >> len)) exit(0);
if (len == -1) exit(0);
vector<int> path(len);
for (int &x : path) {
cin >> x;
--x;
}
return path;
}
void solve() {
int n;
cin >> n;
vector<vector<int>> g(n);
vector<ll> ways(n, -1);
function<void(int)> calc = [&](int v) {
if (ways[v] != -1) return;
ways[v] = 1;
for (int to : g[v]) {
calc(to);
ways[v] += ways[to];
}
};
vector<int> prev = {0};
ll k = 2;
int edge_count = 0;
while (true) {
vector<int> cur = ask(k);
if (cur.empty()) break;
int lcp = 0;
while (lcp < (int)prev.size() && lcp < (int)cur.size() && prev[lcp] == cur[lcp]) {
++lcp;
}
if (lcp < (int)prev.size()) {
calc(prev[lcp]);
}
if (lcp > 0) {
g[cur[lcp - 1]].push_back(cur[lcp]);
++edge_count;
}
if (ways[cur[lcp]] == -1) {
++k;
} else {
k += ways[cur[lcp]];
}
prev = cur;
}
cout << '!' << ' ' << edge_count << endl;
for (int u = 0; u < n; ++u) {
for (int v : g[u]) {
cout << u + 1 << ' ' << v + 1 << endl;
}
}
}
int main() {
setIO();
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
vector<int> ask(ll k) {
cout << '?' << ' ' << k << endl;
int len;
if (!(cin >> len)) exit(0);
if (len == -1) exit(0);
vector<int> path(len);
for (int &x : path) {
cin >> x;
--x;
}
return path;
}
void solve() {
int n;
cin >> n;
vector<vector<int>> g(n);
vector<ll> ways(n, -1);
function<void(int)> calc = [&](int v) {
if (ways[v] != -1) return;
ways[v] = 1;
for (int to : g[v]) {
calc(to);
ways[v] += ways[to];
}
};
vector<int> prev = {0};
ll k = 2;
int edge_count = 0;
while (true) {
vector<int> cur = ask(k);
if (cur.empty()) break;
int lcp = 0;
while (lcp < (int)prev.size() && lcp < (int)cur.size() && prev[lcp] == cur[lcp]) {
++lcp;
}
if (lcp < (int)prev.size()) {
calc(prev[lcp]);
}
if (lcp > 0) {
g[cur[lcp - 1]].push_back(cur[lcp]);
++edge_count;
}
if (ways[cur[lcp]] == -1) {
++k;
} else {
k += ways[cur[lcp]];
}
prev = cur;
}
cout << '!' << ' ' << edge_count << endl;
for (int u = 0; u < n; ++u) {
for (int v : g[u]) {
cout << u + 1 << ' ' << v + 1 << endl;
}
}
}
int main() {
setIO();
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}