算法进阶--线段树(1)

考录到线段树的篇幅过多,写五六十页的观感不佳,我把它分为多个章节,大家可以关注我看后续的更新。每章节我会把涉及的内容放在前面,给大家进行选择和查找。(章节我会用红体来表示,内容我会用蓝体表示)。

 本章会介绍:一般线段树(主要是模板),主席树(可持久化的线段树),二逼平衡树(树套树,线段树套平衡树)。

标题一:线段树+懒标记

先了解什么是线段树:

定义:是一种基于分治思想的二叉树,用来维护区间信息(区间和,区间最值,区间GCD等),在logn的时间内执行区间修改和区间查询。

构造原理:每个叶子节点储存元素本身,非叶子节点储存元素的统计值。

图文讲解:(这里用董老师的图)

这里8号和9号点,8号区间是[1,2]的区间和是7,9号点就是叶子节点。

所以我们代码实现的时候显然要用结构体。

代码格式:

#define first x
#define second y
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pii;
typedef pair<double, int> pdi;
const int N = 1e5 + 10, mod = 998244353, inf = 0x3f3f3f3f;

#define lc p<<1
#define rc p<<1|1
//递归建树
//父节点i,左儿子2*i,右二子2*i + 1
int w[N],n;
struct node{
    int l;
    int r;
    int sum;
}tr[N*4];

void build(int p,int l,int r){
    tr[p] = {l,r,w[l]};
    if(l == r) return ;//叶子返回 
    int m = l + r >> 1;
    build(lc,l,m);
    build(rc,m+1,r);
    tr[p].sum = tr[lc].sum + tr[rc].sum;

}
void work(){

}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);

    int _;
    _ = 1;
    while (_--)
    {
        work();
    }
    return 0;
}

代码运行轨迹:

 点修改的方式(单点修改)

如果想修改7的这个位置的步骤;

第一步:用递归找到叶子节点

第二步:向上更新祖先节点

代码实现:

void updata(int p,int x,int y,int k){
    if(tr[p].l == x&&tr[p].r == x){//查询叶子节点
       tr[p].sum += k;
       return ;
    }
    int m = tr[p].l + tr[p].r >> 1;
    //裂开查找
    if(x <= m) updata(lc,x,y,k);
    if(x > m) updata(rc,x,y,k);
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}

图像显示:

可见时间复杂度是O(logn);

 区间查询:(方法:拆分和拼凑)

举例:我们想查询区间[4,9];看图(下面的):

 最后合并答案即可;

步骤;

第一步:从根节点进入,递归下面的步骤

步骤1:如果区间[x,y],完全在我们要找的区间内,回溯即可,并返回sum的值

步骤2;若左儿子节点和[x,y]有交点且互不包含,那么就递归访问左子树

步骤3: 若右儿子节点和[x,y]有交点且互不包含,那么就递归访问右子树

代码实现:(图像的步骤:红色-> 紫色 -> 绿色 -> 蓝色 -> 黑色)

int query(int p,int x,int y){
    if(x <= tr[p].l&&tr[p].r <= y){
        return tr[p].sum;
    }
    int m = tr[p].l + tr[p].r >> 1;
    int sum = 0;
    if(x <= m) sum += query(lc,x,y);
    if(y > m) sum += query(rc,x,y);
    return sum;
}

时间复杂度还是O(logn)

 区间修改(区间修改):

对于区间[x,y],中每个数抖加上k,那么我们要修改覆盖的每个叶子节点,时间复杂度就是O(n);

优化方法(懒惰修改):当[x,y]完全覆盖节点区间[a,b]时,先修改该区间的sum值,再打上一个“懒标记”,然后返回。到下次需要时再向下传递“懒标记”。

绿色是优化和没有优化的区别:灰色是懒标记

 代码实现:

struct node{
    int l;
    int r;
    int sum;
    int add;
}tr[N*4];

void pushup(int p){
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}

void pushdown(int p){
    if(tr[p].add){
        tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
        tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
        tr[lc].add += tr[p].add;
        tr[rc].add += tr[p].add;
        tr[p].add = 0;
    }
}

