CSP-S 2019 树上的数

树上的数

题面
现在有一棵树,每个点上有一个点权,你切断一条边,就会交换边上两个点的点权,求1N点权的最小字典序。

subtask1 N10

考场上暴力标配,Θ(N!)枚举所有删边顺序,然后取字典序最小的一组。

subtask2 存在度数为N1的节点

存在度数为N1的节点,那么这个图一定是菊花图。
我们设菊花图入度为N1的点为rt
我们每次删边一段必定是rt,假设我们删边顺序是ppi表示第i次删的点编号。
i次删的边就是rt>pi
每次删边,当前pi的点权就确定了,这个性质比较好。
经过几遍模拟,我们发现ap1到了p2ap2到了p3apn1到了rt
这样我们是不是形成了一个环的形式。
rt>p1>p2>p3...>pn1>rt
要想让字典序最小,我们贪心的选择能得到的最小的数字,并且这个环要不能是多个小环。
我们如果想让数字ij点的话,那么首先j这个点没有用过,并且jpi不在同一个环里(不然形成不了一个大环)。
时间复杂度Θ(N2)

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' : ' ');
    }
}

subtask3 树是一条链 N160

如果想出链的部分分,正解也就不远了。
首先按照菊花图的思路,我们要让数字i走向j点。
我们规定三类点(正解也要用到)
1.起点 数字i所在的点。
2.途经点:从起点到终点路过的那些点。
3.终点:j点的位置。
我们先把链dfs处理一下,记录链上第i个点是aiu点是第numu个。

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点的话,我们要分两种情况。
如果numpinumj 那么相当于数字i向左走。

起点

我们考虑起点pi的删边顺序,pi要往左走,那么必然是先右后左,不然pi上的点就不是pi

途经点

我们考虑途经点,如果pi想往左走,那么必然是先删掉左边然后删掉右边,不然整条路径就断了。

终点

我们考虑终点删边顺序,如果我们先删左后删右的话,那么终点的点就被右边的点替换掉了,所以也要先右后左。

反之numpi>numj也同理。

然后我们可以对应的打一个tag上去。
如果想让数字i走向点j 那么就要与先前的tag不重合。

这样时间复杂度为Θ(N3) 但是可以过ccf N2000的数据。

Θ(N3)的代码

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' : ' ');
    }
}

subtask4 树是一条链 N2000

那么如何Θ(N2)解决链的问题呢?

对于数字i我们可以dfs遍历整条链找到最小的合法的位置。然后再标记一下。
枚举每一个数字时间复杂度Θ(N)
找点和标记的复杂度Θ(N)
总复杂度Θ(N2)

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' : ' ');
    }
}

subtask5 树形态任意 N2000

fuck 为什么细节这么多
我们可以向链Θ(N2)算法一样考虑,对于一个点,连接它的边,删边必然有顺序,而且每个点的删边顺序互不影响。 我们可以维护一个点的删边顺序集,这个顺序集构成一条链的形式。

我们用并查集维护每个点的删边顺序集,当前点的最先删除的边的序号,最后删除点的边的序号,当前点有多少条边未删除。

firu为这个点删边集的最先删除的边。
lstu为这个点删边集的最后删除的边。
prep表示这条边是否有前驱。
nxtp表示这条边是否有后继。

我们还是考虑如何将数字i移动到j点,还是起点,途经点,终点。

起点

我们考虑起点pi开始存放的是i,如果我们有删边的话,那么数字i就不会再原位置,说明我们必须让pi走向j点的边最先删除。

其次我们要考虑,如果当前点最后删边已经确定lastu,如果他们在同一集合,但是还有>1,那么说明有点不在删边序列里面,但是lastu和起点边在一个集合里面,说明要么有边在起点边前,要么有边在起点边后,否则形成的就不是一条链,但是既然是起点边就不会有比他前的边,终点边也不可能有更后面的边,所以这种情况必定是要排除。

途经点

我们找一个pij点要经过的点u
假设要从pij点要经过u的边(v,u)(u,w)设这两条边编号为p1p2
那么这两条边的编号 一定是连续的,并且先删p1,再删p2,不然到了u点的数字i就会跑到其他地方去了。

首先如果着两条边已经在同一条关系链里面,因为p1p2不可能为正确的前后关系,因为树的性质,两两点路径唯一确定。

如果p2为删除的起点边,或者p1为删除的终点边,那么必然不合法,不然无法保证p1p2前面且相邻。

如果prep2已经确定了,或者nxtp1已经确定了,因为p1p2不在一个偏序链之中,所以必然不合法。

还有一个就是不能让这条链提前闭合,如果我们已经知道了firulasu,并且firup1在同一个集合,lasup2在同一个集合,那么除非未被删除的点只有两条,否则将p1p2连在一起,无法形成一条完整的链

终点

终点判断的话只需要他是入边p是最后删除的。

如果他要是终点,首先他不能是起点(显然)。

其次终点u点的最后删除边lstu要么为0(未定义),要么为p,如果有nxtp的话,必然也不能作为终点。

为了防止他提前闭合,如果firup在同一个集合里面,那么为加入点的数量必须是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;
}
posted @   Hock  阅读(400)  评论(0编辑  收藏  举报
编辑推荐:
· 从 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)
点击右上角即可分享
微信分享提示