线段树蓝书讲解 + 经典例题AcWing 1275. 最大数

线段树介绍

线段树是一种基于分治思想二叉树结构,用于在区间上进行信息统计。

与按照 “2的次幂” 进行划分的树状数组相比,线段树则是一种更为通用的数据结构:

  • ①线段树每个节点都代表一个区间
  • ②线段树的根节点是唯一的,代表的区间是整个统计范围,如:[1, N]
  • ③线段树每个叶子节点都代表一个长度为1单位区间[x, x]
  • ④对于每个内部节点[l, r],其左儿子[l, mid]右儿子[mid+1, r],其中mid=l+r>>1(即(l+r)/2向下取整

区间视角下的线段树:
微信图片_20220214191942.png
二叉树视角下的线段树:
微信图片_20220214192054.png

上图所展示的线段树,如果除去最后一层,整棵树一定是个完全二叉树,且深度为O(logN)。因此我们可以按照之前学过的,与二叉堆类似的 父子2倍 节点编号 方法:

  • ①根节点编号为1
  • ②编号为x的节点左儿子编号为x<<1,右儿子为x<<1|1

这样一来我们就可以用一个结构体数组来存储整个线段树了。

当然,树的最后一层节点在结构体数组中保存位置是不连续的,直接空出数组多余位置就行。

在理想情况下,N个叶子结点的满二叉树N + N/2 + N/4 + ... + 2 + 1 = 2N - 1个节点。

因为在上述存储方式下,最后一层产生冗余(余最多2N个节点),所以保存线段树的数组长度要不小于4N2N - 1 + 2N = 4N - 1)才可以保证不越界!

一、线段树的建树build

线段树的基本用途是对一个序列a进行维护,支持查询ask修改modify指令。

给定一个长度为Na序列,我们可以在区间[1, N]上建立一棵线段树。每个叶子结点[i, i]保存a[i]的值。

线段树的二叉树结构可以非常方便地从下往上传递信息,对于“从下到上”,即由儿子节点算父亲节点信息,我们可以编写一个pushup函数,我们以区间最大值为例子:

记区间[l, r]最大值为 dat(l, r) = max{a[l], a[l+1], ..., a[r]}

显然 dat(l, r) = max(dat(l, mid), dat(mid+1, r))mid = l+r>>1)。

如果用于建堆的序列a = {3, 6, 4, 8, 1, 2, 9, 5, 7, 0},我们可以用build(1, 1, 10),从根节点"1"开始,在序列 a[1, 10]这个区间建立一棵线段树如下图:

微信图片_20220214195608.png

我们先书写一下用于存储线段树的结构体数组

struct node
{
        int l, r;//区间的左、右边界
        int dat;//区间[l, r]最大值
} t[4*N];//结构体数组存储线段树,大小至少是原序列a的4倍

pushup函数:

//由儿子节点算父亲节点信息
void pushup(int u) {t[u].dat = max(t[u<<1].dat, t[u<<1|1].dat);}

build函数:

void build(int u, int l, int r)//建立一颗线段树并在每个节点上保存对应区间最大值max
{
        t[u].l = l, t[u].r = r;//节点u代表区间[l, r]
        if(l==r) {t[u].dat = a[l]; return ;}//如果当前是叶节点直接return
        int mid = l+r>>1;//当前区间中点
        build(u<<1, l, mid), build(u<<1|1, mid+1, r);
        //分别递归建立左边区间[l, mid], 编号u<<1 右边区间[mid+1, r],编号u<<1|1
        pushup(u);
}

调用build函数入口:

build(1, 1, n);//从根节点"`1`"开始,在序列 `a` 的`[1, n]`这个区间建立一棵线段树

二、线段树单点修改modify

单点修改指令形如“C x v”,表示a[x]修改为v

在线段树中,根节点(编号为1的节点)是执行各种指令的入口

根节点出发,递归找到代表区间[x, x]的叶子节点,之后从下往上更新[x, x]以及它的所有祖先节点上保存的信息。如下图,展示一下modify(1, 7, 1),阴影部分是需要改动的节点

微信图片_20220214223841.png

modify函数:时间复杂度O(logN)

