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.
Forget the original wording for a second and stare at a DFS tree from a candidate root . If is really interesting, what kinds of non-tree edges are even possible without creating a second simple path?
Prove the clean equivalence: is interesting iff in the DFS tree from , every edge is either a tree edge or goes to an ancestor. Any forward/cross edge instantly gives another simple path. And if every non-tree edge goes to an ancestor, a simple path from cannot use such an edge at all, because it would repeat a vertex.
You only need the full list when at least of vertices are interesting. So to find one interesting root, random sampling is enough: if there are at least good vertices, the probability of missing all of them in tries is .
Fix one interesting root and its tree . Now analyze another vertex . Any simple path starting from can use at most one non-tree edge, and if it wants to leave , that edge must go from some vertex inside to a strict ancestor of .
Here is the punchline. For , is interesting iff among all back edges whose source lies in , there is exactly one edge going to a strict ancestor of , and its target is exactly . So compute two subtree values: = number of back edges from to , and = minimum depth of a non-root back-edge target inside . Then is interesting iff and .
Take a candidate root and run a DFS from it. Let the DFS tree be the parent tree.
Claim:
Why?
So one candidate can be checked in using DFS order and ancestor checks.
That part is the easy one. The annoying part is: how do we find such a root?
If there are at least interesting vertices, then a uniformly random vertex is interesting with probability at least .
So if we try random vertices, the probability of missing all interesting ones is
With this is below . Basically: if you still miss, the universe personally hates you.
So:
This is exactly why the problem has the probabilities tag. It is not there for decoration.
Now suppose we found one interesting root . Run DFS from and get its tree .
Because is interesting, every edge is either:
Now take some other vertex .
What can a simple path starting from look like?
This completely kills the complexity of the problem.
For , the vertex is interesting iff among all back edges with source in :
Why is this necessary?
Why is it sufficient?
No choices, no drama, no extra paths.
We only need two subtree aggregates.
Let
This is the number of back edges from directly to .
Let
over all back edges with and .
If no such edge exists, set .
Then for :
Why does the condition work?
Scan all edges once.
For each non-tree edge :
Then process the DFS tree in reverse order (postorder-ish) and merge children into parents:
Finally:
If the number of interesting vertices is and
print , otherwise print them in increasing order.
One root check is .
We do at most random checks, so the randomized search is
with a constant factor. After finding one interesting root, the final subtree DP is another .
So the whole solution is linear per test case up to a small constant:
And yes, for a this one is surprisingly clean once you spot the structure. Before that, it's just graph-shaped pain.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
static mt19937 rng((unsigned)chrono::steady_clock::now().time_since_epoch().count());
bool build_and_check(int root, const vector<vector<int>>& g,
vector<int>& par, vector<int>& dep,
vector<int>& tin, vector<int>& tout,
vector<int>& order) {
int n = (int)g.size() - 1;
par.assign(n + 1, -1);
dep.assign(n + 1, 0);
tin.assign(n + 1, 0);
tout.assign(n + 1, 0);
order.clear();
order.reserve(n);
vector<int> it(n + 1, 0), st;
st.reserve(n);
st.push_back(root);
par[root] = 0;
int timer = 0;
while (!st.empty()) {
int u = st.back();
if (!tin[u]) {
tin[u] = ++timer;
order.push_back(u);
}
if (it[u] < (int)g[u].size()) {
int v = g[u][it[u]++];
if (!tin[v]) {
par[v] = u;
dep[v] = dep[u] + 1;
st.push_back(v);
}
} else {
tout[u] = timer;
st.pop_back();
}
}
if (timer != n) return false;
for (int u = 1; u <= n; ++u) {
for (int v : g[u]) {
if (par[v] == u) continue;
if (tin[v] <= tin[u] && tout[u] <= tout[v]) continue;
return false;
}
}
return true;
}
int main() {
setIO();
int T;
cin >> T;
const int TRIES = 80;
const int INF = (int)1e9;
while (T--) {
int n, m;
cin >> n >> m;
vector<vector<int>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
}
vector<int> perm(n);
iota(perm.begin(), perm.end(), 1);
shuffle(perm.begin(), perm.end(), rng);
vector<int> par, dep, tin, tout, order;
int root = -1;
int lim = min(n, TRIES);
for (int i = 0; i < lim; ++i) {
int cand = perm[i];
if (build_and_check(cand, g, par, dep, tin, tout, order)) {
root = cand;
break;
}
}
if (root == -1) {
cout << -1 << '\n';
continue;
}
vector<int> cnt(n + 1, 0), mn(n + 1, INF);
for (int u = 1; u <= n; ++u) {
for (int v : g[u]) {
if (par[v] == u) continue; // tree edge
if (v == root) cnt[u]++;
else mn[u] = min(mn[u], dep[v]);
}
}
for (int i = n - 1; i >= 0; --i) {
int u = order[i];
int p = par[u];
if (p != 0) {
cnt[p] += cnt[u];
mn[p] = min(mn[p], mn[u]);
}
}
vector<int> ans;
ans.reserve(n);
for (int u = 1; u <= n; ++u) {
if (u == root || (cnt[u] == 1 && mn[u] >= dep[u])) {
ans.push_back(u);
}
}
if ((int)ans.size() * 5 < n) {
cout << -1 << '\n';
} else {
for (int i = 0; i < (int)ans.size(); ++i) {
if (i) cout << ' ';
cout << ans[i];
}
cout << '\n';
}
}
}#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
static mt19937 rng((unsigned)chrono::steady_clock::now().time_since_epoch().count());
bool build_and_check(int root, const vector<vector<int>>& g,
vector<int>& par, vector<int>& dep,
vector<int>& tin, vector<int>& tout,
vector<int>& order) {
int n = (int)g.size() - 1;
par.assign(n + 1, -1);
dep.assign(n + 1, 0);
tin.assign(n + 1, 0);
tout.assign(n + 1, 0);
order.clear();
order.reserve(n);
vector<int> it(n + 1, 0), st;
st.reserve(n);
st.push_back(root);
par[root] = 0;
int timer = 0;
while (!st.empty()) {
int u = st.back();
if (!tin[u]) {
tin[u] = ++timer;
order.push_back(u);
}
if (it[u] < (int)g[u].size()) {
int v = g[u][it[u]++];
if (!tin[v]) {
par[v] = u;
dep[v] = dep[u] + 1;
st.push_back(v);
}
} else {
tout[u] = timer;
st.pop_back();
}
}
if (timer != n) return false;
for (int u = 1; u <= n; ++u) {
for (int v : g[u]) {
if (par[v] == u) continue;
if (tin[v] <= tin[u] && tout[u] <= tout[v]) continue;
return false;
}
}
return true;
}
int main() {
setIO();
int T;
cin >> T;
const int TRIES = 80;
const int INF = (int)1e9;
while (T--) {
int n, m;
cin >> n >> m;
vector<vector<int>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
}
vector<int> perm(n);
iota(perm.begin(), perm.end(), 1);
shuffle(perm.begin(), perm.end(), rng);
vector<int> par, dep, tin, tout, order;
int root = -1;
int lim = min(n, TRIES);
for (int i = 0; i < lim; ++i) {
int cand = perm[i];
if (build_and_check(cand, g, par, dep, tin, tout, order)) {
root = cand;
break;
}
}
if (root == -1) {
cout << -1 << '\n';
continue;
}
vector<int> cnt(n + 1, 0), mn(n + 1, INF);
for (int u = 1; u <= n; ++u) {
for (int v : g[u]) {
if (par[v] == u) continue; // tree edge
if (v == root) cnt[u]++;
else mn[u] = min(mn[u], dep[v]);
}
}
for (int i = n - 1; i >= 0; --i) {
int u = order[i];
int p = par[u];
if (p != 0) {
cnt[p] += cnt[u];
mn[p] = min(mn[p], mn[u]);
}
}
vector<int> ans;
ans.reserve(n);
for (int u = 1; u <= n; ++u) {
if (u == root || (cnt[u] == 1 && mn[u] >= dep[u])) {
ans.push_back(u);
}
}
if ((int)ans.size() * 5 < n) {
cout << -1 << '\n';
} else {
for (int i = 0; i < (int)ans.size(); ++i) {
if (i) cout << ' ';
cout << ans[i];
}
cout << '\n';
}
}
}