【NOIp复习】数据结构之线段树

线段树

  • 会用到的公式:将完全二叉树从左到右、从上到下依次编号,设当前结点编号为n
  • 左节点编号为:2*n 右节点编号为2*n+1
  • 把空节点的编号初始化为-1
  • 线段树的本质其实是二叉搜索树啦,所以说可以很方便的解决区间最大最小、查询区间和、修改区间和的问题
  • 用数组表示线段树的话,如果本身的区间长度为n,线段树节点数2*n左右,开3*n的大小很保险
  • 设线段树上节点代表区间为[a,b],那么左子树代表[a,(a+b)/2],右子树代表[(a+b)/2,b]
  • 如果a==b,那么该节点为叶子结点,否则递归建树

先写了个很难看的代码…(并不能作为模板,模板在后面)

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#define maxn 50010
#define REP(a,b) for(int i=a;i<=b;i++) 
using namespace std;

struct node{
    int left,right,val;//左右端点(代表该节点覆盖线段范围)、权值 
}tree[maxn*3];

int s[maxn];//这里的s是读入的待建树数组
int ans=0;//查询的区间和累加在ans上,注意每次查询之后都要重置ans为0

void create(int l, int r, int num){//建树操作,左右端点与节点编号作为传入参数 
    tree[num].left=l;
    tree[num].right=r;
    if(l==r) {
        tree[num].val=s[l]; return;
    }
    else {
        create(l,(l+r)/2,num*2);
        create((l+r)/2+1,r,num*2+1);
        tree[num].val=tree[num*2].val+tree[num*2+1].val;//这里以维护区间和为例 
        return;
    }
}

void add(int a, int b, int num){//第a个位置增加b,当前修改节点编号为num 
    tree[num].val+=b;
    if(tree[num].left==tree[num].right) return;//已经修改到叶子了,不必再向下修改
    else{
        if(a>(tree[num].left+tree[num].right)/2) add(a,b,num*2+1);//如果a在num节点的右区间,访问右子树
        else add(a,b,num*2);
        return;
    }
}

void ask(int l, int r, int num){//查询从l到r的区间和,当前考察节点编号为num 
    if(l<=tree[num].left&&r>=tree[num].right){//如果该节点存放的是待查找区间的一部分,就可以把这个节点累加上去而不必再往下搜索了 
        ans+=tree[num].val;
        return;
    }
    if(l>tree[num].right||r<tree[num].left) return;
    else{
        int mid=(tree[num].left+tree[num].right)/2;
        if(l>mid) {
            ask(l,r,num*2+1);
            return;
        }
        else if(r<mid) {
            ask(l,r,num*2); return;
        }
        else {
            ask(l,r,num*2);
            ask(l,r,num*2+1);
            return;
        }
        return;
    }
}

线段树的区间修改(lazy思想)

首先是改进了上一段代码的表示方法,不将num节点所存储的区间端点放在结构体中,而是放在函数中作为参数传入,可以用macro简化代码。【代码中的大写字母L,R都代表num节点的左右端点,小写字母l,r代表查询或修改区间的左右端点】
其次是用pushDown函数和lazy标记实现了区间修改,因为区间修改没有必要在查询到它之前去向下传递,在修改时只需要检查当前num节点是否有lazy,如果有,pushDown之后看修改区间在什么位置对左右子树进行递归操作。查询和修改几乎完全一样。

#include <cstdio>
#include <cstring>

#define maxn 100000+10
#define lson L,mid,rt<<1 //左子树 左端点,右端点,编号
#define rson mid+1,R,rt<<1|1
#define root L,R,rt

struct node{
    int val, lazy;
}T[maxn<<2]; 

void pushUp(int num){//num节点的左右子树都已经更新完毕了再pushUp更新num本身 
    T[num].val=T[num<<1].val+T[num<<1|1].val;
}

void pushDown(int L,int R,int num){
    int mid=(L+R)>>1;
    T[num<<1].val=T[num].lazy*(mid-L+1);
    T[num<<1|1].val=T[num].lazy*(R-mid);
    T[num<<1].lazy=T[num].lazy;
    T[num<<1|1].lazy=T[num].lazy;
    T[num].lazy=0;
}

void build(int L,int R,int num){
    if(L==R){
        scanf("%d",&T[num].val);
        return;
        //如果数组s[i]是已经读入的数据的话
        //T[num].val=s[L]; return; 
    }
    int mid=(L+R)>>1;
    build(lson);
    build(rson);
    pushUp(num);//左右子树都计算完成了就可以把自己算出来啦 
}

void update(int l,int r,int v,int L,int R,int rt){
//在覆盖[L,R]的rt节点的[l,r]区间增加v 
    if(l==L&&r==R){
        T[rt].lazy=v;
        T[rt].val=v*(R-L+1);
        return;
    }
    int mid=(L+R)>>1;
    if(T[rt].lazy) pushDown(root);
    if(r<=mid) update(l,r,v,lson);//注意向下取整左取等右不取等 
    else if(l>mid) update(l,r,v,rson);
    else{
        update(l,mid,v,lson);
        update(mid+1,r,v,rson);
    }
    pushUp(num);
}

int query(int l,int r,int L,int R,int rt){
    if(l==L&&r==R) return T[rt].val;
    int mid=(L+R)>>1;
    if(T[rt].lazy) pushDown(root);
    if(r<=mid) return query(l,r,lson);
    else if(l>mid) return query(l,r,rson);
    return query(l,mid,lson)+query(mid+1,r,rson);
}

int main(){
    int n,q;
    scanf("%d",&n);
    build(1,n,1);//从根节点开始建树
    scanf("%d",&q);
    while(q--){
        //应对多组查询 
    } 
    return 0;
}

练习题

HDU 1166 单点修改,区间查询,用树状数组也可以实现
HDU 1754 查询区间最大值
HDU 1698 区间修改,区间查询,lazy思想
HDU 1394 求逆序数,也可以用树状数组,记录每个位置与左边的差
POJ 2777 位运算,区间修改

posted @ 2016-11-05 23:04  Leo.Tan  阅读(205)  评论(0编辑  收藏  举报