void uodata(int p,int x,int y,int k){
    if(x <= tr[p].l&&tr[p].r <= y){
        tr[p].sum += (tr[p].r - tr[p].l + 1)*k;//区间宽度*k
        tr[p].add += k; // 标记这个点对叶子节点的亏欠
        return ;
    }
    int m = tr[p].l + tr[p].r >> 1;
    pushdown(p);
    if(x <= m) updata(lc,x,y,k);
    if(y > m) updata(rc,x,y,k);
    pushup(p); 
}

图文讲解

知识点巩固:

【模板】树状数组 1 - 洛谷

【模板】线段树 1 - 洛谷

(明天晚上我会更新线段树和树状数组的区别)

标题二:主席树

好,这里也是园上了算法提升--二叉堆与树状数组-CSDN博客 这个章节最后的主席树了,splay我后面精进一些就会更新。

主席树也被成为可持久化的线段树
介绍前:我们先搞清每个线段树的特征

                                  普通线段树             权值线段树
节点区间序列的下标区间序列的值域
节点维护的信息区间最值,区间和

值域内树的出现次数

     主席树(可持久线段树)的特征:支持回退,访问之前版本的线段树。

      操作方法:每次只更改logn + 1个节点,对于没有改变的就用借用继承的方法,这里就有点像,洛谷里用二进制,满足异或的方法来走最大的数,我们都知道这可以用树状数组来写。

这里我们也应用一个名词 -- 动态开点。对于每个节点保存左右儿子的编号,对于每个历史版本要保留根节点编号。

我这里用董老师的图来说明:

大家看 ,t = log(n) + 1  = 3 ,改变3个节点 。我们枚举3次。

然后我们看到白色区间是没有动过的,我们就可以将这个图合并。

我说明一下别人都没有主要到的东西,这里我们不能用堆式存储法了(就是上面用的父节点i,左儿子2*i,右二子2*i + 1),为什么呢:

我们看上面的图:[1,4] 这个区间改变了,用堆式存储法,我们就不能做到访问之前的版本。

好,我们言归正传动态开点怎么做

初始的我们建立一个2*n - 1个空间,然后我们就可以插入,每次最多增加logn + 1个节点。

sum = 2*n - 1 + n*(logn + 1) ;

代码实现:

#define lc(x) tr[x].ch[0]
#define rc(x) tr[x].ch[1]

int n,m,a[N];
vector<int> v;
struct node{
    int ch[2];
    int s;
}tr[N];
int root[N],idx;

void build(int &x,int l,int r){
    x = ++idx;
    if(l == r) return ;
    int m = l + r >> 1;
    build(lc(x),l,m); 
    build(rc(x),m + 1,r);

}

这里大家可能会想到,你修改了n次,你的空间不就扩大了n倍了吗。是的,所以我们会用离散化来解决,这个最后我来说明。

构建主席树:

方法:递归建立每个历史版本的线段树

怎么做呢:设两个指针;x,y. x是前一个版本的节点指针,y是当前版本的节点指针,通过子空间y值,给父空间的lc(y)或者rc(y)赋值。

代码实现:

void insert(int x,int &y,int l,int r,int v){
    y = ++idx;tr[y] = tr[x];tr[y].s++;
    if(l == r) return ;
    int m = l + r >> 1;
    if(v <= m) insert(lc(x),lc(y),l,m,v);
    else insert(rc(x),rc(y),m + 1,r,v);
}

图文讲解:

 这里我给大家解释一下节点左右儿子为什么会改变。

大家看图:

看到了每次在insert后y都会++,然后&引用符号会统一上下的y值这样就实现了对历史的维护,对现在的改变

查询:

 就是在主席树上找区间[l,r]的第k小 (第k个小,我会发布一篇堆的方法)

简化:先找[1,r]区间的第k小,然后找到查入r时的历史版本,在权值线段树上用二分查找。然后用前缀和的方法;用[1,r] - [l,l  - 1]的第k小,就是答案了。

代码实现:

int query(int x,int y,int l,int r,int k){
    if(l == r) return l;
    int m = l + r >> 1;
    int s = tr[lc(y)].s - tr[lc(x)].s;
    if(k <= s) return query(lc(x),lc(y),l,m,k);
    else return query(rc(x),rc(y),m + 1,r,k - s);
}

主席树基本上每个函数都会有双指针同步搜索。

最后我们补上前面的坑----离散化来解决空间问题:

