Problem: icpc2025_northern_eurasia_finals_c
Difficulty: medium
Categories: graph, search
Time limit: 3000 ms, memory limit: 1024 MB

Ivan and Petr like to play with cacti — special graphs where each edge belongs to at most one simple cycle, and the graph is connected. Multiple edges between pairs of vertices and loops are allowed.

They invent the following game:

• Petr secretly builds a cactus with $n$ vertices and $m$ edges. The edges are labeled from 1 to $m$ .

• Petr only tells Ivan the number $m$ .

• Ivan is then allowed to ask questions of the following form:

– He chooses a subset $S$ of edge labels (see below about limitations on the subset), and asks: “If we only keep the edges whose labels are in $S$ (and all $n$ vertices), is the resulting graph connected?”   
– Petr must answer either “yes” or “no”.

After asking at most $8 m$ questions, Ivan must determine, for every edge:

1. whether this edge lies on some cycle in the cactus;   
2. if it does, what is the length of that simple cycle.

In this problem, each loop is considered a simple cycle of length 1 and two edges between the same pair of vertices form a simple cycle of length 2.

However, Ivan is still very young and only knows numbers up to 14. So:

• if an edge lies on a simple cycle of length at most 14, he must output that exact length; • if an edge lies on a simple cycle of length greater than 14, he must say that this edge lies on a big cycle.

Also, to avoid having to list a lot of edges each time, Ivan always asks about an edge set obtained from the set used in one of the previous queries, or from the set of all edges, by removing exactly one edge.

Can you design a strategy that allows Ivan to complete this task?

# Input

Each test contains multiple test cases. The first line contains the number of test cases $t$ ( $1 \leq t \leq 1 0 0$ ).   
The description of the test cases follows.

The first line of each test case contains $m$ $1 \leq m \leq 1 0 ^ { 4 }$ ) — the number of edges in the cactus.

It is guaranteed that the sum of $m$ over all test cases does not exceed $1 0 ^ { 4 }$ .

# Interaction Protocol

To ask a query, output a line in the following format (without the quotes):

• “ ? $\textit { p } e ^ { \gamma }$ ( $0 \leq p \leq q$ ; $1 \leq e \leq m$ ), where $p$ is the number of a previous query or zero (queries are numbered from 1 in the order they are asked), $q$ is the number of queries made before the current one, $e$ is the label an edge.

The query denotes a graph consisting of the edges used in the query number $p$ (or all edges if $p = 0$ ), with edge $e$ removed. The interactor checks whether this graph is connected when considering all original vertices and returns a single integer:

$\bullet$ $^ 1$ if the graph denoted by the query is connected;   
$\bullet$ $0$ if the graph denoted by the query is not connected;   
• $^ { - 1 }$ if you have exceeded $\mathrm { 8 } m$ allowed queries or edge $e$ was already removed from the set of edges used in query $p$ . In this case you should terminate your program to receive a Wrong Answer verdict.

When you have found the answer, output a single line in the following format:

• “ ! $e _ { 1 }$ e2 . . . $e _ { m }$ ” (−1 ≤ ei ≤ 14 for all i),

where:

• if $e _ { i }$ is positive, then edge number $i$ belongs to a simple cycle of length exactly $e _ { i }$ , • if $e _ { i } = 0$ , then edge number $i$ belongs to a big cycle (simple cycle of length greater than 14), • if $e _ { i } = - 1$ , then edge number $i$ does not belong to any cycle.

The interactor returns a single integer:

• 1 if your answer is correct. Proceed to the next test case or terminate your program if this was the last case.   
• $- 1$ if your answer is incorrect. In this case you should terminate your program to receive a Wrong Answer verdict.

The interactor is not adaptive, meaning that the graph is fixed before you ask any queries.

# Example

<table><tr><td>standard input</td><td>standard output</td></tr><tr><td>1</td><td></td></tr><tr><td></td><td>？01</td></tr><tr><td>0</td><td></td></tr><tr><td>1</td><td>？03</td></tr><tr><td>1</td><td>？24</td></tr><tr><td>1</td><td>!-1313232</td></tr></table>

# Note

In the example interaction, the input and output contain empty lines to align interactor responses with queries. These empty lines do not appear in the actual input and output.

In this example, the graph has 5 vertices and 7 edges; edges 1 through 7, in this order, are $( 1 , 2 )$ , $( 2 , 3 )$ , $( 3 , 3 )$ , $( 3 , 4 )$ , $( 4 , 5 )$ , $( 2 , 4 )$ , $( 4 , 5 )$ .

