【ACWing】2437. Splay

题目地址:

https://www.acwing.com/problem/content/2439/

给定一个长度为 n n n的整数序列,初始时序列为 1 , 2 , … , n − 1 , n {1,2,…,n−1,n} 1,2,,n1,n。序列中的位置从左到右依次标号为 1 ∼ n 1∼n 1n。我们用 [ l , r ] [l,r] [l,r]来表示从位置 l l l到位置 r r r之间(包括两端点)的所有数字构成的子序列。现在要对该序列进行 m m m次操作,每次操作选定一个子序列 [ l , r ] [l,r] [l,r],并将该子序列中的所有数字进行翻转。例如,对于现有序列1 3 2 4 6 5 7,如果某次操作选定翻转子序列为 [ 3 , 6 ] [3,6] [3,6],那么经过这次操作后序列变为1 3 5 6 4 2 7。请你求出经过 m m m次操作后的序列。

输入格式:
第一行包含两个整数 n , m n,m n,m。接下来 m m m行,每行包含两个整数 l , r l,r l,r,用来描述一次操作。

输出格式:
共一行,输出经过 m m m次操作后的序列。

数据范围:
1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105
1 ≤ l ≤ r ≤ n 1≤l≤r≤n 1lrn

fhq treap的写法参考https://blog.csdn.net/qq_46105170/article/details/121119431。下面介绍splay tree(又叫做”伸展树“)的写法。

要翻转 [ l : r ] [l:r] [l:r]这段区间,则需要一个数据结构,可以将这个区间的数隔离出来,然后进行翻转。执行翻转的时候,只需要每个节点维护一个懒标记即可做到 O ( log ⁡ n ) O(\log n) O(logn)的时间,对于怎么隔离出那个区间,fhq treap可以通过分裂的方式,而splay tree则是通过伸展的方式。

首先关于左旋右旋,可以参考https://blog.csdn.net/qq_46105170/article/details/118997891。而伸展树平衡的方式是,每次查询或添加数据的时候,都将该数据的节点通过旋转转到树根的位置。可以证明如果每次都进行这样的操作,可以使得每次操作期望时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)的。而splay操作可以将指定的节点旋转到树根或者某个节点 p p p的儿子处,这里的 p p p一般指的就是树根(当然 p p p必须是指定的节点的某个祖宗,否则是没法转过去的)。例如,我们想把某个节点 x x x转到树根处。如果 x x x就是树根,那就不用转了;否则,如果 x x x的父亲是树根,则直接做一次单旋即可;否则 x x x的父亲 y y y y y y的父亲 z z z都非空。这里需要将 x x x向上旋两次,要按两种情况讨论:如果 x x x y y y的左儿子且 y y y也是 z z z的左儿子(或者 x x x y y y的右儿子且 y y y也是 z z z的右儿子),那么就先将 y y y旋上去,然后再将 x x x旋上去;如果 x x x y y y的左儿子且 y y y z z z的右儿子(或者 x x x y y y的右儿子且 y y y z z z的左儿子),那么直接将 x x x向上旋转两次。即 x , y , z x,y,z x,y,z如果是直线,则先转父亲再转自己;否则转两次自己。如图所示:
在这里插入图片描述
在这里插入图片描述
注意,只有这样的旋转才能证明每次操作期望时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)

而对于伸展操作,只需要每次都将当前节点向上按上面的方式转上去,转到指定位置停止即可。

设原区间是 A A A,现在考虑实现插入和翻转 A [ l : r ] A[l:r] A[l:r]操作:
1、插入就直接像普通BST那样插入,然后对插入的新节点做splay操作即可;
2、翻转 A [ l : r ] A[l:r] A[l:r],先找到 A [ l − 1 ] A[l-1] A[l1]这个节点(当然这个节点可能不存在,所以我们需要加入两个哨兵节点,分别记为 0 0 0 n + 1 n+1 n+1号节点)splay到树根,然后将 A [ r + 1 ] A[r+1] A[r+1]这个节点splay到树根下面,那么显然 A [ r + 1 ] A[r+1] A[r+1]的左子树就是 A [ l : r ] A[l:r] A[l:r],然后对其翻转即可,即直接打上懒标记。注意在查 A [ l − 1 ] A[l-1] A[l1] A [ r + 1 ] A[r+1] A[r+1]的时候,每次向下之前都需要做pushdown操作。pushdown的做法是,交换左右子树,清空自己的懒标记,然后下传懒标记(即左右儿子打上懒标记,如果原来有懒标记则清除)。