//从根节点出发,递归找到代表区间[x, x]的叶节点,之后从下往上更新[x, x]
//以及它的所有祖先节点上保存的信息。

void modify(int u, int x, int v)//单点修改 把a[x]的值修改为v
{
        if(t[u].l==x&&t[u].r==x) {t[u].dat = v; return ;}//如果找到叶节点直接修改
        //否则判断到底是往左递归还是往右递归
        int mid = t[u].l+t[u].r>>1;//取中点
        if(x<=mid) modify(u<<1, x, v);//x属于左边
        else modify(u<<1|1, x, v);//x属于右边
        //递归完成后,当前节点最大值信息一定要记得更新
        pushup(u);//从下往上回溯更新信息
}

调用modify函数入口:

modify(1, x, v);//从根节点出发,将a[x]的值修改为v

三、线段树的区间查询ask

区间查询指令形如“Q l r”,表示查询序列a在区间[l, r]上的最大值,即max{a[l], a[l+1], ..., a[r]}

我们从根节点开始,递归执行一下过程:

  • ①若[l, r]完全覆盖当前节点代表区间,立即回溯,且该节点dat值为候选答案。
  • ②若左儿子与[l, r]有交集,递归访问左儿子。
    ③若右儿子与[l, r]有交集,递归访问右儿子。

如图:执行ask(1, 2, 8) = max{6, 4, 8, 5} = 8区间[2, 8]恰好包含四个阴影节点

微信图片_20220214232102.png

ask函数:时间复杂度O(logN)

int ask(int u, int l, int r)//区间查询最大值max
{
        if(l<=t[u].l&&r>=t[u].r) return t[u].dat;//树中节点,已经完全包含在[l, r]中了
        int mid = t[u].l+t[u].r>>1;
        int val = -(1<<30);//负无穷大
        if(l<=mid) val = max(val, ask(u<<1, l, r));//和左边有交集
        if(r>=mid+1) val = max(val, ask(u<<1|1, l, r));//和右边有交集
        return val;
}

调用ask函数入口:

cout<<ask(1, l, r)<<endl;//从根节点"1"开始,查询[l, r]内的最大值

可以证明,上述查询过程会将询问区间[l, r]在线段树上分为O(logN)个节点,取它们的最大值作为答案。

以上,就是关于线段树的讲解。

例题:AcWing 1275. 最大数

在这里插入图片描述
在这里插入图片描述

题意

微信图片_20220214233443.png

思路

运用线段树实现两个操作:

