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

来源:互联网 发布:js中prototype的作用 编辑:程序博客网 时间:2024/06/05 05:32

线段树

  • 会用到的公式:将完全二叉树从左到右、从上到下依次编号,设当前结点编号为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为0void 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,rtstruct 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 位运算,区间修改

1 0