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.
All paths with the same first vertex appear in one contiguous block in lexicographic order. Inside the block for vertex , the single-vertex path comes before every longer path starting at .
For a fixed source , outgoing edges show up as the second vertex of paths beginning with . Also, paths starting with are grouped by increasing .
Suppose you already found all outgoing neighbors of up through value . The first path with source and second vertex reveals the next edge from .
The condition 'this path is after the part we already skipped for ' is monotone over the global lexicographic list, so you can binary search the path index .
Do one binary search per discovered edge, plus one final failed search per vertex. Each search costs at most queries plus one verification query, giving at most queries. That bound is the whole damn problem.
Research basis: I checked the official statement and the Codeforces Round 1079 editorial. The official C1 solution uses the binary-search-over-path-index approach described here; the reasoning below is original.
First, do not get baited by the hack format. In the actual interactive solution, your program reads only and then each from the interactor. The hidden m and edge list are for the interactor/hacker input, not for your solution to read. Reading m is a one-way ticket to WA city.
For any vertex , all paths whose first vertex is form one contiguous block in the global lexicographic ordering. Blocks are ordered by the starting vertex: all paths starting at , then all paths starting at , and so on.
Inside the block for , the path comes first, because it is a proper prefix of every longer path starting at . After that, paths are grouped by their second vertex. So if has outgoing neighbors , the block for looks like:
, then all paths starting with , then all paths starting with , and so on.
The first path in the group for neighbor is exactly , since that direct two-vertex path is a prefix of every longer path . Therefore, if we can find the first path whose source is and whose second vertex is larger than the last neighbor we found, we get the next outgoing edge of .
Fix a source and let be the largest outgoing neighbor of already discovered. Initially . Define a queried path to be after the skipped prefix if one of these holds:
Everything else is still before or inside the part already consumed: it starts below , it is the singleton , or it starts with where . Because of the ordering above, this predicate is monotone over : false for a prefix of indices, then true forever. No weird interleaving happens; lexicographic order does the bookkeeping for us.
So we binary search from to for the first index where the predicate becomes true. If the returned path starts at and has length at least , then its first two vertices are and the smallest not-yet-found neighbor , so edge exists. Add it, set , and repeat.
If the returned path is empty or no longer starts at , there is no remaining outgoing neighbor from . The first real path of any later starting vertex is its singleton path, so a later block cannot make us add a fake edge for . Still, checking path[0] == u in code is cheap paranoia, and cheap paranoia is usually good CP hygiene.
Correctness proof:
For a fixed vertex , lexicographic order makes all paths starting at contiguous. Within that block, after the singleton prevuprevuprev[u,v]v(u,v)$ is correct.
The binary search is valid because the predicate describes a suffix of the sorted path list. Thus it finds exactly the first candidate after the skipped prefix. If that candidate begins with and has length at least , the previous paragraph proves it gives the next real outgoing edge. If it does not, then no remaining path of the form with exists, so all outgoing edges of have already been found.
By induction over repeated searches for one source , the algorithm finds all outgoing edges of in increasing order and no fake edges. Running this for every source vertex outputs every edge exactly once.
Each actual edge causes one successful binary search. Each vertex causes one additional failed binary search proving its outgoing list is done. That is searches. A search over uses at most queries, and we ask one more query at the found index to read the path, so the total is at most queries.
Complexity: at most queries. Local work is plus reading returned paths, and memory is .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
vector<int> ask(int 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;
return path;
}
bool afterSkippedPrefix(const vector<int> &path, int source, int prevNeighbor) {
if (path.empty()) return true;
if (path[0] > source) return true;
if (path[0] < source) return false;
return path.size() >= 2 && path[1] > prevNeighbor;
}
void solve() {
int n;
cin >> n;
vector<pair<int, int>> edges;
for (int source = 1; source <= n; source++) {
int prevNeighbor = -1;
while (true) {
int low = 1, high = 1 << 30, answer = 1 << 30;
while (low <= high) {
int mid = low + (high - low) / 2;
vector<int> path = ask(mid);
if (afterSkippedPrefix(path, source, prevNeighbor)) {
answer = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
vector<int> path = ask(answer);
if (path.size() >= 2 && path[0] == source) {
edges.push_back({path[0], path[1]});
prevNeighbor = path[1];
} else {
break;
}
}
}
cout << '!' << ' ' << edges.size() << '\n';
for (auto [u, v] : edges) {
cout << u << ' ' << v << '\n';
}
cout.flush();
}
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(int 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;
return path;
}
bool afterSkippedPrefix(const vector<int> &path, int source, int prevNeighbor) {
if (path.empty()) return true;
if (path[0] > source) return true;
if (path[0] < source) return false;
return path.size() >= 2 && path[1] > prevNeighbor;
}
void solve() {
int n;
cin >> n;
vector<pair<int, int>> edges;
for (int source = 1; source <= n; source++) {
int prevNeighbor = -1;
while (true) {
int low = 1, high = 1 << 30, answer = 1 << 30;
while (low <= high) {
int mid = low + (high - low) / 2;
vector<int> path = ask(mid);
if (afterSkippedPrefix(path, source, prevNeighbor)) {
answer = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
vector<int> path = ask(answer);
if (path.size() >= 2 && path[0] == source) {
edges.push_back({path[0], path[1]});
prevNeighbor = path[1];
} else {
break;
}
}
}
cout << '!' << ' ' << edges.size() << '\n';
for (auto [u, v] : edges) {
cout << u << ' ' << v << '\n';
}
cout.flush();
}
int main() {
setIO();
int t;
cin >> t;
while (t--) solve();
return 0;
}