Q L:询问序列中最后 L 个数的最大数是多少
A t:则表示向序列后面加一个数,加入的数是 (t+a) mod p。其中,t 是输入的参数,a 是在这个添加操作之前最后一个询问操作的答案(如果之前没有询问操作,则 a=0

由于输入指令“A t”修改的时候所用到的值t和上一次查询的值a是有关系的,因此本题是个动态问题,我们不能用静态的方法,先把它们都读进来预处理一遍再去处理,只有知道前一个问题的答案才知道当前准备加入的数是多少。

本题没有设置序列a来初始化线段树,因此初始直接先build一个全0的线段树即可。

之后基本上是前面讲过的线段树基本操作了。

时间复杂度

O(mlogm)

空白代码

#include<bits/stdc++.h>

using namespace std;
#define int long long
const int N = 2e5+10;
int m, p;

struct node
{
        int l, r;
        int dat;
} t[4*N];

void pushup(int u) {t[u].dat = max(t[u<<1].dat, t[u<<1|1].dat);}

void build(int u, int l, int r)
{
        t[u].l = l, t[u].r = r;
        if(l==r) return ;
        int mid = l+r>>1;
        build(u<<1, l, mid), build(u<<1|1, mid+1, r);
}

void modify(int u, int x, int v)
{
        if(t[u].l==x&&t[u].r==x) {t[u].dat = v; return ;}
        int mid = t[u].l+t[u].r>>1;
        if(x<=mid) modify(u<<1, x, v);
        else modify(u<<1|1, x, v);
        pushup(u);
}

int ask(int u, int l, int r)
{
        if(l<=t[u].l&&r>=t[u].r) return t[u].dat;
        int mid = t[u].l+t[u].r>>1;
        int val = -(1<<30);
        if(l<=mid) val = max(val, ask(u<<1, l, r));
        if(r>=mid+1) val = max(val, ask(u<<1|1, l, r));
        return val;
}

signed main()
{
        int now = 0;
        int last = 0;
        cin>>m>>p;
        build(1, 1, m);

        int x;
        char op[2];
        while(m--)
        {
                scanf("%s%d", op, &x);
                if(*op=='Q')
                {
                        last = ask(1, now-x+1, now);
                        printf("%d\n", last);
                }
                else
                {
                        modify(1, now+1, (last+x)%p);
                        ++now;
                }
        }
        return 0;
}

注释代码:

#include<bits/stdc++.h>

using namespace std;
#define int long long
const int N = 2e5+10;
int m, p;//操作数和取模的数

//node结构体
struct node
{
        int l, r;
        int dat;//区间[l, r]最大值
} t[4*N];//结构体数组存储线段树

//pushup函数
//由儿子节点算父亲节点信息
void pushup(int u) {t[u].dat = max(t[u<<1].dat, t[u<<1|1].dat);}

//build函数
void build(int u, int l, int r)//建立一颗线段树并在每个节点上保存对应区间最大值max
{
        t[u].l = l, t[u].r = r;//节点u代表区间[l, r]
        if(l==r)
        {
                //t[p].dat = a[l];//本题没有用于建立线段树的初始数组a,故不要这一句
                return ;//如果当前是叶节点直接return
        }
        int mid = l+r>>1;//当前区间中点
        build(u<<1, l, mid), build(u<<1|1, mid+1, r);
        //递归建立左边区间[l, mid], 编号u<<1 递归建立右边区间[mid+1, r],编号u<<1|1
        //pushup(u);//本题初始化线段树时并没有给dat赋值,因此无需pushup
}

//modify函数
//从根节点出发,递归找到代表区间[x, x]的叶节点,之后从下往上更新[x, x]
//以及它的所有祖先节点上保存的信息。
void modify(int u, int x, int v)//单点修改 把a[x]的值修改为v
{
        if(t[u].l==x&&t[u].r==x) {t[u].dat = v; return ;}//如果找到叶节点直接修改
        //否则判断到底是往左递归还是往右递归
        int mid = t[u].l+t[u].r>>1;//取中点
        if(x<=mid) modify(u<<1, x, v);//x属于左边
        else modify(u<<1|1, x, v);//x属于右边
        //递归完成后,当前节点最大值信息一定要记得更新
        pushup(u);//从下往上回溯更新信息
}

//ask函数
//从根节点开始,递归执行一下过程:
//1.若[l, r]完全覆盖当前节点代表的区间,立即回溯,并且该节点的dat值为候选答案
//2.若左子节点与[l, r]有交集,则递归访问左子节点
//3.若右子节点与[l, r]有交集,则递归访问右子节点
int ask(int u, int l, int r)//区间查询最大值max
{
        if(l<=t[u].l&&r>=t[u].r) return t[u].dat;//树中节点,已经完全包含在[l, r]中了
        int mid = t[u].l+t[u].r>>1;
        int val = -(1<<30);//负无穷大
        if(l<=mid) val = max(val, ask(u<<1, l, r));//和左边有交集
        if(r>=mid+1) val = max(val, ask(u<<1|1, l, r));//和右边有交集
        return val;
}

signed main()
{
        int now = 0;//当前数据的个数
        int last = 0;//存储上一个询问的答案
        cin>>m>>p;
        build(1, 1, m);//建立线段树,操作数是m因此最大长度是m

        int x;
        char op[2];
        while(m--)
        {
                scanf("%s%d", op, &x);
                if(*op=='Q')
                {
                        last = ask(1, now-x+1, now);//从根节点开始查询,当前查询区间是后x个数,n-x+1即查询区间最左边的位置,最右边为n
                        printf("%d\n", last);
                }
                else//把下一个位置n+1的数修改
                {
                        modify(1, now+1, (last+x)%p);//从第一个数开始找,修改第n+1个位置,修改为(last+x)%p
                        ++now;
                }
        }
        return 0;
}
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值