CSP-S 2019 树上的数
树上的数
题面
现在有一棵树,每个点上有一个点权,你切断一条边,就会交换边上两个点的点权,求\(1 \to N\)点权的最小字典序。
\(\text{subtask1}\) \(N \leq 10\)
考场上暴力标配,\(\Theta(N!)\)枚举所有删边顺序,然后取字典序最小的一组。
\(\text{subtask2}\) 存在度数为\(N - 1\)的节点
存在度数为\(N-1\)的节点,那么这个图一定是菊花图。
我们设菊花图入度为\(N-1\)的点为\(rt\)。
我们每次删边一段必定是\(rt\),假设我们删边顺序是\(p\),\(p_i\)表示第\(i\)次删的点编号。
第\(i\)次删的边就是\(rt -> p_i\)。
每次删边,当前\(p_i\)的点权就确定了,这个性质比较好。
经过几遍模拟,我们发现\(a_{p_1}\)到了\(p_2\),\(a_{p_2}\)到了\(p_3\),\(a_{p_{n-1}}\)到了\(rt\)。
这样我们是不是形成了一个环的形式。
\(rt->p_1->p_2->p_3...->p_{n-1}->rt\)
要想让字典序最小,我们贪心的选择能得到的最小的数字,并且这个环要不能是多个小环。
我们如果想让数字\(i\)到\(j\)点的话,那么首先\(j\)这个点没有用过,并且\(j\)和\(p_i\)不在同一个环里(不然形成不了一个大环)。
时间复杂度\(\Theta(N^2)\)
namespace subtask2 {
int fa[N], ans[N];
bool vis[N];
inline int find (int x) {
return x == fa[x] ? x : fa[x] = find (fa[x]);
}
inline void merge (int x, int y) {
fa[find (x)] = find (y);
}
void main () {
for (int i = 1; i <= n; i ++ ) fa[i] = i, vis[i] = 0;
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ ) {
if (!vis[j] && (i == n || find (j) != find (p[i]))) {
ans[i] = j; vis[j] = 1; merge (j, p[i]);
break;
}
}
}
for (int i = 1; i <= n; i ++ ) printf ("%d%c", ans[i], i == n ? '\n' : ' ');
}
}
\(\text{subtask3}\) 树是一条链 \(N \leq 160\)
如果想出链的部分分,正解也就不远了。
首先按照菊花图的思路,我们要让数字\(i\)走向\(j\)点。
我们规定三类点(正解也要用到)
1.起点 数字\(i\)所在的点。
2.途经点:从起点到终点路过的那些点。
3.终点:\(j\)点的位置。
我们先把链\(dfs\)处理一下,记录链上第\(i\)个点是\(a_i\),\(u\)点是第\(num_u\)个。
inline void dfs (int u, int f) {
a[num[u] = ++ cnt] = u;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == f) continue;
dfs (v, u);
}
}
首先对于一个点,删边顺序有两种,先删掉左后删右,先删右后删左。
我们用一个\(tag\)数组表示一下这个点删边顺序。\(tag = 0\)表示这个点没有标记,\(tag = 1\)表示先删掉左后删右,\(tag = 2\)表示先删右后删左。
那么如果我们想让数字\(i\)走向\(j\)点的话,我们要分两种情况。
如果\(num_{p_i} \leq num_j\) 那么相当于数字\(i\)向左走。
起点
我们考虑起点\(p_i\)的删边顺序,\(p_i\)要往左走,那么必然是先右后左,不然\(p_i\)上的点就不是\(p_i\)
途经点
我们考虑途经点,如果\(p_i\)想往左走,那么必然是先删掉左边然后删掉右边,不然整条路径就断了。
终点
我们考虑终点删边顺序,如果我们先删左后删右的话,那么终点的点就被右边的点替换掉了,所以也要先右后左。
反之\(num_{p_i} > num_j\)也同理。
然后我们可以对应的打一个\(tag\)上去。
如果想让数字\(i\)走向点\(j\) 那么就要与先前的\(\text{tag}\)不重合。
这样时间复杂度为\(\Theta (N^3)\) 但是可以过\(\text {ccf}\) \(N \leq 2000\)的数据。
\(\Theta (N^3)\)的代码
namespace subtask3 {
int cnt, a[N], ans[N], tag[N], num[N];
bool vis[N];
inline void dfs (int u, int f) {
a[num[u] = ++ cnt] = u;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == f) continue;
dfs (v, u);
}
}
// tag[1] 表示先左后右
// tag[2] 表示先右后左
inline bool check_l (int p1, int p2) { //判断p1->p2的可行性
if (tag[p1] == 1 || tag[p2] == 1) return false;
for (int i = p1 + 1; i < p2; i ++ ) if (tag[i] == 2) return false;
return true;
}
inline void push_l (int p1, int p2) { //打标记
if (p1 != 1 && p1 != n) tag[p1] = 2;
if (p2 != 1 && p2 != n) tag[p2] = 2;
for (int i = p1 + 1; i < p2; i ++ ) tag[i] = 1;
return;
}
inline bool check_r (int p1, int p2) {
if (tag[p1] == 2 || tag[p2] == 2) return false;
for (int i = p2 + 1; i < p1; i ++ ) if (tag[i] == 1) return false;
return true;
}
inline void push_r (int p1, int p2) {
if (p1 != 1 && p1 != n) tag[p1] = 1;
if (p2 != 1 && p2 != n) tag[p2] = 1;
for (int i = p2 + 1; i < p1; i ++ ) tag[i] = 2;
return;
}
void main () {
for (int i = 1; i <= n; i ++ ) tag[i] = vis[i] = 0; cnt = 0;
for (int i = 1; i <= n; i ++ ) if (in[i] == 1) {dfs (i, 0); break;}
//将数字i移动到j点
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ ) {
if (!vis[j] && num[j] != num[p[i]]) {
bool flag = false;
if (num[p[i]] <= num[j]) {
if (check_l (num[p[i]], num[j])) push_l (num[p[i]], num[j]), flag = true;
}
else {
if (check_r (num[p[i]], num[j])) push_r (num[p[i]], num[j]), flag = true;
}
if (flag) {ans[i] = j; vis[j] = 1;break;}
}
}
}
for (int i = 1; i <= n; i ++ ) printf ("%d%c", ans[i], i == n ? '\n' : ' ');
}
}
\(\text {subtask4}\) 树是一条链 \(N \leq 2000\)
那么如何\(\Theta (N ^ 2)\)解决链的问题呢?
对于数字\(i\)我们可以\(\text{dfs}\)遍历整条链找到最小的合法的位置。然后再标记一下。
枚举每一个数字时间复杂度\(\Theta (N)\)
找点和标记的复杂度\(\Theta (N)\)
总复杂度\(\Theta (N^2)\)
namespace subtask4 {
int cnt, a[N], ans[N], tag[N], num[N];
bool vis[N];
inline void dfs (int u, int f) {
a[num[u] = ++ cnt] = u;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == f) continue;
dfs (v, u);
}
}
inline void push_l (int p1, int p2) { //打标记
if (p1 != 1 && p1 != n) tag[p1] = 2;
if (p2 != 1 && p2 != n) tag[p2] = 2;
for (int i = p1 + 1; i < p2; i ++ ) tag[i] = 1;
return;
}
inline void push_r (int p1, int p2) {
if (p1 != 1 && p1 != n) tag[p1] = 1;
if (p2 != 1 && p2 != n) tag[p2] = 1;
for (int i = p2 + 1; i < p1; i ++ ) tag[i] = 2;
return;
}
inline int find_l (int u) {
int res = n + 1;
if (tag[num[u]] == 2) return res;
for (int j = num[u] - 1; j >= 1; j -- ) {
if (tag[j] == 1) {
if (!vis[j]) res = min (res, a[j]);
break;
}
if (tag[j] == 0 && !vis[j]) res = min (res, a[j]);
}
return res;
}
inline int find_r (int u) {
int res = n + 1;
if (tag[num[u]] == 1) return res;
for (int j = num[u] + 1; j <= n; j ++ ) {
if (tag[j] == 2) {
if (!vis[j]) res = min (res, a[j]);
break;
}
if (tag[j] == 0 && !vis[j]) res = min (res, a[j]);
}
return res;
}
void main () {
for (int i = 1; i <= n; i ++ ) tag[i] = vis[i] = 0; cnt = 0;
for (int i = 1; i <= n; i ++ ) if (in[i] == 1) {dfs (i, 0); break;}
for (int i = 1; i <= n; i ++ ) {
int rt = find_l (p[i]), tp;
if (rt < (tp = find_r (p[i]))) push_r (num[p[i]], num[rt]);
else rt = tp, push_l (num[p[i]], num[rt]);
ans[i] = rt;
vis[num[rt]] = 1;
}
for (int i = 1; i <= n; i ++ ) printf ("%d%c", ans[i], i == n ? '\n' : ' ');
}
}
\(\text{subtask5}\) 树形态任意 \(N \leq 2000\)
fuck 为什么细节这么多
我们可以向链\(\Theta (N^2)\)算法一样考虑,对于一个点,连接它的边,删边必然有顺序,而且每个点的删边顺序互不影响。 我们可以维护一个点的删边顺序集,这个顺序集构成一条链的形式。
我们用并查集维护每个点的删边顺序集,当前点的最先删除的边的序号,最后删除点的边的序号,当前点有多少条边未删除。
\(fir_u\)为这个点删边集的最先删除的边。
\(lst_u\)为这个点删边集的最后删除的边。
\(pre_p\)表示这条边是否有前驱。
\(nxt_p\)表示这条边是否有后继。
我们还是考虑如何将数字\(i\)移动到\(j\)点,还是起点,途经点,终点。
起点
我们考虑起点\(p_i\)开始存放的是\(i\),如果我们有删边的话,那么数字\(i\)就不会再原位置,说明我们必须让\(p_i\)走向\(j\)点的边最先删除。
其次我们要考虑,如果当前点最后删边已经确定\(last_u\),如果他们在同一集合,但是还有\(>1\),那么说明有点不在删边序列里面,但是\(last_u\)和起点边在一个集合里面,说明要么有边在起点边前,要么有边在起点边后,否则形成的就不是一条链,但是既然是起点边就不会有比他前的边,终点边也不可能有更后面的边,所以这种情况必定是要排除。
途经点
我们找一个\(p_i\)到\(j\)点要经过的点\(u\)
假设要从\(p_i\)到\(j\)点要经过\(u\)的边\((v,u)\)和\((u,w)\)设这两条边编号为\(p_1\)和\(p_2\)
那么这两条边的编号 一定是连续的,并且先删\(p_1\),再删\(p_2\),不然到了\(u\)点的数字\(i\)就会跑到其他地方去了。
首先如果着两条边已经在同一条关系链里面,因为\(p_1\)和\(p_2\)不可能为正确的前后关系,因为树的性质,两两点路径唯一确定。
如果\(p2\)为删除的起点边,或者\(p_1\)为删除的终点边,那么必然不合法,不然无法保证\(p_1\)在\(p_2\)前面且相邻。
如果\(pre_{p_2}\)已经确定了,或者\(nxt_{p_1}\)已经确定了,因为\(p_1\)和\(p_2\)不在一个偏序链之中,所以必然不合法。
还有一个就是不能让这条链提前闭合,如果我们已经知道了\(fir_u\)和\(las_u\),并且\(fir_u\)和\(p_1\)在同一个集合,\(las_u\)和\(p_2\)在同一个集合,那么除非未被删除的点只有两条,否则将\(p_1\)和\(p_2\)连在一起,无法形成一条完整的链
终点
终点判断的话只需要他是入边\(p\)是最后删除的。
如果他要是终点,首先他不能是起点(显然)。
其次终点\(u\)点的最后删除边\(lst_u\)要么为\(0\)(未定义),要么为\(p\),如果有\(nxt_p\)的话,必然也不能作为终点。
为了防止他提前闭合,如果\(fir_u\)和\(p\)在同一个集合里面,那么为加入点的数量必须是\(1\)(即只有\(p\)没有加入)。
inline int dfs (int u, int f) {//f表示上一条边的编号。
int res = n + 1;
if (f && (!t[u].lst || t[u].lst == f) ) {
if (!t[u].nxt[f] && !(t[u].fir && in[u] > 1 && t[u].same (f, t[u].fir)))
res = u;
}
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to, id = i >> 1;
if (f == id) continue;
if (!f) {
if (!t[u].fir || t[u].fir == id) {
if (t[u].pre[id]) continue;
if (t[u].lst && in[u] > 1 && t[u].same (t[u].lst, id)) continue;
chkmin (res, dfs (v, id));
} else continue;
//起点判断
} else {
//途经点
if (t[u].fir == id || t[u].lst == f || t[u].same (id, f)) continue;
if (t[u].pre[id] || t[u].nxt[f]) continue;
if (t[u].fir && t[u].lst && in[u] > 2 && t[u].same (t[u].fir, f) && t[u].same (t[u].lst, id)) continue;
chkmin (res, dfs (v, id));
}
}
return res;
}
然后我们既然找到了终点\(i\),接下来就是愉快的删除操作了。
inline bool push (int u, int f, int ed) {
if (u == ed) {
t[u].lst = f;
return true;
}
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to, id = i >> 1;
if (id == f) continue;
if (push (v, id, ed)) {
if (!f) {
t[u].fir = id;
} else {
t[u].nxt[f] = t[u].pre[id] = 1; in[u] -- ;
t[u].merge (f, id);
}
return true;
}
}
return false;
}
终于\(AC\)了,激动。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <iostream>
#include <set>
#include <map>
#include <queue>
using namespace std;
typedef long long ll;
const int INF = 2139062143;
template <typename T> void chkmax(T &x, T y) {x = x > y ? x : y;}
template <typename T> void chkmin(T &x, T y) {x = x > y ? y : x;}
template <typename T> void read (T &x) {
x = 0; bool f = 1; char ch;
do {ch = getchar(); if (ch == '-') f = 0;} while (ch > '9' || ch < '0');
do {x = x * 10 + ch - '0'; ch = getchar();} while (ch >= '0' && ch <= '9');
x = f ? x : -x;
}
template <typename T> void write (T x) {
if (x < 0) x = ~x + 1, putchar ('-');
if (x > 9) write (x / 10);
putchar (x % 10 + '0');
}
const int N = 2000 + 50;
const int M = 4000 + 50;
struct EDGE {
int u, to, nxt;
} edge[M];
int T, n, E, Max_In, p[N], x[N], y[N], in[N], head[N];
inline void addedge (int u, int v) {
edge[++E].to = v;
edge[E].nxt = head[u];
head[u] = E;
}
inline void Clear () {
E = 1; Max_In = 0;
for (int i = 0; i <= n; i ++ ) head[i] = in[i] = 0;
}
namespace subtask2 {
int fa[N], ans[N];
bool vis[N];
inline int find (int x) {
return x == fa[x] ? x : fa[x] = find (fa[x]);
}
inline void merge (int x, int y) {
fa[find (x)] = find (y);
}
void main () {
for (int i = 1; i <= n; i ++ ) fa[i] = i, vis[i] = 0;
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ ) {
if (!vis[j] && (i == n || find (j) != find (p[i]))) {
ans[i] = j; vis[j] = 1; merge (j, p[i]);
break;
}
}
}
for (int i = 1; i <= n; i ++ ) printf ("%d%c", ans[i], i == n ? '\n' : ' ');
}
}
namespace subtask4 {
int cnt, a[N], ans[N], tag[N], num[N];
bool vis[N];
inline void dfs (int u, int f) {
a[num[u] = ++ cnt] = u;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == f) continue;
dfs (v, u);
}
}
inline void push_l (int p1, int p2) { //打标记
if (p1 != 1 && p1 != n) tag[p1] = 2;
if (p2 != 1 && p2 != n) tag[p2] = 2;
for (int i = p1 + 1; i < p2; i ++ ) tag[i] = 1;
return;
}
inline void push_r (int p1, int p2) {
if (p1 != 1 && p1 != n) tag[p1] = 1;
if (p2 != 1 && p2 != n) tag[p2] = 1;
for (int i = p2 + 1; i < p1; i ++ ) tag[i] = 2;
return;
}
inline int find_l (int u) {
int res = n + 1;
if (tag[num[u]] == 2) return res;
for (int j = num[u] - 1; j >= 1; j -- ) {
if (tag[j] == 1) {
if (!vis[j]) res = min (res, a[j]);
break;
}
if (tag[j] == 0 && !vis[j]) res = min (res, a[j]);
}
return res;
}
inline int find_r (int u) {
int res = n + 1;
if (tag[num[u]] == 1) return res;
for (int j = num[u] + 1; j <= n; j ++ ) {
if (tag[j] == 2) {
if (!vis[j]) res = min (res, a[j]);
break;
}
if (tag[j] == 0 && !vis[j]) res = min (res, a[j]);
}
return res;
}
void main () {
for (int i = 1; i <= n; i ++ ) tag[i] = vis[i] = 0; cnt = 0;
for (int i = 1; i <= n; i ++ ) if (in[i] == 1) {dfs (i, 0); break;}
for (int i = 1; i <= n; i ++ ) {
int rt = find_l (p[i]), tp;
if (rt < (tp = find_r (p[i]))) push_r (num[p[i]], num[rt]);
else rt = tp, push_l (num[p[i]], num[rt]);
ans[i] = rt;
vis[num[rt]] = 1;
}
for (int i = 1; i <= n; i ++ ) printf ("%d%c", ans[i], i == n ? '\n' : ' ');
}
}
namespace subtask5 {
struct UnionFindSet {
int fa[N], fir, lst;
bool pre[N], nxt[N];
inline void build () {
for (int i = 1; i <= n; i ++ ) pre[i] = nxt[i] = false, fa[i] = i;
fir = lst = 0;
}
inline int find (int x) {
return x == fa[x] ? x : fa[x] = find (fa[x]);
}
inline bool same (int x, int y) {
return find (x) == find (y);
}
inline void merge (int x, int y) {
fa[find (x)] = find (y);
}
} t[N];
inline int dfs (int u, int f) {
int res = n + 1;
if (f && (!t[u].lst || t[u].lst == f) ) {
if (!t[u].nxt[f] && !(t[u].fir && in[u] > 1 && t[u].same (f, t[u].fir)))
res = u;
}
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to, id = i >> 1;
if (f == id) continue;
if (!f) {
if (!t[u].fir || t[u].fir == id) {
if (t[u].pre[id]) continue;
if (t[u].lst && in[u] > 1 && t[u].same (t[u].lst, id)) continue;
chkmin (res, dfs (v, id));
} else continue;
} else {
if (t[u].fir == id || t[u].lst == f || t[u].same (id, f)) continue;
if (t[u].pre[id] || t[u].nxt[f]) continue;
if (t[u].fir && t[u].lst && in[u] > 2 && t[u].same (t[u].fir, f) && t[u].same (t[u].lst, id)) continue;
chkmin (res, dfs (v, id));
}
}
return res;
}
inline bool push (int u, int f, int end) {
if (u == end) {
t[u].lst = f;
return true;
}
for (int i = head[u]; i; i = edge[i].nxt) {
int id = i >> 1, v = edge[i].to;
if (id == f) continue;
if (push (v, id, end)) {
if (!f) t[u].fir = id;
else {
t[u].nxt[f] = t[u].pre[id] = true; in[u] -- ;
t[u].merge (f, id);
}
return true;
}
}
return false;
}
inline void main () {
for (int i = 1; i <= n; i ++ ) t[i].build ();
for (int i = 1; i <= n; i ++ ) {
int ret = dfs (p[i], 0);
push (p[i], 0, ret);
printf ("%d%c", ret, i == n ? '\n' : ' ');
}
}
}
int main () {
read (T);
while (T -- ) {
read (n); Clear ();
for (int i = 1; i <= n; i ++ ) read (p[i]);
for (int i = 1, u, v; i < n; i ++ ) {
read (u); read (v);
x[i] = u; y[i] = v;
addedge (u, v);
addedge (v, u);
in[u] ++ ; in[v] ++ ;
chkmax (Max_In, max (in[u], in[v]));
}
if (Max_In == n - 1) subtask2::main ();
else if (Max_In == 2) subtask4::main ();
else subtask5::main ();
}
return 0;
}