首页 > 编程语言 >[算法学习笔记01]线段树

[算法学习笔记01]线段树

时间:2024-02-02 09:04:02浏览次数:35  
标签:lazy 01 int 线段 tree 算法 sum id

# [算法学习笔记01]线段树
### 每日蒟蒻小故事(1/1)
蒟蒻刚刚升到S组,发现S组正在学习线段树Ⅲ.

蒟蒻并不知道什么是线段树.

蒟蒻十分害怕,向大佬询问什么是线段树.

大佬邪魅一笑,并未解释.

于是可怜的蒟蒻什么也听不懂,只得在洛谷和OI WIKI上自学线段树.

“所以什么是线段树?”
### 什么是线段树?
线段树是算法竞赛中常用的用来维护**区间信息**的数据结构。

一般来说,它可以:

1. 查询某个区间

2. 修改某个区间或点

线段树将每个长度不为 $1$ 的区间划分成左右两个区间递归求解,把整个线段划分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。这种数据结构可以方便的进行大部分的区间操作。

“其实感觉没啥用.”蒟蒻说.

不,你错了!它的用处很大,对于所有区间的问题,他几乎都可以快速解决。

“真的假的?那怎么建树呢?”
### 怎么建树?
有个大小为 5 的数组 $a={10,11,12,13,14}$ ,要将其转化为线段树,有以下做法:设线段树的根节点编号为 $1$,用数组 $d$ 来保存我们的线段树,$di$ 用来保存线段树上编号为 $i$ 的节点的值(这里每个节点所维护的值就是这个节点所表示的区间总和)。

我们先给出这棵线段树的形态,如图所示:

