1 条题解
-
0
思路与算法
把每条边的交换看作一个事件。某顶点初始的数字由该点最早发生的事件带走;从边e进入该点的数字,若e是最后事件就停在该点,否则从紧接e的下一事件出去。因此只需给各点的关联边安排一个局部全序。已固定数字的路径会强制某些局部事件连续,并固定局部首事件或尾事件。
依数字1到n处理。它的初始位置就是输入pos[i],不是排列的逆。树上起终点间路径唯一,从起点DFS所有仍可扩展的路径,在可停位置中取最小编号。经过一处中间顶点时,要求进入事件尚无后继、出去事件尚无前驱,且连接不会成环;若连接含首链和尾链,也不能把尚未接入的其他链封在外面。起点相应检查新首事件,终点检查新尾事件。并查集维护强制连续链,cnt记录该点尚有几条链。选定最小终点后,把路径的首、连续及尾约束永久写入,再处理下一个数字。n=1直接输出1。
正确性说明
单个顶点的可扩展性:强制连续约束构成若干互不相交的有向链。没有环、首链无前驱、尾链无后继,且未过早合并首尾链时,可以把剩余链以任意顺序接起来,得到满足所有约束的全序;程序的连接、起始和停止条件正是这些必要充分条件。
全局可实现性:把有共同端点的边事件按局部全序定向。树的线图由对应各原顶点的团粘接而成,不同团仅在割点相接。一个最短有向环若跨团,必须重复经过团间割点,矛盾;若完全处在一个团,又与局部全序无环矛盾。因此事件依赖无环,存在全局拓扑序,能按此顺序交换并删除每条边。局部连续关系保证已确定数字沿指定路径行走。
DFS枚举所有满足局部必要充分条件的路径,所以得到的终点都有完整删除顺序可扩展,也不会遗漏任何可扩展终点。选择其中最小者,保留此前较小数字的终点,符合字典序逐位最小原则。对数字编号归纳,最终答案就是所有合法删除顺序中最小的位置序列。
复杂度
每组初始化邻接表、事件数组和并查集O(n)。每个数字对树做一次DFS并固化一条至多n-1边的路径,总计O(n²α(n))时间;找根采用路径压缩。所有数组、显式DFS栈和路径均为O(n)空间,多组顺序处理。
易错点
pos[i]表示数字i的初始位置;输出也按数字编号,而非按顶点编号。局部事件在边的两个端点使用不同有向编号;反向编号用异或1。不能只检查并查集是否成环,还要避免在其他链未接入时提前连通首尾链。树深可到n-1,使用显式栈。数据为自编训练数据与公开样例,不是官方原始测试数据。
参考代码(C++11)
#include <bits/stdc++.h> using namespace std; struct Solver { int n; vector<vector<int>> g; vector<int> to, fa, prev, next, head, tail, cnt; int find(int x) { while (fa[x] != x) { fa[x] = fa[fa[x]]; x = fa[x]; } return x; } bool endOK(int u, int e) { return tail[u] < 0 && (head[u] < 0 || find(e) != find(head[u]) || cnt[u] == 1); } bool joinOK(int u, int e, int f) { if (prev[f] >= 0 || f == head[u] || find(e) == find(f)) return false; return !(head[u] >= 0 && tail[u] >= 0 && find(e) == find(head[u]) && find(f) == find(tail[u]) && cnt[u] > 2); } void run() { cin >> n; vector<int> pos(n); for (int &v : pos) { cin >> v; --v; } g.assign(n, {}); to.clear(); for (int i = 1, u, v; i < n; i++) { cin >> u >> v; --u; --v; int e = to.size(); to.push_back(v); to.push_back(u); g[u].push_back(e); g[v].push_back(e + 1); } if (n == 1) { cout << 1 << '\n'; return; } int m = to.size(); fa.resize(m); iota(fa.begin(), fa.end(), 0); prev.assign(m, -1); next.assign(m, -1); head.assign(n, -1); tail.assign(n, -1); cnt.resize(n); for (int u = 0; u < n; u++) cnt[u] = g[u].size(); vector<int> par(n), pe(n); vector<pair<int, int>> stack; vector<int> path; for (int s : pos) { fill(par.begin(), par.end(), -1); stack.clear(); stack.push_back({s, -1}); int dest = n; while (!stack.empty()) { int u = stack.back().first, e = stack.back().second; stack.pop_back(); if (e >= 0 && endOK(u, e)) dest = min(dest, u); for (int f : g[u]) { int v = to[f]; if (v == par[u]) continue; bool ok = e < 0 ? (prev[f] < 0 && f != head[u] && (tail[u] < 0 || find(f) != find(tail[u]) || cnt[u] == 1)) : (next[e] < 0 && tail[u] != e && joinOK(u, e, f)); if (ok) { par[v] = u; pe[v] = f; stack.push_back({v, f ^ 1}); } } } assert(dest < n); path.clear(); for (int u = dest; u != s; u = par[u]) path.push_back(pe[u]); reverse(path.begin(), path.end()); head[s] = path[0]; for (size_t i = 1; i < path.size(); i++) { int e = path[i - 1] ^ 1, f = path[i], u = to[path[i - 1]]; next[e] = f; prev[f] = e; fa[find(e)] = find(f); --cnt[u]; } tail[dest] = path.back() ^ 1; cout << dest + 1 << ' '; } cout << '\n'; } }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { Solver s; s.run(); } }
- 1