CSP-S 2019 树上的数
树上的数
题面
现在有一棵树,每个点上有一个点权,你切断一条边,就会交换边上两个点的点权,求点权的最小字典序。
考场上暴力标配,枚举所有删边顺序,然后取字典序最小的一组。
存在度数为的节点
存在度数为的节点,那么这个图一定是菊花图。
我们设菊花图入度为的点为。
我们每次删边一段必定是,假设我们删边顺序是,表示第次删的点编号。
第次删的边就是。
每次删边,当前的点权就确定了,这个性质比较好。
经过几遍模拟,我们发现到了,到了,到了。
这样我们是不是形成了一个环的形式。
要想让字典序最小,我们贪心的选择能得到的最小的数字,并且这个环要不能是多个小环。
我们如果想让数字到点的话,那么首先这个点没有用过,并且和不在同一个环里(不然形成不了一个大环)。
时间复杂度
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' : ' ');
}
}
树是一条链
如果想出链的部分分,正解也就不远了。
首先按照菊花图的思路,我们要让数字走向点。
我们规定三类点(正解也要用到)
1.起点 数字所在的点。
2.途经点:从起点到终点路过的那些点。
3.终点:点的位置。
我们先把链处理一下,记录链上第个点是,点是第个。
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);
}
}
首先对于一个点,删边顺序有两种,先删掉左后删右,先删右后删左。
我们用一个数组表示一下这个点删边顺序。表示这个点没有标记,表示先删掉左后删右,表示先删右后删左。
那么如果我们想让数字走向点的话,我们要分两种情况。
如果 那么相当于数字向左走。
起点
我们考虑起点的删边顺序,要往左走,那么必然是先右后左,不然上的点就不是
途经点
我们考虑途经点,如果想往左走,那么必然是先删掉左边然后删掉右边,不然整条路径就断了。
终点
我们考虑终点删边顺序,如果我们先删左后删右的话,那么终点的点就被右边的点替换掉了,所以也要先右后左。
反之也同理。
然后我们可以对应的打一个上去。
如果想让数字走向点 那么就要与先前的不重合。
这样时间复杂度为 但是可以过 的数据。
的代码
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' : ' ');
}
}
树是一条链
那么如何解决链的问题呢?
对于数字我们可以遍历整条链找到最小的合法的位置。然后再标记一下。
枚举每一个数字时间复杂度
找点和标记的复杂度
总复杂度
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' : ' ');
}
}
树形态任意
fuck 为什么细节这么多
我们可以向链算法一样考虑,对于一个点,连接它的边,删边必然有顺序,而且每个点的删边顺序互不影响。 我们可以维护一个点的删边顺序集,这个顺序集构成一条链的形式。
我们用并查集维护每个点的删边顺序集,当前点的最先删除的边的序号,最后删除点的边的序号,当前点有多少条边未删除。
为这个点删边集的最先删除的边。
为这个点删边集的最后删除的边。
表示这条边是否有前驱。
表示这条边是否有后继。
我们还是考虑如何将数字移动到点,还是起点,途经点,终点。
起点
我们考虑起点开始存放的是,如果我们有删边的话,那么数字就不会再原位置,说明我们必须让走向点的边最先删除。
其次我们要考虑,如果当前点最后删边已经确定,如果他们在同一集合,但是还有,那么说明有点不在删边序列里面,但是和起点边在一个集合里面,说明要么有边在起点边前,要么有边在起点边后,否则形成的就不是一条链,但是既然是起点边就不会有比他前的边,终点边也不可能有更后面的边,所以这种情况必定是要排除。
途经点
我们找一个到点要经过的点
假设要从到点要经过的边和设这两条边编号为和
那么这两条边的编号 一定是连续的,并且先删,再删,不然到了点的数字就会跑到其他地方去了。
首先如果着两条边已经在同一条关系链里面,因为和不可能为正确的前后关系,因为树的性质,两两点路径唯一确定。
如果为删除的起点边,或者为删除的终点边,那么必然不合法,不然无法保证在前面且相邻。
如果已经确定了,或者已经确定了,因为和不在一个偏序链之中,所以必然不合法。
还有一个就是不能让这条链提前闭合,如果我们已经知道了和,并且和在同一个集合,和在同一个集合,那么除非未被删除的点只有两条,否则将和连在一起,无法形成一条完整的链
终点
终点判断的话只需要他是入边是最后删除的。
如果他要是终点,首先他不能是起点(显然)。
其次终点点的最后删除边要么为(未定义),要么为,如果有的话,必然也不能作为终点。
为了防止他提前闭合,如果和在同一个集合里面,那么为加入点的数量必须是(即只有没有加入)。
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;
}
然后我们既然找到了终点,接下来就是愉快的删除操作了。
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;
}
终于了,激动。
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)