![线段树的形态](https://oi-wiki.org/ds/images/segt1.svg)

(图片来自OI WIKI)

我们可以清楚地通过上图发现线段树的每个子节点都是由它的父亲节点二分而来的,再通过二叉树的性质,我们可以知道对于一个父节点$i$,它的左右儿子节点分别为 $2i$ 和 $2i+1$ 。

所以我们可以根据这个性质用数组来模拟线段树,代码如下。


```cpp
void build(int id,int l,int r){
tree[id].l=l;
tree[id].r=r;
if(l==r){
tree[id].sum=a[l];
return;
}
int mid=(l+r)/2;
build(id*2,l,mid);
build(id*2+1,mid+1,r);
tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;
}
```
蒟蒻看到一连串的代码十分开心,连忙将其Ctrl+c+v提交到[题目](https://www.luogu.com.cn/problem/P3372)里。

竟然是 WA !

蒟蒻十分生气,质问道:“为什么成功不了?”

你还没有区间更新呢!相当于是你只写了四分之一的代码啊!

“那怎么解决区间更新?”
### 怎么解决区间更新?
如果我们要对这颗树上的区间加上 v ,是不是只用找到代表这个区间的节点(可能不止一个节点,例如[2,3]这个区间就需要查找[2,2]和[3,3]这两个区间,然后将他们的 sum 加起来),然后将这个节点的sum值加上v乘以这个区间所包含的数的个数?下次访问相同的区间的时候直接将这个数返回过去,根本就不用再向下遍历了。

但是问题又来了,上一次我对[1,2]这个区间加了 v ,这一次我想知道[2,3]的区间和怎么办呢?要是只用我们上面提到的方法,很明显遍历到[2,2]的时候这个点并没有加v,那返回时答案就错了呀!

怎么办?

这时延迟标记( lazy )就派上用场了,它代表我对这个区间的数加过的值,我们在对一个区间加了 v 之后,我们将 lazy 的值也加上这个 v ,后来我们再访问这个区间但只是访问这个区间的一部分时我们就可以知道这个父节点下面的儿子节点需要加上多少了,但我们现在不用急着将这个 lazy 标记给它的儿子们加上,因为这很费时,当我们下次将要访问它的儿子节点时再给他们加上 lazy 标记,此时这个父节点的 lazy 值就要清零,我们就称其为标记下传。

举个例子:我们在访问[2,3]时,我们会访问[2,2]和[3,3]这两个节点,将要访问[2,2]时,我们肯定会访问[1,2]这个点,那么在向下遍历的同时,我们就顺便将 lazy 标记下传,下传时就把每个叶子节点的 lazy 加上这个值,再按之前说到的方法更新 sum 。如此反复。

代码则在这:

```cpp
void down(int id){
tree[id*2].lazy+=tree[id].lazy;
tree[id*2].sum+=(tree[id*2].r-tree[id*2].l+1)*tree[id].lazy;
tree[id*2+1].lazy+=tree[id].lazy;
tree[id*2+1].sum+=(tree[id*2+1].r-tree[id*2+1].l+1)*tree[id].lazy;
tree[id].lazy=0;
}
void update(int id,int l,int r,int v){
if(tree[id].l>r || tree[id].r<l) return;
if(tree[id].l>=l && tree[id].r<=r){
tree[id].lazy+=v;
tree[id].sum+=(tree[id].r-tree[id].l+1)*v;
return;
}
if(tree[id].lazy>0) down(id);
update(id*2,l,r,v);
update(id*2+1,l,r,v);
tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;
}
```

蒟蒻高兴地复制代码。

还是 WA !

“为什么!!!”

这只是两个片段啊!要结合起来才能使用!

“那赶快总结!”
### 快速总结一下!
其实就分为这么两大步:


>1.判断访问区间和遍历区间的关系(相离,部分相交,完全包含)

>>如果相离,直接返回。

>>如果部分相交,那么就需要将标记下传(如果有),然后向下找完全包含的区间。

>>如果完全包含,更新 lazy 标记,并且将 sum 更新(因为当前不会再向下,所以不能通过左右儿子的 sum 来计算)要按代码那样计算(也就是将这个节点的 sum 值加上 v * 这个区间所包含的数的个数)

>>在更新时一定要标记下传!


>2.标记下传

>>先下传到左儿子,再下传到右儿子

>>下传时你需要更新儿子节点的 lazy (因为它还可能有儿子,下次遍历它的儿子时要继续下传)和它的 sum (与1中的 sum 更新方法相同)

>>最后,将自己的 lazy 标记清空(否则会重复下传,造成答案错误)

“还有什么吗?”

还有最后一步,查询!

其实查询原理差不多,那就直接上查询代码啦!

```cpp
long long query(int id,int l,int r)
{
if(tree[id].l>r || tree[id].r<l) return 0;
if(tree[id].l>=l && tree[id].r<=r) return tree[id].sum;
if(tree[id].lazy>0) down(id);
return query(id*2,l,r)+query(id*2+1,l,r);/
}
```
“所以结合代码呢?”

蒟蒻怒目圆睁。

别急啊,这不就来了吗!
```cpp
#include<cstdio>
using namespace std;
int n,m,a[100010];
struct llk{
long long l,r,lazy,sum;
}tree[500010];
void build(int id,int l,int r){
tree[id].l=l;
tree[id].r=r;
if(l==r){
tree[id].sum=a[l];
return;
}
int mid=(l+r)/2;
build(id*2,l,mid);
build(id*2+1,mid+1,r);
tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;
}
void down(int id){
tree[id*2].lazy+=tree[id].lazy;
tree[id*2].sum+=(tree[id*2].r-tree[id*2].l+1)*tree[id].lazy;
tree[id*2+1].lazy+=tree[id].lazy;
tree[id*2+1].sum+=(tree[id*2+1].r-tree[id*2+1].l+1)*tree[id].lazy;
tree[id].lazy=0;
}
void update(int id,int l,int r,int v){
if(tree[id].l>r || tree[id].r<l) return;
if(tree[id].l>=l && tree[id].r<=r){
tree[id].lazy+=v;
tree[id].sum+=(tree[id].r-tree[id].l+1)*v;
return;
}
if(tree[id].lazy>0) down(id);
update(id*2,l,r,v);
update(id*2+1,l,r,v);
tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;
}
long long query(int id,int l,int r){
if(tree[id].l>r || tree[id].r<l) return 0;
if(tree[id].l>=l && tree[id].r<=r) return tree[id].sum;
if(tree[id].lazy>0) down(id);
return query(id*2,l,r)+query(id*2+1,l,r);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build(1,1,n);
for(int i=1;i<=m;i++){
int o,x,y;
scanf("%d",&o);
if(o==1){
int k;
scanf("%d%d%d",&x,&y,&k);
update(1,x,y,k);
}
else {
scanf("%d%d",&x,&y);
printf("%lld\n",query(1,x,y));
}
}
}
```
### 最后的故事

蒟蒻高兴地复制代码提交了。

喂,你要不要脸啊!

“怎么能诬陷人呢!这是我学了之后自己写的!”

蒟蒻骄傲地又写了一遍代码。

看来你学会了啊!

“嗯!我离 AK IOI 的梦想,又近了一步呢!”

# 全文·完

标签:lazy,01,int,线段,tree,算法,sum,id
From: https://www.cnblogs.com/firepaw/p/18002477

相关文章

  • day27 代码随想录算法训练营 40. 组合总和 II
    题目:40.组合总和II我的感悟:只要在路上就不怕走的慢。卡尔的视频慢慢听0.75倍听还是可以的。只要状态好,就可以学。多学会鼓励理解难点:代码难点:①notused[i-1]等同于used[i-1]==0 这里用的是True和False,所以用的是notused[i-1]②i>0为了防止i-1越界③剪枝......
  • 寒假碎碎念01
    让我想想这段时间都做了什么…周二之前的几天和实验室其他几个队员(chy、fa、lyy)还有教练讨论了一些对训练方式的改革,23号白天匆忙整理完PPT,晚上和大家讲了一下寒假训练的事项。周三周四做了一些题,一方面是想起来傅老师之前跟我说,“感觉你没有走出你的舒适圈”,于是就一拍脑门,加了......
  • 全源最短路径——Floyd算法
    目录问题引入思路一览算法原理代码部分问题引入给出一个含有n个点,m条边的图,如何快速的给出任意两个点的最短路径思路一览这个,感觉没啥思路,可能能想到的最暴力的解法就是对每一个点进行dfs遍历,在遍历过程中更新,但是这样的做法肯定是时间复杂度爆炸的;因此还是老算法Floyd香,Floyd......
  • Python 机器学习 K-近邻算法 K值的选择
     1、选择说明K-近邻算法通过查找测试数据点的K个最近的邻居来进行预测。这些邻居的类别(对于分类问题)或值(对于回归问题)用于决定测试点的类别或值。K是一个正整数,通常较小。1)避免过小的K值K值过小可能会导致模型过于复杂,容易受到数据中噪声的影响,从而导致过拟合。避免在K-近邻......
  • PAT乙级-1001(害死人不偿命的(3n+1)猜想)
    卡拉兹(Callatz)猜想:对任何一个正整数n,如果它是偶数,那么把它砍掉一半;如果它是奇数,那么把(3n+1)砍掉一半。这样一直反复砍下去,最后一定在某一步得到n=1。卡拉兹在1950年的世界数学家大会上公布了这个猜想,传说当时耶鲁大学师生齐动员,拼命想证明这个貌似很傻很天真的命题,结果......
  • 很好用的python游戏环境(续2):强化学习算法走迷宫游戏环境(导航问题 navigation):分享一个py
    相关前文:很好用的python游戏环境(续):强化学习算法走迷宫游戏环境(导航问题navigation):分享一个python语言的迷宫游戏环境项目的GitHub地址:https://github.com/Wonz5130/Maze_AIPS.这个游戏有个非常严重且致命的error,那就是单击这个游戏界面的时候会自动转成AI执行,否则就是人......
  • [NOIP2012 提高组] 借教室
    原题链接一道二分+差分的题目,作为学习前缀和和差分的引入题目非常合适。首先检验其单调性,如果一个申请人订单不用修改,那么其前面的申请人也不用修改,符合单调性。接着,这道题暴力的思路就很简单,但是看到运算量(n,m高达1e6),暴力的时间复杂度为O(n*m)显然超时。那么就是运用差分思想......
  • 洛谷 P8687 [蓝桥杯 2019 省 A] 糖果
    题意有\(m\)种口味,每次\(k\)颗一袋出售,给你\(n\)包均为\(k\)颗的糖果,求最少买几袋可以吃到所有口味的糖果。思路暴力对\(n\)包糖果做组合。如果找到其中一种包含了所有口味,将所有满足的方案取糖果包数最小即可。时间复杂度\(\mathcal{O(2^n)}\)。正解考虑状......
  • 很好用的python游戏环境(续):强化学习算法走迷宫游戏环境(导航问题 navigation):分享一个pyt
    相关:很好用的python游戏环境:强化学习算法走迷宫游戏环境(导航问题navigation):分享一个python语言的迷宫游戏环境前文分享了一个python下的maze游戏环境,本文再给出一个不错的实现项目,这个项目的实现更加的简单,并且可视化界面做的很好看,是用tkinter框架做的可视化:相关:迷宫游戏p......
  • Day01 GUI编程入门
    GUI编程入门告诉大家该怎么学?这是什么?它怎么玩?该如何去在我们平时运用?组件窗口弹窗面板文本框列表框按钮图片监听事件鼠标键盘事件破解工具1、简介Gui的核心技术:SwingAWT不流行的原因:​1.因为界面不美观。​2.需要jre环......