步骤(给大家一个口诀):排序,去重,二分找下标。

int getid(int x){
    return lower_bound(v.begin(),v.end(),x) - v.begin() + 1;
}
void work(){
    cin >> n;
    for(int i = 1;i <= n;i++){
        cin >> a[i];
        v.push_back(a[i]);
    }
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());
}

 最后告诉大家这样我们的时间复杂度就是O(n*log(n*n));

知识点应用:【模板】可持久化线段树 2 - 洛谷

标题三:二逼平衡树(树套树)

这里的章节涉及了splay的这个算法,我后天好吧给大家带来我对于splay算法的理解,明天我是给大家发布上面的第i小的堆方法的算法。

代码实现:

#define ls(x) tr[x].s[0]
#define rs(x) tr[x].s[1]

struct node{
    int s[2];
    int p;
    int v;
    int siz;
    void init(int p1,int v1){
        p = p1,v = v1,siz = 1;
    }
}tr[N*40];
int n,m,w[N],idx;

void pushup(int x){
    tr[x].siz = tr[ls(x)].siz + tr[rs(x)].siz + 1;
}
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;
}

void splay(int &root,int x,int k){
    while(tr[x].p != k){
        int y = tr[x].p,z = tr[y].p;
        if(z != k){
            if((rs(y)== x)^(rs(z) == y)) rotate(x);
            else rotate(y);
        }
        rotate(x);
    }
    if(!k) root = x;
}

 其实对于没有学习或者掌握不熟splay的同学,看过我写梳妆数组那一章节,也是可以看懂的.

查找某个区间中某个值的排名:

方法:线段树用来裂开,平衡树负责查找。把线段树不断裂开,遇到已覆盖的区间,在该区间平衡树中查找比某值小的元素个数,区间结果合并时,将小的元素个位求和。

代码实现:

int getrank(int root,int v){
    int u = root,res = 0;
    while(u){
        if(tr[u].v < v){
            res += tr[ls(u)].siz + 1;
            u = rs(u);
        }
        else u = ls(u);
    }
    return res;
}
int queryrank(int u,int l,int r,int x,int y,int v){
    if(x <= l && r <= y) return getrank(root[u],v) - 1;
    int mid = l + r >> 1, res = 0;
    if(x <= mid) res += queryrank(lc,l,mid,x,y,v);
    if(y > mid) res += queryrank(rc,mid + 1,r,x,y,v);
}

这里考一下大家 为什么这里会有一个 -1?

那是因为我们初始化的时候插入了两个极端的数字,所以你要减去1;

查找区间中排名k的数值:

这里大家可能会直接想用上面学的分裂求排名,但这个分裂的区间的大小关系是不一样的,是不可能凑出来第k小的数字,除非题目可以这样安排,这就要回归以前的知识--- 二分查找。

代码实现:

int queryval(int u,int x,int y,int k){
    int l = 0,r = 1e8,ans;
    while(l <= r){
        int mid = l + r >> 1;
        if(queryrank(1,1,n,x,y,mid) + 1 <= k) l = mid + 1,ans = mid;
        else r = mid - 1;
    }
    return ans;
}

修改某一个位置的数值:

void del(int &root,int v){
    int u = root;
    while(u){
        if(tr[u].v == v) break;
        if(tr[u].v < v) u = rs(u);
        else u = ls(u);
    }
    splay(root,u,0);
    int l = ls(u),r = rs(u);
    while(rs(l)) l = rs(l);
    while(ls(r)) r = ls(r);
    splay(root,l,0);
    splay(root,r,l);
    ls(r) = 0;
    splay(root,r,0);
}

void change(int u,int l,int r,int pos,int v){
    del(root[u],w[pos]);
    insert(root[u],v);
    if(l == r) return ;
    int mid = l + r >> 1;
    if(pos <= mid) change(lc,l,mid,pos,v);
    else change(rc,mid + 1,r,pos,v);
}

最后就是求前驱,后继的问题,这里的代码原理是和我前2个文章上已经有了介绍,这里就不做陈述了。

知识点巩固:【模板】树套树 - 洛谷

好,到这里我们的线段树的第一部分就这样结束了,其实线段树就已经结束了,之后的文章是用来补充线段树的杂碎知识,比如合并问题,和与其他算法连用的我的一些算法心得。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值