![](images/380fab149723c0e3b0b1a7c96c515456622fb609e35534e63249d071c7191b1b.jpg)  
Page 2 of 2

Non-adaptive interactor (C++):
#include "testlib.h"
#include <bits/stdc++.h>
using namespace std;




static long long current_queries = 0;
static long long current_limit = 0;



void log_metrics() {
    if (current_limit > 0) {
        try {
            tout << "queries=" << current_queries << endl;
            tout << "query_limit=" << current_limit << endl;
        } catch (...) {}
    }
}


void finish(TResult verdict, const string& msg) {
    log_metrics();
    quitf(verdict, "%s", msg.c_str());
}




const int MAXN = 20005; 
const int MAXM = 20005;



int head[MAXN];
struct EdgeNode {
    int to;
    int nxt;
    int id; 
} edges_store[MAXM * 2]; 
int ecnt = 0;

void add_edge(int u, int v, int id) {
    edges_store[ecnt] = {v, head[u], id};
    head[u] = ecnt++;
    edges_store[ecnt] = {u, head[v], id};
    head[v] = ecnt++;
}

int N, M;
vector<int> edge_ans; 





vector<vector<bool>> history_sets;




int vis[MAXN];
int p_node[MAXN];
int p_edge[MAXN]; 
int depth[MAXN];


void find_cycles() {
    edge_ans.assign(M, -1); 
    
    for(int i = 0; i <= N; ++i) {
        vis[i] = 0;
        p_node[i] = 0;
        p_edge[i] = -1;
        depth[i] = 0;
    }

    if (N < 1) return;

    
    
    
    function<void(int, int)> dfs = [&](int u, int pe_idx) {
        vis[u] = 1; 
        for (int i = head[u]; i != -1; i = edges_store[i].nxt) {
            int v = edges_store[i].to;
            int idx = edges_store[i].id;
            
            if (idx == pe_idx) continue; 
            
            if (vis[v] == 1) {
                
                
                vector<int> cycle;
                cycle.push_back(idx);
                
                int curr = u;
                while (curr != v) {
                    cycle.push_back(p_edge[curr]);
                    curr = p_node[curr];
                }
                
                int len = cycle.size();
                int val = (len <= 14 ? len : 0);
                
                
                for (int eid : cycle) {
                    edge_ans[eid] = val;
                }
            } else if (vis[v] == 0) {
                p_node[v] = u;
                p_edge[v] = idx;
                depth[v] = depth[u] + 1;
                dfs(v, idx);
            }
        }
        vis[u] = 2; 
    };
    
    
    dfs(1, -1);
}




int bfs_vis[MAXN];
int bfs_cookie = 0; 
int bfs_q[MAXN];


bool check_conn(const vector<bool>& mask) {
    if (N <= 1) return true;

    bfs_cookie++;
    int q_head = 0, q_tail = 0;
    
    
    bfs_q[q_tail++] = 1;
    bfs_vis[1] = bfs_cookie;
    int visited_count = 0;
    
    while(q_head < q_tail) {
        int u = bfs_q[q_head++];
        visited_count++;
        
        for (int i = head[u]; i != -1; i = edges_store[i].nxt) {
            int v = edges_store[i].to;
            int idx = edges_store[i].id;
            
            
            if (mask[idx] && bfs_vis[v] != bfs_cookie) {
                bfs_vis[v] = bfs_cookie;
                bfs_q[q_tail++] = v;
            }
        }
    }
    
    return visited_count == N;
}




