线段树--从入门到精通

线段树,强大的数据结构,用处也是比较广的。

首先,我们要明白线段树是个啥?

线段树,线段嘛,有左右端点,那么它当然可以代表一个区间,那么区间上的好多事情都可以用它来搞,比如:区间加,区间乘,区间求和。

首先让我们先看个线段树的模型。

如图,这就是一棵线段树的模型。

圈内的点表示这是第几个点,红色表示这个点表示的区间范围。

每个点和它的左右两个儿子的编号是有一定的关系的:

点N,它的左儿子编号为N$\times$2,右儿子编号为N$\times$2+1.

线段树支持单点修改,区间修改,单点查询,区间查询。

讲解有易到难。

先放一张后边当例子讲解的图(每个圈中的数表示的为这个区间的和)。

构建线段树框架

假设一段长度为 N 的序列,那么我们需要维护总长为 1--N 的线段。

对于每一个点,我们需要确定它所表示的线段的 左端点 右端点 以及我们要维护的区间和

对于每个点的左儿子和右儿子来说,左儿子继承前一半 [L,(L+R)/2],右儿子继承后一半( (L+R)/2,R ]。

还有我们维护的区间和,每个大区间都是有两个小区间组成,那么 大区间的和 = 左儿子的和+右儿子的和。

这部分代码:

struct ahah{
    long long l,r,sum,f;  //对于 f 的作用,后边会有解释,此处忽略。
}tree[200000<<2];    注意此处四倍空间。
void build(int k,int l,int r)
{
    tree[k].l=l;tree[k].r=r;
    if(tree[k].l==tree[k].r)
    {
        scanf("%lld",&tree[k].sum);
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}

单点查询与修改

单点修改,我们已知单点的位置,那么我们从一号点开始,根据两个儿子所代表的区间范围,选择下一步是走左儿子还是右儿子,今儿一步步的确定准确的点。

单点查询与单点修改几乎一样,查询到具体的位置后,输出其结果。

拿上边的图进行模拟下:

修改4号点:左儿子[0,4],右儿子[5,8] ->选择左儿子 ->左儿子[0,2],右儿子[3,4] ->选择右儿子 ->... -> 找到4号点修改。

查询同上。

当我们修改完某个点以后,包含这个点的区间的和发生了改变,所以最后我们还要加一句:

$tree[k].sum=tree[k \times 2].sum+tree[k \times 2+1].sum$ 以确保维护的区间和不会改变。

代码:k表示点的编号,需要给x号点加上y 。

void update(int k)
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].sum+=y;
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)update(k<<1);
    else update(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}

区间求和与修改

区间修改与查询也有很大的相似。

区间修改,暂时来说我们没有好办法,只能一个一个的修改区间中的每一个元素,后边会有优秀做法的讲解。

区间查询,我们需要明确这个被查询的区间位置。

以下被查询的区间用[a,b]表示,k表示当前的点的编号。

首先我们从最大的区间开始,判断被查询的区间,有三种情况:

1.位于左儿子中($b \le tree[k<<1].l $)还是右儿子中($a > tree[k<<1|1].l $),然后选择下一步是去左儿子还是右儿子。

2.被查询的区间被两部分都包括,那么我们就将区间分开,一部分查询左区间,一部分查询右区间。

3.现在的点所代表的区间$(a <= tree[k].l  , b >= tree[k].r )$ 被要查询的区间所包含,那么不需要再查下去,直接将答案加上这段区间所维护的和就好了。

拿查询区间[3,5]模拟一下:

mid表示当前区间的二分点。

    

代码用递归实现:

void query(int k)
{
    if(x<=tree[k].l&&y>=tree[k].r)
    {
        ans+=tree[k].sum;
        return ;
    }
    if(tree[k].f)down(k);    //先省略就好。
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)query(k<<1);
    if(y>mid)query(k<<1|1);
}

重点来了:懒标记

对于区间修改来说,我们一个一个的修改浪费大量的时间,并且修改了还不一定查修这个点,为了解决这个问题,我们引入懒标记 f 。

