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;
}
posted @ 2020-02-29 09:52  Hock  阅读(398)  评论(0编辑  收藏  举报