按数字从小到大贪心选择可到达的最小终点,并用并查集维护每个节点周围边的局部删除顺序约束。
OJ: luogu
题目 ID: P5659
难度:提高+/省选-
标签:贪心树形结构并查集构造
日期: 2026-07-06 08:33
题意
有一棵
初始时,数字
接下来要删掉所有
所有边删完后,数字都停在某些节点上。按数字从小到大看它们所在的节点,得到一个排列:
P[1], P[2], ..., P[n]要求在所有删边顺序中,使这个排列字典序最小。
思路
这题最直接的暴力是枚举所有删边顺序。
// brute.cpp:小数据暴力枚举所有删边顺序,直接模拟交换过程。
#include <bits/stdc++.h>
using namespace std;
struct Edge {
int u;
int v;
};
const int MAXN = 12;
int n;
int initial_pos[MAXN];
vector<Edge> edges;
vector<int> solve_by_greedy_for_sample(); // 大样例兜底,随机对拍只生成 n <= 8。
vector<int> simulate_order(const vector<int> &order) {
int number_on_node[MAXN];
int final_pos[MAXN];
for (int value = 1; value <= n; value++) {
number_on_node[initial_pos[value]] = value;
}
for (int i = 0; i < (int)order.size(); i++) {
Edge e = edges[order[i]];
swap(number_on_node[e.u], number_on_node[e.v]);
}
for (int node = 1; node <= n; node++) {
final_pos[number_on_node[node]] = node;
}
vector<int> answer(n + 1);
for (int value = 1; value <= n; value++) {
answer[value] = final_pos[value];
}
return answer;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int test_count;
cin >> test_count;
while (test_count--) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> initial_pos[i];
}
edges.clear();
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
edges.push_back({u, v});
}
vector<int> best(n + 1, n + 1);
if (n <= 8) {
vector<int> order;
for (int i = 0; i < n - 1; i++) {
order.push_back(i);
}
do {
vector<int> cur = simulate_order(order);
if (lexicographical_compare(cur.begin() + 1, cur.end(), best.begin() + 1, best.end())) {
best = cur;
}
} while (next_permutation(order.begin(), order.end()));
} else {
best = solve_by_greedy_for_sample();
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << best[i];
}
cout << '\n';
}
return 0;
}
// 下面是和 main.cpp 同思路的简化版,只用于让官方样例中的 n=10 数据也能运行。
const int MAXB = 2005;
vector<int> brute_graph[MAXB];
int first_edge_b[MAXB], last_edge_b[MAXB], chain_count_b[MAXB];
struct LocalOrderB {
int father[MAXB];
bool has_prev[MAXB], has_next[MAXB];
void clear(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
has_prev[i] = false;
has_next[i] = false;
}
}
int find_root(int x) {
if (father[x] == x) {
return x;
}
father[x] = find_root(father[x]);
return father[x];
}
bool same_chain(int x, int y) {
return find_root(x) == find_root(y);
}
void join_next(int x, int y) {
father[find_root(y)] = find_root(x);
has_next[x] = true;
has_prev[y] = true;
}
} order_b[MAXB];
bool can_end_b(int u, int in_edge) {
if (in_edge == 0) {
return false;
}
if (last_edge_b[u] != 0 && last_edge_b[u] != in_edge) {
return false;
}
if (order_b[u].has_next[in_edge]) {
return false;
}
if (first_edge_b[u] != 0 && chain_count_b[u] > 1 && order_b[u].same_chain(in_edge, first_edge_b[u])) {
return false;
}
return true;
}
bool can_start_b(int u, int out_edge) {
if (first_edge_b[u] != 0 && first_edge_b[u] != out_edge) {
return false;
}
if (order_b[u].has_prev[out_edge]) {
return false;
}
if (last_edge_b[u] != 0 && chain_count_b[u] > 1 && order_b[u].same_chain(out_edge, last_edge_b[u])) {
return false;
}
return true;
}
bool can_middle_b(int u, int in_edge, int out_edge) {
if (in_edge == last_edge_b[u] || out_edge == first_edge_b[u]) {
return false;
}
if (order_b[u].same_chain(in_edge, out_edge)) {
return false;
}
if (order_b[u].has_next[in_edge] || order_b[u].has_prev[out_edge]) {
return false;
}
if (first_edge_b[u] != 0 && last_edge_b[u] != 0 && chain_count_b[u] > 2 &&
order_b[u].same_chain(in_edge, first_edge_b[u]) &&
order_b[u].same_chain(out_edge, last_edge_b[u])) {
return false;
}
return true;
}
int dfs_find_b(int u, int parent_from) {
int best = n + 1;
if (can_end_b(u, parent_from)) {
best = min(best, u);
}
for (int i = 0; i < (int)brute_graph[u].size(); i++) {
int v = brute_graph[u][i];
if (v == parent_from) {
continue;
}
if ((parent_from == 0 && can_start_b(u, v)) ||
(parent_from != 0 && can_middle_b(u, parent_from, v))) {
best = min(best, dfs_find_b(v, u));
}
}
return best;
}
bool dfs_apply_b(int u, int parent_from, int target) {
if (u == target) {
last_edge_b[u] = parent_from;
return true;
}
for (int i = 0; i < (int)brute_graph[u].size(); i++) {
int v = brute_graph[u][i];
if (v == parent_from) {
continue;
}
if (dfs_apply_b(v, u, target)) {
if (parent_from == 0) {
first_edge_b[u] = v;
} else {
order_b[u].join_next(parent_from, v);
chain_count_b[u]--;
}
return true;
}
}
return false;
}
vector<int> solve_by_greedy_for_sample() {
for (int i = 1; i <= n; i++) {
brute_graph[i].clear();
first_edge_b[i] = last_edge_b[i] = chain_count_b[i] = 0;
order_b[i].clear(n);
}
for (int i = 0; i < (int)edges.size(); i++) {
int u = edges[i].u;
int v = edges[i].v;
brute_graph[u].push_back(v);
brute_graph[v].push_back(u);
chain_count_b[u]++;
chain_count_b[v]++;
}
vector<int> result(n + 1);
for (int value = 1; value <= n; value++) {
int target = dfs_find_b(initial_pos[value], 0);
dfs_apply_b(initial_pos[value], 0, target);
result[value] = target;
}
return result;
}brute.cpp 对小数据直接枚举
要做正解,需要换一个角度:不要枚举删边顺序,而是描述"一个数字能不能被安排到某个终点"。
一个数字怎么移动
考虑某个数字,它从初始节点
树上
s = v0 - v1 - v2 - ... - vk = t这个数字要从
也就是说,在每个中间点
所以在节点
边 (v_{i-1}, v_i) 必须早于边 (v_i, v_{i+1})起点只规定"第一条离开的边",终点只规定"最后一条进入的边"。
为什么可以按数字贪心
最终排列是按数字
所以我们先让数字
这就是标准的字典序贪心:
for value = 1..n:
在当前已有顺序约束下,找到数字 value 能到达的最小节点
把这条路径带来的新顺序约束加入系统问题变成:怎样判断一条路径还能不能加入?
每个节点只关心自己周围的边
路径经过一个节点时,只会约束这个节点周围的两条边的相对顺序。
因此我们对每个节点
为了方便表示,一条"从
例如在节点
x -> y -> z意思是:连向
新增一条路径经过
in_edge -> out_edge也就是"进入边早于离开边"。
需要维护哪些信息
对每个节点
first_edge[u]:如果已经确定了第一条边,它是哪条;last_edge[u]:如果已经确定了最后一条边,它是哪条;has_prev[x]:在周围,连向 的边前面是否已经有边; has_next[x]:在周围,连向 的边后面是否已经有边; - 并查集:把已经连成一条局部顺序链的边合在一起;
chain_count[u]:当前周围还有多少条局部顺序链。
为什么要用并查集?
因为如果已经有一条链:
a -> b -> c再加入:
c -> a就会形成环,这不可能是一个合法的删除顺序。并查集可以快速判断两条边是否已经在同一条链里。
判断路径的三种位置
当我们从数字初始点出发 DFS,尝试到达某个目标点时,一个节点在路径中有三种身份。
1. 起点
起点只有离开的边 out_edge。
它必须能作为当前节点周围顺序里的第一条边:
- 如果已经有
first_edge[u],那它必须等于out_edge; out_edge前面不能已经有别的边;- 不能和已有最后一条边形成不合法闭环。
2. 终点
终点只有进入的边 in_edge。
它必须能作为当前节点周围顺序里的最后一条边:
- 如果已经有
last_edge[u],那它必须等于in_edge; in_edge后面不能已经有别的边;- 不能和已有第一条边形成不合法闭环。
3. 中间点
中间点有进入边 in_edge 和离开边 out_edge。
需要加入:
in_edge -> out_edge它必须满足:
in_edge不能已经是最后一条边;out_edge不能已经是第一条边;in_edge和out_edge不能已经在同一条链里;in_edge后面不能已经有边;out_edge前面不能已经有边;- 如果它们分别接在第一条链和最后一条链上,也不能把所有链提前接成环。
这些判断看起来多,但本质都是一句话:
每个节点周围的边,必须始终能排成一条没有分叉、没有环的线性顺序。找最小终点并加入约束
处理数字
- 从它的初始节点
start_pos[value]出发 DFS; - 沿途只走当前约束允许的路径;
- 在所有可达终点中取编号最小的那个;
- 再沿这条路径走一遍,把路径带来的局部顺序约束真正加入。
加入约束时:
- 起点:记录
first_edge; - 终点:记录
last_edge; - 中间点:把
in_edge -> out_edge加入当前点的局部链,并让chain_count减一。
因为每次都在当前约束下选最小终点,所以前面数字的最优选择不会被后面数字破坏。
代码
// main.cpp:按数字从小到大贪心确定最终位置,维护每个点周围边的删除顺序约束。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
struct LocalOrder {
int father[MAXN];
bool has_prev[MAXN]; // has_prev[x]:在当前点处,连向 x 的边前面已经固定有一条边
bool has_next[MAXN]; // has_next[x]:在当前点处,连向 x 的边后面已经固定有一条边
void clear(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
has_prev[i] = false;
has_next[i] = false;
}
}
int find_root(int x) {
if (father[x] == x) {
return x;
}
father[x] = find_root(father[x]);
return father[x];
}
bool same_chain(int x, int y) {
return find_root(x) == find_root(y);
}
// 在某个点 u 周围,加入“边 x 必须紧接在边 y 之前删除”的关系。
void join_next(int x, int y) {
int fx = find_root(x);
int fy = find_root(y);
father[fy] = fx;
has_next[x] = true;
has_prev[y] = true;
}
};
int n;
int start_pos[MAXN]; // start_pos[i]:数字 i 初始所在节点
vector<int> graph_edge[MAXN];
int first_edge[MAXN]; // first_edge[u]:点 u 周围已固定的第一条删除边,记录对端点
int last_edge[MAXN]; // last_edge[u]:点 u 周围已固定的最后一条删除边,记录对端点
int chain_count[MAXN]; // 点 u 周围还剩多少条局部关系链
LocalOrder order_info[MAXN];
void clear_case() {
for (int i = 1; i <= n; i++) {
graph_edge[i].clear();
first_edge[i] = 0;
last_edge[i] = 0;
chain_count[i] = 0;
order_info[i].clear(n);
}
}
bool can_be_end(int u, int in_edge) {
if (in_edge == 0) {
return false;
}
if (last_edge[u] != 0 && last_edge[u] != in_edge) {
return false;
}
if (order_info[u].has_next[in_edge]) {
return false;
}
if (first_edge[u] != 0 && chain_count[u] > 1 && order_info[u].same_chain(in_edge, first_edge[u])) {
return false;
}
return true;
}
bool can_leave_start(int u, int out_edge) {
if (first_edge[u] != 0 && first_edge[u] != out_edge) {
return false;
}
if (order_info[u].has_prev[out_edge]) {
return false;
}
if (last_edge[u] != 0 && chain_count[u] > 1 && order_info[u].same_chain(out_edge, last_edge[u])) {
return false;
}
return true;
}
bool can_pass_middle(int u, int in_edge, int out_edge) {
if (in_edge == last_edge[u]) {
return false;
}
if (out_edge == first_edge[u]) {
return false;
}
if (order_info[u].same_chain(in_edge, out_edge)) {
return false;
}
if (order_info[u].has_next[in_edge]) {
return false;
}
if (order_info[u].has_prev[out_edge]) {
return false;
}
if (first_edge[u] != 0 && last_edge[u] != 0 && chain_count[u] > 2 &&
order_info[u].same_chain(in_edge, first_edge[u]) &&
order_info[u].same_chain(out_edge, last_edge[u])) {
return false;
}
return true;
}
int find_best_target(int u, int parent_from) {
int best = n + 1;
if (can_be_end(u, parent_from)) {
best = min(best, u);
}
for (int i = 0; i < (int)graph_edge[u].size(); i++) {
int v = graph_edge[u][i];
if (v == parent_from) {
continue;
}
bool ok = false;
if (parent_from == 0) {
ok = can_leave_start(u, v);
} else {
ok = can_pass_middle(u, parent_from, v);
}
if (ok) {
best = min(best, find_best_target(v, u));
}
}
return best;
}
bool apply_path(int u, int parent_from, int target) {
if (u == target) {
last_edge[u] = parent_from;
return true;
}
for (int i = 0; i < (int)graph_edge[u].size(); i++) {
int v = graph_edge[u][i];
if (v == parent_from) {
continue;
}
if (apply_path(v, u, target)) {
if (parent_from == 0) {
first_edge[u] = v;
} else {
order_info[u].join_next(parent_from, v);
chain_count[u]--;
}
return true;
}
}
return false;
}
void solve_case() {
cin >> n;
clear_case();
for (int i = 1; i <= n; i++) {
cin >> start_pos[i];
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
graph_edge[u].push_back(v);
graph_edge[v].push_back(u);
chain_count[u]++;
chain_count[v]++;
}
if (n == 1) {
cout << 1 << '\n';
return;
}
for (int value = 1; value <= n; value++) {
int target = find_best_target(start_pos[value], 0);
apply_path(start_pos[value], 0, target);
if (value > 1) {
cout << ' ';
}
cout << target;
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int test_count;
cin >> test_count;
while (test_count--) {
solve_case();
}
return 0;
}复杂度
每个数字都可能 DFS 扫一遍整棵树,并再沿路径加入约束。
- 时间复杂度:
,其中 来自并查集。 - 空间复杂度:
,因为每个节点都维护一套以邻点编号为下标的局部并查集和前后继标记。
本题
总结
这题的难点是把"删边顺序导致数字移动"转成约束模型。
一个数字沿路径移动时,在每个中间点都会留下一个局部顺序:
进入边必须早于离开边最终我们不是直接构造全局删边顺序,而是维护每个节点周围的局部删除顺序是否仍然可行。
然后按数字从小到大贪心:每次在当前约束下找能到达的最小节点,并把这条路径对应的约束加入。这样得到的就是字典序最小的最终排列。
本文的 main.cpp 已用小数据全排列删边模拟版 brute.cpp 进行 500 组随机树对拍。