首先我们要明确他的一个性质: 懒,用得着的时候动一下,用不着的时候就永远在那。

每个节点的的懒标记记录的是它所代表的这个区间所加的值 f 。

就像区间查询一样,当区间不被包含时,分开查找,当目前区间已被要修改的区间包含时,那么我们就可以直接给这个点,打上懒标记,不需要去准确的一个一个的修改区间内的元素了。

那这样的话必究没法维护区间和了?

我们维护区间和为的是啥?当然是为了求区间和了,当我们在查询的时候,若用得到这整个区间,那么返回 维护的值 + 区间元素个数$\times$懒标记的值,若不全用得到的话,那么我们将懒标记下传给它的左右两个儿子,然后继续查找。区间和并不是没有维护,而是在维护懒标记从而间接地维护者区间和。

这里需要注意的是:当节点的懒标记下传给儿子的时候它的懒标记则需要清空,因为已经传给了儿子。

蓝标即下传代码:

void down(long long k)
{
    tree[k<<1].f+=tree[k].f;
    tree[k<<1|1].f+=tree[k].f;
    tree[k<<1].sum+=(tree[k<<1].r-tree[k<<1].l+1)*tree[k].f;
    tree[k<<1|1].sum+=(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].f;
    tree[k].f=0;
}

用到蓝标即的区间加以及求和:

