ACM竞赛算法之线段树

线段树是一个很重要的数据结构,而且在算法竞赛中用处也十分巨大,但很多人往往认为线段树是一个算法,可以完成某些功能,但是实际上完全可以把它看成是一个容器,用来执行的操作可以按照需求修改


首先思考如下问题:
思考一、现在如果有一个长度为n的序列,有m次操作,操作中包含区间求和,某点修改,并且n和m特别大
思考二、现在给你一个长度为n的序列,有m次操作,操作中仅包含求区间内最大值或最小值的问题,n和m特别大


这两个思考题看起来都很容易,暴力都可以解决问题,但是如果n和m特别大的时候,就会出现时间超限的问题,所以使用线段树进行维护就会节省时间,但要消耗内存,适当的开结点数量才可以完美的AC,如果开的结点数量少的话就会出现运行错误,如果开的结点数量大 的话又会出现内存超限,所以经过计算及测试后,确定将结点个数开成序列长度的四倍刚刚好


学习线段树,首先要知道线段树是一棵二叉树,如果从左至右从上至下给每个结点进行编号的话,如果双亲结点的标号为root,那么它的左孩子的编号就为root*2,右孩子的标号为root*2,这就是双亲结点与孩子结点的关系,也是线段树结点之间的关系,还要确定结点内需要存储记录的东西

int a[1000];//存储数据
int sum=0;//计算和
struct st
{//线段树结点信息
    int l,r,len,lazy,sum;
    //左端点、右端点、区间长度、懒惰标记、区间和
}tree[4005];//四倍序列长度

建树操作
线段树的建树方法也十分重要,一般采用二分回溯的方法建树及处理,先进行建树,在组建结点全部完成后再回溯执行向上更新
操作

void push_up(int root)
{//线段树向上更新,即双亲结点的和等于孩子结点之和
    tree[root].sum=tree[root<<1].sum+tree[root<<1|1].sum;
}
void bulid(int l,int r,int root)
{//建树操作
    tree[root].l=l;//左端点
    tree[root].r=r;//右端点
    tree[root].len=r-l+1;//区间长度
    tree[root].lazy=0;//lazy标记
    if(l==r){//叶子结点存值
        tree[root].sum=a[l];
        return ;
    }
    int mid=(l+r)>>1;//找寻中间值
    bulid(l,mid,root<<1);//建立左子树
    bulid(mid+1,r,root<<1|1);//建立右子树
    push_up(root);//向上更新线段树
}

在数据结构中,有两个十分重要的操作,那就是查询和更新,线段树也是这样的,查询和更新也很重要
线段树更新操作有两种,一种是单点更新,一种是区间更新
查询操作也对应的有两种,但是它们之间的联系比较紧密


线段树单点更新
线段树单点更新还是比较容易理解的,找到线段树中某一个叶子结点进行修改,然后再更新部分结点信息即可

void update_point(int i,int x,int root)
{//单点更新操作,某个点增加x
    if(tree[root].l==tree[root].r&&tree[root].l==i){
        //找到修改位置,进行修改
        tree[root].sum+=x;
        return ;
    }
    int mid=(tree[root].l+tree[root].r)>>1;
    if(i<=mid) update_point(l,mid,root<<1);
    //需要更新结点在当前结点的左子树部分
    else update_point(mid+1,r,root<<1|1);
    //需要更新结点在当前结点的右子树部分
    push_up(root);
}

线段树区间更新
线段树区间更新相对于线段树单点更新比较难,简单暴力也可以实现区间更新,暴力每一个更新的点进行单点更新,但是这样往往得到的答案都是TLE,所以就需要使用一个懒惰标记lazy进行暂停更新,如果需要查询到结点的话再向下更新,否则不更新,这样就会节省时间

void push_down(int root)
{//线段树向下更新,即lazy更新
    tree[root<<1].sum+=tree[root<<1].len*tree[root<<1].lazy;
    tree[root<<1|1].sum+=tree[root<<1|1].len*tree[root<<1|1].lazy;
    //更新孩子结点sum值操作
    tree[root<<1].lazy+=tree[root].lazy;
    tree[root<<1|1].lazy+=tree[root].lazy;
    //更新孩子结点lazy操作
    tree[root].lazy=0;
    //双亲结点lazy清零
}
void update_interval(int l,int r,int c,int root)
{//线段树区间更新操作
    if(l<=tree[root].l&&r>=tree[root].r){
        //如果要更新区间包含当前结点区间
        tree[root].sum+=tree[root].len*c;
        //修改当前结点和
        tree[root].lazy+=c;
        //修改当前结点lazy标记
        return ;
    }
    if(tree[root].lazy) push_down(root);
    //判断当前结点的lazy标记是否存在,存在即向下更新
    int mid=(tree[root].l+tree[root].r)>>1;
    if(l<=mid) update_interval(l,r,c,root<<1);
    //如果成立,表示更新区间部分在当前结点的左子树中
    if(r>mid) update_interval(l,r,c,root<<1|1);
    //如果成立,表示更新区间部分在当前结点的右子树中
    push_up(root);
}

线段树查询操作
线段树查询是查询区间内区间和、最大最小值问题,也可以查询其他的东西,主要是看解决什么问题,这里我写了两段查询的代码,分别为单点更新查询、区间更新查询的两段代码,代码中注释很多。

void query_point(int l,int r,int root)
{//线段树单点更新查询操作
    if(tree[root].l==l&&tree[root].r==r){
        //查到符合要求区间,加和
        sum+=tree[root].sum;
        return ;
    }
    /*if(tree[root].lazy) push_down(root);
    加上这行即可完成区间更新操作*/
    root<<=1;
    if(l<=tree[root].r){
        if(r<=tree[root].r)
            //所求区间在结点左子树中
            query_point(l,r,root);
        else
            //所求区间部分在结点左子树中
            query_point(l,tree[root].r,root);
    }
    root++;
    if(r>=tree[root].l){
        if(l<=tree[root].l)
            //所求区间在结点右子树中
            query_point(l,r,root);
        else
            //所求区间部分在结点右子树中
            query_point(tree[root].l,r,root);
    }
}
int query_interval(int l,int r,int root)
{//线段树区间更新查询操作
    if(l<=tree[root].l&&r>=tree[root].r){
        //查询区间包含结点区间
        return tree[root].sum;
    }
    if(tree[root].lazy) push_down(root);
    //判断是否可以向下更新
    int mid=(tree[root].l+tree[root].r)>>1;
    int ans=0;//计算值
    if(l<=mid) ans+=query(l,r,root<<1);
    //查询区间部分在左子树部分
    if(r>mid) ans+=query(l,r,root<<1|1);
    //查询区间部分在右子树部分
    return ans;
}

以上只是线段树的基础知识,建议多多理解线段树的思想及实现过程,ACM竞赛中几乎没有模板,思想要比代码重要一些
如有错误,还请批评指正

你可能感兴趣的:(算法)