【POJ - 3468】线段树的初步了解

线 段 树 : {\color{Red} 线段树:} 线:

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 O ( l o g N ) O(logN) O(logN)。而未优化的空间复杂度为 2 N 2N 2N,实际应用时一般还要开 4 N 4N 4N的数组以免越界,因此有时需要离散化让空间压缩。

定 义 : {\color{Red}定义:} :

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

对于线段树中的每一个非叶子节点 [ a , b ] [a,b] [a,b],它的左儿子表示的区间为 [ a , ( a + b ) / 2 ] [a,(a+b)/2] [a,(a+b)/2],右儿子表示的区间为 [ ( a + b ) / 2 + 1 , b ] [(a+b)/2+1,b] [(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为 N N N,即整个线段区间的长度。

使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 O ( l o g N ) O(logN) O(logN。而未优化的空间复杂度为 2 N 2N 2N,因此有时需要离散化让空间压缩。

基 本 结 构 : {\color{Red}基本结构:} :

线段树是建立在线段的基础上,每个结点都代表了一条线段 [ a , b ] [a,b] [a,b]。长度为 1 1 1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为 [ a , ( a + b ) / 2 ] [a,(a + b) / 2] [a,(a+b)/2],右结点代表的线段为 [ ( ( a + b ) / 2 ) + 1 , b ] [((a + b) / 2)+1,b] [((a+b)/2+1,b]
下图就是两棵长度范围为 [ 1 , 5 ] [ 1 , 10 ] [1,5][1,10] [1,5][1,10]的线段树。

长度范围为 [ 1 , L ] [1,L] [1,L] 的一棵线段树的深度为 l o g ( L ) + 1 log (L) + 1 log(L)+1。这个显然,而且存储一棵线段树的空间复杂度为 O ( L ) O(L) O(L

线段树支持最基本的操作为插入和删除一条线段。下面以插入为例,详细叙述,删除类似。

将一条线段 [ a , b ] [a,b] [a,b] 插入到代表线段 [ l , r ] [l,r] [l,r]的结点 p p p中,如果 p p p不是元线段,那么令 m i d = ( l + r ) / 2 mid=(l+r)/2 mid=l+r/2。如果 b &lt; m i d b&lt;mid b<mid,那么将线段 [ a , b ] [a,b] [a,b] 也插入到 p p p的左儿子结点中,如果 a &gt; m i d a&gt;mid a>mid,那么将线段 [ a , b ] [a,b] [a,b] 也插入到 p p p的右儿子结点中。

插入(删除)操作的时间复杂度为 O ( l o g n ) O(logn) Ologn

实 际 应 用 : {\color{Red}实际应用:} :

上面的都是些基本的线段树结构,但只有这些并不能做什么,就好比一个程序有输入没输出,根本没有任何用处。

最简单的应用就是记录线段是否被覆盖,随时查询当前被覆盖线段的总长度。那么此时可以在结点结构中加入一个变量 i n t int int c o u n t count count;代表当前结点代表的子树中被覆盖的线段长度和。这样就要在插入(删除)当中维护这个 c o u n t count count值,于是当前的覆盖总值就是根节点的 c o u n t count count值了。

另外也可以将 c o u n t count count换成 b o o l bool bool c o v e r cover cover;支持查找一个结点或线段是否被覆盖。

实际上,通过在结点上记录不同的数据,线段树还可以完成很多不同的任务。例如,如果每次插入操作是在一条线段上每个位置均加 k k k,而查询操作是计算一条线段上的总和,那么在结点上需要记录的值为 s u m sum sum

这里会遇到一个问题:为了使所有 s u m sum sum值都保持正确,每一次插入操作可能要更新 O ( N ) O(N) O(N s u m sum sum值,从而使时间复杂度退化为 O ( N ) O(N) O(N

解决方案

L a z y Lazy Lazy思想:对整个结点进行的操作,先在结点上做标记,而并非真正执行,直到根据查询操作的需要分成两部分。

根据 L a z y Lazy Lazy思想,我们可以在不代表原线段的结点上增加一个值toadd,即为对这个结点,留待以后执行的插入操作 k k k值的总和。对整个结点插入时,只更新 s u m sum sum t o a d d toadd toadd值而不向下进行,这样时间复杂度可证明为 O ( l o g N ) O(logN) O(logN

进行懒惰标记的初级代码

void Lazy(int o,int left)
{
    if(toadd[o])
    {
        toadd[o<<1]+=toadd[o];
        toadd[o<<1|1]+=toadd[o];
        sum[o<<1]+=toadd[o]*(left-(left>>1));
        sum[o<<1|1]+=toadd[o]*(left>>1);
        toadd[o]=0;
    }
}

对一个 t o a d d toadd toadd 值为 0 0 0的结点整个进行查询时,直接返回存储在其中的 s u m sum sum值;而若对 t o a d d toadd toadd不为 0 0 0的一部分进行查询,则要更新其左右子结点的 s u m sum sum值,然后把 t o a d d toadd toadd值传递下去,再对这个查询本身,左右子结点分别递归下去。时间复杂度也是 O ( n l o g N ) O(nlogN) O(nlogN

要想运用首先需要建树这是建树代码:

void build_tree(int left,int right,int o)//建树
{
    if(left==right)
    {
        cin>>sum[o];
        return ;
    }
    int mid=(left+right)>>1;
    build_tree(left,mid,o<<1);
    build_tree(mid+1,right,o<<1|1);
    sum[o]=sum[o<<1]+sum[o<<1|1];
}

要想真正掌握还是需要做题了解;

下面看一道题

A Simple Problem with Integers POJ - 3468

题意

给定 Q ( 1 ≤ Q ≤ 100 , 000 ) 个 数 A 1 , A 2 … A Q Q (1 ≤ Q ≤ 100,000)个数A1,A2 … AQ Q(1Q100,000)A1,A2AQ,以及可能多次进行的两个操作:

  1. 对某个区间 A i … A j Ai … Aj AiAj的每个数都加 n n n( n n n可变)
  2. 求某个区间 A i … A j Ai … Aj AiAj的数的和

值得注意的是

  1. .数据范围,保存数据必须用 l o n g long long l o n g long long保存(除了区间可以用整形,其他最后用64位);

  2. .只存和,会导致每次加数的时候都要更新到叶子节点,速度太慢,这是必须要避免的。

操作代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
const int inf=0x3f3f3f3f;
#define ll long long
#define mem(a,b) memset(a,b,sizeof(a))
const int N=400040;
ll sum[N],add[N];
void build_tree(int l,int r,int o)//建树
{
    if(l==r)
    {
        cin>>sum[o];
        return ;
    }
    int mid=(l+r)>>1;
    build_tree(l,mid,o<<1);
    build_tree(mid+1,r,o<<1|1);
    sum[o]=sum[o<<1]+sum[o<<1|1];
}
void pushdown(int o,int l)
{
    if(add[o])
    {
        add[o<<1]+=add[o];
        add[o<<1|1]+=add[o];
        sum[o<<1]+=add[o]*(l-(l>>1));
        sum[o<<1|1]+=add[o]*(l>>1);
        add[o]=0;
    }
}
void update(int x,int y,int l,int r,int o,int c)
{
    if(l>=x&&r<=y)
    {
        sum[o]+=c*(r-l+1);
        add[o]+=c;
        return ;
    }
    pushdown(o,r-l+1);
    int mid=(r+l)>>1;
    if(x<=mid)update(x,y,l,mid,o<<1,c);
    if(y>mid)update(x,y,mid+1,r,o<<1|1,c);
    sum[o]=sum[o<<1]+sum[o<<1|1];
}
ll query(int x,int y,int l,int r,int o)
{
    if(l>=x&&r<=y)
        return sum[o];
    pushdown(o,r-l+1);
    int mid=(l+r)>>1;
    ll Sum=0;
    if(x<=mid)Sum=query(x,y,l,mid,o<<1);
    if(y>mid)Sum+=query(x,y,mid+1,r,o<<1|1);
    return Sum;
}
int main()
{
    int n,m;
    while(cin>>n>>m)
    {
        mem(sum,0);
        mem(add,0);
        build_tree(1,n,1);
        while(m--)
        {
            string str;
            int l,r,c;
            cin>>str;
            if(str=="Q")
            {
                cin>>l>>r;
                cout<<query(l,r,1,n,1)<<endl;
            }
            else
            {
                cin>>l>>r>>c;
                update(l,r,1,n,1,c);
            }
        }
    }
    return 0;
}

未 完 待 续 ⋅ ⋅ ⋅ ⋅ ⋅ {\color{Magenta} 未完待续\cdot \cdot \cdot \cdot \cdot}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值