void query(int k)
{
    if(x<=tree[k].l&&y>=tree[k].r)
    {
        ans+=tree[k].sum;
        return ;
    }
    if(tree[k].f)down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)query(k<<1);
    if(y>mid)query(k<<1|1);
}
void add(long long k)
{
    if(tree[k].l>=x&&tree[k].r<=y)
    {
        tree[k].sum+=(tree[k].r-tree[k].l+1)*val;
        tree[k].f+=val;
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)add(k<<1);
    if(y>mid)add(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}

综上就是先对简单的线段树操作。

贴上模板:

#include<cstdio>
#include<iostream>
using namespace std;

long long n,m,ans,x,y,ch,val;
struct ahah{
    long long l,r,sum,f;
}tree[200000<<2];
void build(int k,int l,int r)
{
    tree[k].l=l;tree[k].r=r;
    if(tree[k].l==tree[k].r)
    {
        scanf("%lld",&tree[k].sum);
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
void update(int k)
{
    if(tree[k].l==tree[k].r)
    {
        tree[k].sum+=y;
        return ;
    }
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)update(k<<1);
    else update(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
void down(long long k)
{
    tree[k<<1].f+=tree[k].f;
    tree[k<<1|1].f+=tree[k].f;
    tree[k<<1].sum+=(tree[k<<1].r-tree[k<<1].l+1)*tree[k].f;
    tree[k<<1|1].sum+=(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].f;
    tree[k].f=0;
}
void query(int k)
{
    if(x<=tree[k].l&&y>=tree[k].r)
    {
        ans+=tree[k].sum;
        return ;
    }
    if(tree[k].f)down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)query(k<<1);
    if(y>mid)query(k<<1|1);
}
void add(long long k)
{
    if(tree[k].l>=x&&tree[k].r<=y)
    {
        tree[k].sum+=(tree[k].r-tree[k].l+1)*val;
        tree[k].f+=val;
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(x<=mid)add(k<<1);
    if(y>mid)add(k<<1|1);
    tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
int main()
{
    scanf("%lld%lld",&n,&m);
    build(1,1,n);
    for(int i=1;i<=m;i++)
    {
        ans=0;
        cin>>ch>>x>>y;
        if(ch==1)
        {
            cin>>val;
            add(1);
        }
        else
        {
            query(1);
            cout<<ans<<"\n";
        }
    }
}

例题:

入门

模板:洛谷线段树1:https://www.luogu.org/problemnew/show/P3372

单点修改与区间查询:最大数https://www.luogu.org/problemnew/show/P1198

进阶:

妖梦斩木棒:https://www.luogu.org/problemnew/show/P3797

无聊的数列:https://www.luogu.org/problemnew/show/P1438

原文地址:https://www.cnblogs.com/rmy020718/p/9571490.html

时间: 2024-08-09 08:32:15

线段树--从入门到精通的相关文章

hdu 1754:I Hate It(线段树,入门题,RMQ问题)

I Hate It Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 33726    Accepted Submission(s): 13266 Problem Description 很多学校流行一种比较的习惯.老师们很喜欢询问,从某某到某某当中,分数最高的是多少.这让很多学生很反感.不管你喜不喜欢,现在需要你做的是,就是按照老师的要求

线段树之入门篇

线段树(interval tree) 是把区间逐次二分得到的一树状结构,它反映了包括归并排序在内的很多分治算法的问题求解方式. 上图是一棵典型的线段树,它对区间[1,10]进行分割,直到单个点.这棵树的特点 是: 1. 每一层都是区间[a, b]的一个划分,记 L = b - a 2. 一共有log2L层 3. 给定一个点p,从根到叶子p上的所有区间都包含点p,且其他区间都不包含点p. 4. 给定一个区间[l; r],可以把它分解为不超过2log2 L条不相交线段的并. 其中第四点并不是很显然,

线段树入门---给定多个线段求点的出现个数

线段树是一颗二叉树,他的每个节点都是一个区间,此题为线段树的入门题目,只是学习笔记.例题:给定N个线段,给定M个点,求点在多少个线段中出现过,此时如果用传统的方法来求,时间复杂度太高,但是,线段树的时间复杂度还可以接受. 步骤为: 1. 首先找一个区间,能覆盖给定的所有区间, 然后把此区间建立线段树,建立线段树的方式是二分法建立,即它的左孩子是他的左半个区间,右孩子是它的右边那个区间.一个图足以说明清楚 2. 将所有的区间映射到此树上, 从根节点开始遍历, 每遍历一个节点考虑四种情况: 1) 当

bzoj-1012 1012: [JSOI2008]最大数maxnumber(线段树)

题目链接: 1012: [JSOI2008]最大数maxnumber Time Limit: 3 Sec  Memory Limit: 162 MB Description 现在请求你维护一个数列,要求提供以下两种操作:1. 查询操作.语法:Q L 功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值.限制:L不超过当前数列的长度.2. 插入操作.语法:A n 功能:将n加上t,其中t是最近一次查询操作的答案(如果还未执行过查询操作,则t=0),并将所得结果对一个固定的常数D取模,将所得

HDU 1166 敌兵布阵 (线段树 &amp; 树状数组)

敌兵布阵 Time Limit:1000MS    Memory Limit:32768KB    64bit IO Format:%I64d & %I64u SubmitStatus 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1166 Description C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的

HDU 1754 I Hate It (线段树 &amp; 树状数组)

I Hate It Time Limit: 9000/3000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 39959 Accepted Submission(s): 15863 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1754 Problem Description 很多学校流行一种比较的习惯.老师们很喜欢询问,从某某到某某当

白话数据结构之【线段树】

线段树的入门 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b].因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度. 使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN).而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩. ----来自百度百科 [

codeforces 339C Xenia and Bit Operations(线段树水题)

转载请注明出处: http://www.cnblogs.com/fraud/          ——by fraud Xenia and Bit Operations Xenia the beginner programmer has a sequence a, consisting of 2n non-negative integers: a1, a2, ..., a2n. Xenia is currently studying bit operations. To better unders

HDU 1754 I Hate It (线段树单点更新)

Problem Description 很多学校流行一种比较的习惯.老师们很喜欢询问,从某某到某某当中,分数最高的是多少. 这让很多学生很反感. 不管你喜不喜欢,现在需要你做的是,就是按照老师的要求,写一个程序,模拟老师的询问.当然,老师有时候需要更新某位同学的成绩. Input 本题目包含多组测试,请处理到文件结束. 在每个测试的第一行,有两个正整数 N 和 M ( 0<N<=200000,0<M<5000 ),分别代表学生的数目和操作的数目. 学生ID编号分别从1编到N. 第二