C++函数式实现BST、线段树(单点修改)(2)

C++函数式实现BST、线段树(单点修改)(2)

C++函数式实现BST、线段树(单点修改)(1)

上一次是之前写的BST,那么我们今天就来聊一聊线段树~单点修改的。

本篇前置知识:线段树(普通递归的写法,非zwk)

最好掌握但是不掌握也没什么很大关系只是后面有一段论述可能会看不懂而已的知识:主席树


Part I:线段树复习

我觉得每个人的对于线段树的写法可能不一样,比如这样一个例题:Problem - 1754

单点更新:给出两个数x y,代表把x位置的数更新为y (y>=0)

查询区间max值:给出两个数x y,求区间[x,y]上的最大值

一开始长度为n的数组会指定初始值。

那么关键就是三个函数:(node代表当前节点编号,le和ri代表当前node代表的区间,st就是线段树数组)

void init(int node,int le,int ri){
    if(le==ri)
        st[node]=GIVEN_VALUE //GIVEN_VALUE只表示这里的给定的初始值,并不是预定义的常量
    else{
        int m=(le+ri)>>1;
        init(node<<1,le,m);
        init(node<<1|1,m+1,ri);
        st[node]=max(st[node<<1],st[node<<1|1]);
    }
}

void update(int node,int le,int ri){
    if(le==ri)
        st[node]=y;
    else{
        int m=(le+ri)>>1;
        if(x<=m)
            update(node<<1,le,m);
        else
            update(node<<1|1,m+1,ri);
        st[node]=max(st[node<<1],st[node<<1|1]);
    }
}

int query(int node,int le,int ri){
    int m=(le+ri)>>1;
    if(x<=le&&ri<=y)
        return st[node];
    if(y<=m)
        return query(node<<1,le,m);
    if(x>m)
        return query(node<<1|1,m+1,ri);
    if(le<x||ri>y)
        return max(query(node<<1,le,m),query(node<<1|1,m+1,ri));
}

(好早之前写的代码了...但是逻辑应该还是很清楚的,注意update是单点更新)


Part II: 函数式写法

(没有提交,只测了样例,应该是正确的,提交肯定过不了..)

(new完没有delete)

让我们首先定义Node结构体,同之前的BST是类似的,注意声明了一个全局的root变量

struct Node{
    int v;
    Node* le,*ri;
    explicit Node(int value=0,Node* lef=nullptr,Node* righ=nullptr):
        v(value),le(lef),ri(righ){}
}*root;

然后就是init函数

Node* init(int le,int ri){
    if(le==ri)
        return new Node(GIVEN_VALUE);
    else{
        int m=(le+ri)>>1;
        Node* lef=init(le,m),*rig = init(m+1,ri);
        return new Node(max(lef->v,rig->v),lef,rig);
    }
}

这里的le和ri代表当前节点代表的区间,因此一开始的调用是init(1,n)。假设le==ri,代表当前已经走到叶子节点了,则直接返回,否则分别init完左右子之后再返回自身节点。

然后就是update函数:

Node* update(Node* st,int le,int ri){
    if(le==ri)
        return new Node(y);
    else{
        int m=(le+ri)>>1;
        Node* tmp;
        if(x<=m){
            tmp=update(st->le,le,m);
            return new Node(max(tmp->v,st->ri->v),tmp,st->ri);
        }else{
            tmp=update(st->ri,m+1,ri);
            return new Node(max(tmp->v,st->le->v),st->le,tmp);
        }
    }
}

注意这里是单点更新..即更新的区间是[x,x],因此只要考虑x<=m和x>m的情况即可。同BST类似,这一下又会开创出一条新链出来,即从root节点到最终的叶子节点是一条新链,然后这条链有链向原来的节点。这个过程基本和之前BST是一样的,就不画图了,而且鼠绘完全二叉树好难......,相比BST只是多了一个更新自身的值的操作而已。


Part III:主席树

在BST中,我们最外层调用bst_insert是这样调用的

root=bst_insert(rand()%100,root);

同理,我们这里最外层调用update

root=update(root,1,n);

但是,假如我们这里不把root的值覆盖掉,而是把root声明为一个数组,比如说这样

int size=0;
root[size++]=init(1,n);

我们每一次都这样更新

root[size+1]=update(root[size],1,n);
++size;

这里的意思就是,我们把单点更新之后的线段树,即仅仅有着一条新链,但是其余节点仍然指向上一棵线段树的节点的线段树的根节点保存起来。这也就意味着,我们做了m次操作之后,root里面会有m+1棵线段树(初始有1棵)。对于相邻两棵线段树来说,后面一棵仅仅和之前的一棵有一条链是不一样的而已,后面一棵其余的节点是仍然指向前面那一棵的,换而言之,后面一棵的非新链上的节点是和前面一棵公用的。

这样做有什么好处?

一个显而易见的好处就是我们现在能够查询历史版本了,比如说,询问在第k次修改之后的区间[x,y]上的最大值,因为对于每一次修改之后的线段树的root节点我们都保存在了数组里面,因此便直接调用

query(root[k],1,n)

即可。注意此处适用于单点修改,区间修改需要打lazy tag,我还需要仔细想一想。


那么最后再来一个例题讲解 2104 -- K-th Number

题意是给出 n和m,n是数组长度,m是操作次数,n为100,000,m为5000

每一次操作给出三个数x y k询问区间[x,y]上第k小的数

首先考虑简单情况,如何用线段树解决询问区间[1,n]上的第k小的数。

假设数组元素的数据范围是[1,100],那么我们开一个至少有100个叶子节点的线段树,线段树的节点维护当前区间数字出现次数之和,每来一个数v,就对区间[v,v]进行+1操作。查询时先看左子树的次数之和left_sum是否超过k,若超过,进入左子树查询,否则进入右子树查询第k-left_sum小的数。

但是现在元素的数据范围是1e9太大了,因为n最多只有100,000,离散化即可。

我们解决了对于整个数组查询第k小的问题,接下来解决区间查询

注意到区间数字出现次数之和可以用前缀和来维护,即对于任意一个区间[x,y]

[x,y]中的数字出现次数之和等于 [1,y]中数字出现次数之和 - [1,x-1]中数字出现次数之和

这也就意味着我们只需要n棵线段树就能求出任意一个区间的数字出现次数之和,这n棵线段树对应着区间[1,1]、[1,2]、[1,3]、......、[1,n]。

对于查询任意一个区间[x,y],我们只要拿对应[1,y]线段树上节点的值减掉[1,x-1]线段树上对应节点的值即可。

最后看这n个线段树有什么性质

对于相邻两个线段树,即[1,i]和[1,i+1],相当于把源数组的第i+1个元素,无妨说是a,在[1,i]线段树上对区间[a,a]进行了+1操作,即,满足我们上面所说的,相邻两棵线段树只有一条链是不一样的。


Part IV:

函数式实现线段树代码:github.com/Hatsunespica

有关于例题的k-number的AC代码可以参看我的回答,我的回答只有两个...

写了很多,若有错误,还请指正。

编辑于 2018-04-04

文章被以下专栏收录