数据结构进阶

并查集

朴素版

const int N = 1e5 + 10;

int p[N];

//返回x的祖宗节点
int find(int x){
    //只有根节点才会有p[x]=x
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

//初始化
void init(){
    //初始每个点都是根节点
    for (int i = 1; i <= n; i ++ )p[i] = i;
}

//合并a,b的两个根节点
void merge(int a, int b){
    p[find(a)] = find(b);
}

维护根节点的子元素个数

const int N = 1e5 + 10;

int p[N], size[N],n = 550;

//返回x的祖宗节点
int find(int x){
    //只有根节点才会有p[x]=x
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

//初始化
void init(){
    //初始每个点都是根节点
    for (int i = 1; i <= n; i ++ ){
        p[i] = i;
        size[i] = 1;
    }
}

//合并a,b的两个根节点
void merge(int a, int b){
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);
}

维护到祖宗节点距离的并查集

const int N = 1e5 + 10;

int p[N], d[N],n = 550;

//返回x的祖宗节点
int find(int x){
    //只有根节点才会有p[x]=x
    if(p[x] != x) {
        int u = find(p[x]);
        //当前节点要加上父节点的距离
        d[x] += d[p[x]];
        p[x] = u;
    }
    return p[x];
}

//初始化
void init(){
    //初始每个点都是根节点
    for (int i = 1; i <= n; i ++ ){
        p[i] = i;
        d[i] = 0;
    }
}

//合并a,b的两个根节点
void merge(int a, int b){
    d[find(a)] = 1; // 根据具体问题,初始化find(a)的偏移量
    p[find(a)] = find(b);
}

树状数组

树状数组基本用途是维护序列的前缀和。对于给定序列a,我们建立一个数组c,其中c[x]保存序列a的区间[x-lowbit(x) + 1, x]中所有数的和,即 ∑ i = x − l o w b i t ( x ) + 1 x a [ i ] \sum_{i=x-lowbit(x)+1}^{x}a[i] i=xlowbit(x)+1xa[i]

在这里插入图片描述

树状数组的前缀和

//返回1-k的前缀和
int ask(int x){
	int ans = 0;
	for(;x > 0; x -= (x & - x)) ans += c[x];
	return ans;
}
//返回区间[l,r]的和
int getSum(int l, int r){
	return ask(r) - ask(l - 1);
}

树状数组的单点增加

//给序列中的一个数a[x]加上y
void add(int x, int y){
	for(; x<= N; x += (x & -x)) c[x] += y;
}

树状数组的初始化

直接建立一个全为0的数组c,然后堆每个位置执行add(x,a[x])。就完成了堆原始序列a构造数组数组的过程。时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

更高效的方法是:从小到大依次考虑每个节点x,借助 l o w b i t lowbit lowbit运算扫描它的子节点并求和。时间复杂度O(N)

线段树

线段树是一种基于分治思想的二叉树结构,用于区间上进行信息统计。

  1. 线段树的每个节点都代表一个区间
  2. 线段树具有唯一的根节点,代表的区间是整个统计范围,如 [ 1 , N ] [1,N] [1,N]
  3. 线段树的每个叶节点都代表一个长度为1的元区间[x,x]
  4. 对于每个内部节点 [ l , r ] [l,r] [l,r],它的左子节点是 [ l , m i d ] [l,mid] [l,mid],右子节点是 [ m i d + 1 , r ] [mid+1,r] [mid+1,r],其中 m i d = ( l + r ) / 2 ( 向 下 取 整 ) mid=(l+r)/2(向下取整) mid=(l+r)/2()

在这里插入图片描述

如何保存线段树

上图展示了一颗线段树。可以发现,出去树的最后一层,整棵线段树一定是一棵完全二叉树,树的深度为 O ( l o g N ) O(logN) O(logN)。因次可用二叉堆类似的"父子2倍的"节点编号方法:

  1. 根节点编号为1
  2. 编号为 x x x的节点的左子节点编号为 x ∗ 2 x*2 x2,右子节点编号为 x ∗ 2 + 1 x*2+1 x2+1
  3. N N N个叶节点的满二叉树有 N + N / 2 + N / 4 + . . . + 2 + 1 = 2 N − 1 N+N/2+N/4+...+2+1=2N-1 N+N/2+N/4+...+2+1=2N1个节点。因为在上述存储方式下,最后一层还产生空余,所以保存线段树的数组长度要不小于 2 N + 2 N = 4 N 2N+2N=4N 2N+2N=4N

线段树的建树

线段树的基本用途是对序列进行维护,支持查询和修改指令。给定一个长度为 N N N的序列A。我们可以在区间[1,N]上建立一颗线段树,每个叶节点[i,i]保存A[i]的值。线段树的二叉树结构可以很方便地从下往上传递信息。以区间问题最大值问题为例,记 d a t ( l , r ) dat(l,r) dat(l,r)等于 m a x l < = i < = r { A [ i ] } max_{l<=i<=r}\{A[i]\} maxl<=i<=r{A[i]},显然 d a t ( l , r ) = m a x ( d a t ( l , m i d ) , d a t ( m i d + 1 , r ) ) dat(l,r)=max(dat(l,mid), dat(mid+1,r)) dat(l,r)=max(dat(l,mid),dat(mid+1,r))

在这里插入图片描述

struct Node{
    int l, r;
    int dat;
} t[N * 4];

void build(int p, int l, int r){
    //节点p代表区间[l,r]
    t[p].l = l, t[p].r = r;
    //找到叶节点,赋值
    if(l == r){
        t[p].dat = a[l]; 
        return;
    }
    //折半
    int mid = l + r >> 1;
    //左子节点,区间[l,mid]
    build(p*2, l, mid);
    //右子节点,区间[mid + 1, r]
    build(p *2 + 1, mid  + 1, r);
    //从子节点返回了
    //利用左右子节点更新当且节点的信息
    t[p].dat = max(t[p*2].dat, t[p*2 + 1].dat);
}
build(1,1,n);//调用入口

线段树的单点修改

在线段树中,根节点(编号为1的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间[x,x]的叶节点,然后从下往上更新[x,x]及其它的祖先节点

// p:线段树的节点,x:数组a中要修改的位置,v:要修改的值
void change(int p, int x, int v){
    //找到叶子节点,更新叶子节点的值
    if(t[p].l == t[p].r){
        t[p].dat = v;
        return;
    }
    int mid = (t[p].l, t[p].r) >> 1;
    //x
    if(x <= mid) change(p*2, x, v);
    else change(p*2  + 1, x, v);
    //从下往上更新信息
    t[p].dat = max(t[p*2].dat, t[p*2+1].dat);
}

在这里插入图片描述

线段树的区间查询

查询序列A在区间 [ l , r ] [l,r] [l,r]上的最大值

  1. [ l , r ] [l,r] [l,r]完全覆盖了当且节点代表的区间,即立即回溯,并且该节点的dat值为候选答案。
  2. 若左子节点与 [ l , r ] [l,r] [l,r]有重叠部分,则递归访问左子节点。
  3. 若右子节点与 [ l , r ] [l,r] [l,r]有重叠部分,则递归访问右子节点。
int ask(int p, int l, int r){
    //当前区间被[l,r]完全包含,立即返回
    if(l <= t[p].l && r>= t[p].r) return t[p].dat;
    int mid = t[p].l + t[p].r >> 1;
    //负无穷大
    int val = -1e9;
    //左子节点有重叠
    if(l <= mid) val = max(val, ask(p*2, l, r));
    //右子节点有重叠
    if(r > mid) val = max(val, ask(p*2+1, l, r));
    return val;
}

在这里插入图片描述

二叉查找树与平衡树初步

BST(Binary Search Tree)

给定一颗二叉树,树上每一个节点都带有一个数值,称为节点的“关键码”(key)。对于树中任意一个节点

  1. 该节点的key大于等于它的左子树任意节点的key
  2. 该节点的key小于等于它的右子树任意节点的key

满足上述性质的二叉树就是二叉查找树,二叉查找树的中序遍历是一个key单调递增的节点序列

BST的初始化

为了避免越界冲突,减少边界情况的特殊判断,在BST中额外插入一个key为正无穷和一个key为负无穷的节点。仅有这两个节点构成的BST就是一颗初始的空BST

const int N = 1e5 + 10;

struct BST{
    int l, r;
    int val;
} a[N];

int tot = 0, root, INF= 1e9;

int newOne(int val){
    a[++tot].val = val;
    return tot;
}

void build(){
    for (int i = 0; i < N; i ++ ){
        a[i].l = 0;
        a[i].r = 0;
    }
    newOne(-INF), newOne(INF);
    //根节点是负无穷,根节点的右子节点是正无穷
    root = 1, a[1].r = 2;
}

BST的检索

在BST中检索是否存在key为val的节点
变量p为根节点root,执行以下过程

  1. 若p.key == val,找到并返回
  2. 若p.key > val,若左子节点为空,说明不存在该val.;否则在p的左子树递归进行检索
  3. 若p.key<val,若右子节点为空,说明不存在该val.;否则在p的右子树递归进行检索
    在这里插入图片描述
//若存在返回>0得到数
int get(int p, int val){
    //检索失败
    if(p == 0) return 0;
    if(val == a[p].val) return p;
    return val < a[p].val ? get(a[p].l, val) : get(a[p].r, val);
}

BST的插入

与BST的检索过程类似,在发现p的子节点为空,说明val不存在,直接建立key为val的新节点作为p的子节点
在这里插入图片描述

void insert(int &p, int val){
    if(p == 0){
        //找到为空的节点。创建并赋值
        p = newOne(val);
        return;
    }
    if(val == a[p].val) return;
    if(val < a[p].val) insert(a[p].l, val);
    else insert(a[p].r, val);
}

BST求前驱/后继

前驱:所有小于指定值中最大的一个
后继:所有大于指定值中最小的一个

求后继

  1. 初始化ans为key为正无穷的节点编号。然后,在BST中检索val,每经过一个点,都检查该节点的key,判断能否更新所求的ans
  2. 检索完成有三种结果
  3. 没有找到val,此时val的后继就已经在经过的节点中,ans即为所求,
  4. 找到了key为val的节点p,但p没有右子树,ans即为所求
  5. 找到了key为val的节点p,p有右子树。从p的右子节点出发,一直向左走,就找到了val的后继
int getNext(int val){
    int ans = 2;
    int p = root;
    while (p > 0){
        if(val == a[p].val){
            if(a[p].r > 0){
                //走到右子树
                p = a[p].r;
                //从右子树一直往左走
                while (a[p].l > 0) p = a[p].l;
                ans = p;
            }
            break;
        }
        //每经过一个节点,都尝试更新后继
        //如果前节点的val大于参数的val并且当前节点的val小于当前答案的val,更新答案
        if(a[p].val > val && a[p].val < a[ans].val) ans = p;
        //若当前节点的val大于参数的val,向左子树走,否则向右子树走
        p = val < a[p].val ? a[p].l : a[p].r;
    }
    return ans;
}

求前驱

int getPrev(int val){
    //负无穷小的节点
    int ans = 1;
    int p = root;
    while (p > 0){
        if(val == a[p].val){
            //有左子树
            if(a[p].l > 0){
                //走到左子树
                p = a[p].l;
                //从左子树一直往右走
                while (a[p].r > 0) p = a[p].r;
                ans = p;
            }
            break;
        }
        //每经过一个节点,都尝试更新前驱
        //如果前节点的val小于参数的val并且当前节点的val大于当前答案的val,更新答案
        if(a[p].val < val && a[p].val > a[ans].val) ans = p;
        //若当前节点的val小于参数的val,向右子树走,否则向左子树走
        p = val > a[p].val ? a[p].r : a[p].l;
    }
    return ans;
}

BST的节点删除

1. 在BST中检索val,得到节点p
2. 若p的子节点个数小于2,直接删除p,并令p的子节点代替p的位置,与p的父节点相连
3. 若p既有左子树又有右子树,则在BST中求出val的后继节点next。因为next没有左子树,所以可以直接删除next,并令next的右子树代替next的位置,最后,再让next节点代替p节点,删除p即可

//从子树p中删除值为val的节点
//p:子树的节点,val:值
//注意p是引用
void rm(int &p, int val){
    //检索边界
    if(p == 0) return;
    //检索边界
    if(val == a[p].val){
        //检索到指定值val
        if(a[p].l == 0){
            //左子树为空,让右子树代替当前节点
            p = a[p].r;
        }else if(a[p].r == 0){
            //右子树为空,让左子树代替当前节点
            p = a[p].l;
        }else{
            //寻找后继节点,从右子树一直往左走
            int next = a[p].r;
            while(a[next].l > 0) next = a[next].l;
            //val的后继节点一定没有左子树
            //从当前节点的右子树删除后继节点
            rm(a[p].r, a[next].val);
            //令后继节点代替节点p的位置
            a[next].l = a[p].l, a[next].r = a[p].r;
            p = next;
        }
        return;
    }
    //检索过程
    if(val < a[p].val){
        rm(a[p].l, val);
    }else{
        rm(a[p].r, val); 
    }
}

Treap

BST很容易退化,插入从小到大序列会让BST操作复杂度退化成O(n)。Treap是入门的平衡树,通过旋转改变二叉树的形态,且保持BST的性质

改变形态并保持BST性质的方法是旋转,最基本的旋转操作称为单旋转,单旋转又分为左旋和右旋。
在这里插入图片描述

以右旋为例子。在初始情况下,x是y的左子节点,A和B分别为是x的左右子树,C是y的右子树。右旋操作在保持BST的性质的基础上,把x变为y’的父节点。因为x的key小于y的key,所以应该作为x的右子节点。当x变成y的父节点后,y的左子树就空了出来,于是x原理的右子树B就恰好做为y的左子树。

zig (p)可以理解为把p的左子节点绕着p向右旋转

void zig(int p){
    int q = a[p].l;
    a[p].l = a[q].r, a[q].r = p;
    p = q;
}

zag(p)可以理解为把p的右子节点绕着p向左旋转

void zag(int &p){
    int q = a[p].r;
    a[p].r = a[q].l, a[q].l = p;
    p = q;
}

acwing253

//副本数cnt解决重复关键值的问题,增加时cnt+1,减少时cnt-1,为0时删除
//size统计每个根的副本数,解决排名问题
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;
struct Treap{
    int l, r;
    int val, dat; //key,权值
    int cnt, size; //副本数,子树大小
} a[N];

int tot, root, n, INF = 0x7fffffff;

int newOne(int val){
    //把值存起来
    a[++tot].val = val;
    //随机权值
    a[tot].dat = rand();
    //副本数为1,以该节点为根的总副本数为1(没有左右子节点)
    a[tot].cnt = a[tot].size = 1;
    return tot;
}

//更新size
void update(int p){
    //size = 左子节点的size + 右子节点的size + 当前节点的副本数
    a[p].size = a[a[p].l].size + a[a[p].r].size + a[p].cnt;
}

//构建Treap
void build(){
    newOne(-INF),newOne(INF);
    root = 1, a[1].r = 2;
    update(root);
}

//根据值获取排名
int getRankByVal(int p, int val){
    if(p == 0) return 0;
    //相等就返回左子树的size, +1代表p自己
    if(val == a[p].val) return a[a[p].l].size + 1;
    //val小于当前节点的值, 统计左子的排名
    if(val < a[p].val) return getRankByVal(a[p].l, val);
    //右子节点的排名 + 左子节点的排名 + 自己的副本数
    return getRankByVal(a[p].r, val) + a[a[p].l].size + a[p].cnt;
}

//根据排名获取值
int getValByRank(int p, int rank){
    if(p == 0) return INF;
    if(a[a[p].l].size >= rank) return getValByRank(a[p].l, rank);
    if(a[a[p].l].size + a[p].cnt >= rank) return a[p].val;
    return getValByRank(a[p].r, rank - a[a[p].l].size - a[p].cnt);
}

void zag(int &p){
    int q = a[p].r;
    a[p].r = a[q].l, a[q].l = p, p = q;
    update(a[p].l), update(p);
}

void zig(int &p){
    int q = a[p].l;
    a[p].l = a[q].r, a[q].r = p, p = q;
    update(a[p].r), update(p);
}

void insert(int &p, int val){
    if(p == 0){
        p = newOne(val);
        return;
    }
    if(val == a[p].val){
        a[p].cnt++,update(p);
        return;
    }
    if(val < a[p].val){
        insert(a[p].l, val);
        if(a[p].dat < a[a[p].l].dat) zig(p);
    }else{
        insert(a[p].r, val);
        if(a[p].dat < a[a[p].r].dat) zag(p);
    }
    update(p);
}

int getPrev(int val){
    int ans = 1;
    int p = root;
    while(p > 0){
        if(val == a[p].val){
            if(a[p].l > 0){
                p = a[p].l;
                while(a[p].r > 0) p = a[p].r;
                ans = p;
            }
            break;
        }
        if(a[p].val < val && a[p].val > a[ans].val) ans = p;
        p = val < a[p].val ? a[p].l: a[p].r;
    }
    return a[ans].val;
    
}

int getNext(int val){
    int ans = 2;
    int p = root;
    while(p > 0){
        if(val == a[p].val){
            if(a[p].r > 0){
                p = a[p].r;
                while(a[p].l > 0) p = a[p].l;
                ans = p;
            }
            break;
        }
        if(a[p].val > val && a[p].val < a[ans].val) ans = p;
        p = val < a[p].val ? a[p].l: a[p].r;
    }
    return a[ans].val;
}

void rm(int &p, int val){
    if(p == 0) return;
    if(val == a[p].val){
        if(a[p].cnt > 1){
            a[p].cnt --, update(p);
            return;
        }
        if(a[p].l || a[p].r){
            if(a[p].r == 0 || a[a[p].l].dat > a[a[p].r].dat) {
                zig(p),rm(a[p].r, val);
            }else{
                zag(p), rm(a[p].l, val);
            }
            update(p);
        }else p = 0;
        return;
    }
    val < a[p].val ? rm(a[p].l, val) : rm(a[p].r, val);
    update(p);
}

int main()
{
    build();
    cin >> n;
    while (n -- ){
        int opt, x;
        scanf("%d%d", &opt, &x);
        switch(opt){
            case 1:
                insert(root, x);
                break;
            case 2:
                rm(root, x);
                break;
            case 3:
                printf("%d\n", getRankByVal(root, x) - 1);
                break;
            case 4:
                printf("%d\n", getValByRank(root, x + 1));
                break;
            case 5:
                printf("%d\n", getPrev(x));
                break;
            case 6:
                printf("%d\n", getNext(x));
                break;
        }
    }
    
}

可持久化数据结构

可持久化Trie

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值