void solve_case() {
    
    N = inf.readInt(); 
    M = inf.readInt();
    
    
    ecnt = 0;
    for(int i = 0; i <= N; ++i) head[i] = -1;
    
    
    for(int i = 0; i < M; ++i) {
        int u = inf.readInt();
        int v = inf.readInt();
        add_edge(u, v, i);
    }
    
    
    find_cycles();
    
    
    cout << M << endl;
    
    
    current_queries = 0;
    current_limit = 8LL * M;
    log_metrics();
    
    
    history_sets.clear();
    
    history_sets.reserve(80005); 
    
    history_sets.push_back(vector<bool>(M, true));
    
    
    while (true) {
        string token;
        
        if (!(cin >> token)) {
            finish(_wa, "Unexpected EOF while waiting for query/answer");
        }
        
        if (token == "!") {
            
            bool correct = true;
            for (int i = 0; i < M; ++i) {
                int val;
                if (!(cin >> val)) finish(_wa, "Unexpected EOF reading answer values");
                if (val != edge_ans[i]) correct = false;
            }
            
            if (correct) {
                cout << 1 << endl;
                return; 
            } else {
                cout << -1 << endl;
                finish(_wa, "Incorrect answer provided");
            }
        } else {
            
            int p;
            int e_label;
            
            if (token == "?") {
                if (!(cin >> p >> e_label)) finish(_wa, "Invalid query format");
            } else {
                
                try {
                    p = stoi(token);
                    if (!(cin >> e_label)) finish(_wa, "Invalid query format");
                } catch (...) {
                    finish(_wa, "Invalid query token (expected '!', '?', or integer)");
                }
            }
            
            current_queries++;
            
            if (current_queries % 100 == 0) log_metrics();
            
            
            
            
            if (current_queries > current_limit) {
                cout << -1 << endl;
                finish(_wa, "Query limit exceeded");
            }
            
            
            if (p < 0 || p >= (int)history_sets.size()) {
                cout << -1 << endl;
                finish(_wa, "Invalid query index p (out of range)");
            }
            
            
            if (e_label < 1 || e_label > M) {
                cout << -1 << endl;
                finish(_wa, "Invalid edge label e (out of range)");
            }
            
            int e_idx = e_label - 1; 
            
            
            if (!history_sets[p][e_idx]) {
                cout << -1 << endl;
                finish(_wa, "Edge not present in referenced query set");
            }
            
            
            history_sets.push_back(history_sets[p]);
            history_sets.back()[e_idx] = false;
            
            
            bool connected = check_conn(history_sets.back());
            cout << (connected ? 1 : 0) << endl;
        }
    }
}

int main(int argc, char* argv[]) {
    
    registerInteraction(argc, argv);
    atexit(log_metrics);
    
    
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    
    
    int T = inf.readInt();
    cout << T << endl;
    
    
    for (int i = 0; i < T; ++i) {
        solve_case();
    }
    
    finish(_ok, "All test cases passed");
    return 0;
}


Adaptive interactor (C++):

Test generator (C++):
#include "testlib.h"
#include <bits/stdc++.h>
using namespace std;


struct Graph {
    int n;
    vector<pair<int, int>> edges;
};



vector<pair<int, int>> genTree(int n) {
    vector<pair<int, int>> tree_edges;
    if (n <= 1) return tree_edges;
    
    
    
    
    vector<int> p(n + 1);
    for (int i = 2; i <= n; i++) {
        p[i] = rnd.next(1, i - 1);
        tree_edges.push_back({i, p[i]});
    }
    
    
    vector<int> perm(n + 1);
    for (int i = 1; i <= n; i++) perm[i] = i;
    shuffle(perm.begin() + 1, perm.end());
    
    vector<pair<int, int>> ret;
    for (auto e : tree_edges) {
        ret.push_back({perm[e.first], perm[e.second]});
    }
    return ret;
}