代码如下:

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int n, m;
struct Node {
    
    
    int s[2], p, v;
    int size;
    // 这里的reverse就是懒标记
    bool reverse;
    void init(int _v, int _p) {
    
    
        v = _v, p = _p;
        size = 1;
    }
} tr[N];
int root, idx;

void pushup(int x) {
    
    
    tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
}

// 下传懒标记
void pushdown(int x) {
    
    
	// 如果自己有懒标记,则交换左右子树,下传懒标记,最后清空自己的懒标记
    if (tr[x].reverse) {
    
    
        swap(tr[x].s[0], tr[x].s[1]);
        tr[tr[x].s[0]].reverse ^= 1;
        tr[tr[x].s[1]].reverse ^= 1;
        tr[x].reverse = 0;
    }
}

// 将x向上转一次
void rotate(int x) {
    
    
    int y = tr[x].p, z = tr[y].p;
    int k = tr[y].s[1] == x;
    tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
    tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
    tr[x].s[k ^ 1] = y, tr[y].p = x;
    pushup(y), pushup(x);
}

// 将x转到g的下面。如果g = 0,则转到树根。一般这里的g要么是0要么是树根
void splay(int x, int g) {
    
    
    while (tr[x].p != g) {
    
    
        int y = tr[x].p, z = tr[y].p;
        if (z != g) 
        	// 如果三者不构成直线,则转父亲;否则转自己
            if ((tr[y].s[0] == x) ^ (tr[z].s[0] == y)) rotate(x);
            else rotate(y);
        rotate(x);
    }

    if (!g) root = x;
}

void insert(int v) {
    
    
    int u = root, p = 0;
    while (u) p = u, u = tr[u].s[v > tr[u].v];
    u = ++idx;
    // 如果p不空,则插在p下面;如果p空,则说明树空,则直接new出节点v
    if (p) tr[p].s[v > tr[p].v] = u;
    tr[u].init(v, p);
    splay(u, 0);
}

// 查树中中序遍历第k个数
int get_k(int k) {
    
    
    int u = root;
    while (u) {
    
    
    	// 下去查之前要先pushdown
        pushdown(u);
        if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
        else if (tr[tr[u].s[0]].size + 1 < k) {
    
    
            k -= tr[tr[u].s[0]].size + 1;
            u = tr[u].s[1];
        } else return u;
    }

    return -1;
}

void output(int u) {
    
    
    if (!u) return;

	// 走到下一层之前要pushdown
    pushdown(u);
    output(tr[u].s[0]);
    if (tr[u].v >= 1 && tr[u].v <= n)
        printf("%d ", tr[u].v);
    output(tr[u].s[1]);
}

int main() {
    
    
    scanf("%d%d", &n, &m);
    for (int i = 0; i <= n + 1; i++)
        insert(i);

    while (m--) {
    
    
        int l, r;
        scanf("%d%d", &l, &r);
        // 要翻转原数组的[l : r],由于有哨兵节点,所以实际上是在
        // 翻转[l + 1: r + 1],即要找第l和第r + 2个节点
        l = get_k(l), r = get_k(r + 2);
        // 将l转到树根,将r转到树根下面,则r的左子树即为要翻转的区间
        splay(l, 0), splay(r, l);
        // 翻转只需要打上懒标记即可
        tr[tr[r].s[0]].reverse ^= 1;
    }

    output(root);

    return 0;
}

每次操作时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)(splay两次加上一次打懒标记),空间 O ( n ) O(n) O(n)

おすすめ

転載: blog.csdn.net/qq_46105170/article/details/121119399