Graph genCactus(int n, int target_m, bool force_tricky = false) {
    Graph g;
    g.n = n;
    
    
    
    vector<pair<int, int>> tree_edges = genTree(n);
    
    
    
    vector<vector<pair<int, int>>> adj(n + 1);
    for (int i = 0; i < (int)tree_edges.size(); i++) {
        adj[tree_edges[i].first].push_back({tree_edges[i].second, i});
        adj[tree_edges[i].second].push_back({tree_edges[i].first, i});
    }
    
    
    vector<bool> edge_in_cycle(tree_edges.size(), false);
    vector<pair<int, int>> extra_edges;
    int current_m = (int)tree_edges.size();
    
    
    
    auto getPath = [&](int u, int v) -> vector<int> {
        if (u == v) return {};
        queue<pair<int, vector<int>>> q;
        q.push({u, {}});
        vector<bool> vis(n + 1, false);
        vis[u] = true;
        
        while(!q.empty()){
            auto [curr, path] = q.front();
            q.pop();
            if (curr == v) return path;
            
            for (auto& edge : adj[curr]) {
                int nxt = edge.first;
                int id = edge.second;
                if (!vis[nxt]) {
                    vis[nxt] = true;
                    vector<int> new_path = path;
                    new_path.push_back(id);
                    q.push({nxt, new_path});
                }
            }
        }
        return {};
    };
    
    
    int attempts = 0;
    while (current_m < target_m && attempts < 2000) {
        attempts++;
        
        int len_target = -1;
        if (force_tricky) {
            int r = rnd.next(0, 8);
            if (r == 0) len_target = 14;      
            else if (r == 1) len_target = 15; 
            else if (r == 2) len_target = 1;  
            else if (r == 3) len_target = 2;  
            
        } else {
            
            if (rnd.next(0, 30) == 0) len_target = 14;
            if (rnd.next(0, 30) == 0) len_target = 15;
        }
        
        
        if (len_target != -1) {
            if (len_target == 1) {
                
                int u = rnd.next(1, n);
                extra_edges.push_back({u, u});
                current_m++;
                continue;
            }
            
            
            int dist_needed = len_target - 1;
            bool found = false;
            
            
            for(int k=0; k<15; ++k) {
                int u = rnd.next(1, n);
                
                
                vector<int> candidates;
                queue<pair<int, int>> q;
                q.push({u, 0});
                vector<int> dist(n + 1, -1);
                dist[u] = 0;
                
                while(!q.empty()){
                    auto [curr, d] = q.front();
                    q.pop();
                    if (d == dist_needed) candidates.push_back(curr);
                    if (d < dist_needed) {
                        for(auto& e : adj[curr]) {
                            if(dist[e.first] == -1) {
                                dist[e.first] = d+1;
                                q.push({e.first, d+1});
                            }
                        }
                    }
                }
                
                if(candidates.empty()) continue;
                int v = candidates[rnd.next(0, (int)candidates.size() - 1)];
                vector<int> path = getPath(u, v);
                
                
                bool ok = true;
                for(int id : path) if(edge_in_cycle[id]) ok = false;
                
                if(ok) {
                    for(int id : path) edge_in_cycle[id] = true;
                    extra_edges.push_back({u, v});
                    current_m++;
                    found = true;
                    break;
                }
            }
        } else {
            
            int u = rnd.next(1, n);
            int v = rnd.next(1, n);
            
            if (u == v) {
                
                extra_edges.push_back({u, u});
                current_m++;
            } else {
                vector<int> path = getPath(u, v);
                bool ok = true;
                for(int id : path) if(edge_in_cycle[id]) ok = false;
                
                if(ok) {
                    for(int id : path) edge_in_cycle[id] = true;
                    extra_edges.push_back({u, v});
                    current_m++;
                }
            }
        }
    }
    
    
    for (auto e : tree_edges) g.edges.push_back(e);
    for (auto e : extra_edges) g.edges.push_back(e);
    
    
    shuffle(g.edges.begin(), g.edges.end());
    
    return g;
}

int main(int argc, char* argv[]) {
    registerGen(argc, argv, 1);
    
    string mode = opt<string>("mode", "non");
    int seed = atoi(argv[1]);
    
    vector<Graph> cases;
    
    
    if (seed <= 3) {
        if (seed == 1) {
            
            cases.push_back(genCactus(8, 7)); 
            
            
            Graph g_loop; g_loop.n = 1; g_loop.edges = {{1, 1}};
            cases.push_back(g_loop);
            
            
            Graph g_double; g_double.n = 2; g_double.edges = {{1, 2}, {1, 2}};
            cases.push_back(g_double);
            
            
            
            Graph g14; g14.n = 14;
            for(int i=1; i<14; i++) g14.edges.push_back({i, i+1});
            g14.edges.push_back({14, 1});
            shuffle(g14.edges.begin(), g14.edges.end());
            cases.push_back(g14);
            
        } else if (seed == 2) {
            
            Graph g15; g15.n = 15;
            for(int i=1; i<15; i++) g15.edges.push_back({i, i+1});
            g15.edges.push_back({15, 1});
            shuffle(g15.edges.begin(), g15.edges.end());
            cases.push_back(g15);
            
            
            cases.push_back(genCactus(20, 25, true));
            
        } else { 
            
            cases.push_back(genCactus(10, 20, true));
            cases.push_back(genCactus(6, 10, true));
        }
    } 
    
    else {
        int total_m = 1000;
        
        if (seed > 10) total_m = rnd.next(2000, 10000);
        else total_m = rnd.next(100, 1000);
        
        int t = rnd.next(1, 10);
        
        if (total_m < 20) t = 1;
        
        
        vector<int> ms = rnd.partition(t, total_m, 1);
        
        for (int m : ms) {
            
            
            int n_low = max(1, (int)sqrt(m));
            int n_high = m + 1;
            int n = rnd.next(n_low, n_high);
            
            
            if (n < 1) n = 1;
            
            
            cases.push_back(genCactus(n, m, (seed % 7 == 0)));
        }
    }
    
    
    
    cout << cases.size() << endl;
    for(const auto& g : cases) {
        if (mode == "non") {
            cout << g.n << " " << g.edges.size() << endl;
            for(const auto& e : g.edges) {
                cout << e.first << " " << e.second << endl;
            }
        } else {
            
            cout << g.n << " " << g.edges.size() << endl;
        }
    }
    
    return 0;
}