线段树模板题

洛谷传送门:线段树模板

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 k。
  2. 求出某区间每一个数的和。

输入格式

第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 [x,y] 内每个数加上 k。
  2. 2 x y:输出区间 [x,y] 内每个数的和。

输出格式

输出包含若干行整数,即为所有操作 2 的结果。

说明/提示

对于 30% 的数据:n≤8,m≤10。
对于 70%70% 的数据:n≤10^3,m≤10^4。
对于 100%100% 的数据:1≤n,m≤10^5。

保证任意时刻数列中所有元素的绝对值之和 ≤10^18。

正文来啦!

首先 考虑到这里用到了10的18次方量级,我们统一用long long类型存储数据,但是请注意,无脑用long long虽然一般不会数据溢出,但是需要知道的是他占用内存比int大,并且访问速度有差距

本文统一用ll代替long long 即using ll=long long;

本篇完全没有自定义结构体和指针的内容,请放心食用!

线段树,即把一条线段上存储的数据按照树的形式进行归纳,并且可以进行增删改查的数据结构。线段树的原型还是二叉树,虽然第一次看代码总归会有些手足无措,但是细细品味之后,每一个函数都非常形象,也很有意思。

首先,为什么要用线段树:以本题为例,暴力方法直接写的话无非就是录入数组,然后按区间加上k或者求和输出,很明显是O(n^2)的时间,但是如果我们采用树来存储,因为是二叉树,假设有n个数字,那么树深不会超过lg(n)。所以复杂度就是O(nlgn),对数级的优化,这真是个棒极了的结构,所以我们要学习。

那么接下来,让我们逐步来解开线段树神秘的面纱。

考虑到零基础的同学,先来展示一下完全二叉树的结构:

93c8479ce4d9297ff3102e90e0da5397.jpeg

这里 D E F G,最底层的节点,称之为“叶子节点”是我们用来存储真值的地方。也就是说,我们将要把输入的数字放在这一行,然后对其建树。对于这个题而言,我们是从上往下查找的,所以我们要考虑如何通过第一个A节点访问到B节点和C节点。我们约定A节点序数为1,顺序向下B C D E F G分别是 2 3 4 5 6 7,那么不难发现,对于一个节点的左子节点(比如说对于A的B节点,就是左子节点,为方便,我们约定称呼为左儿子,右儿子同理),节点i的左儿子是2i,右儿子是2i+1。

咖啡说:要有封装,所以,函数出现了。(函数其实就是一种代码的封装,使用频率高的代码我们都可以封装成为函数) 

ll ls(ll p){return p*2;}//p可以理解为position,即当前正在处理的节点位置
ll rs(ll p){return p*2+1;}

数据量大小上限我们给好常数,方便开数组:

const int N=100005;
ll a[N],ans[N*4],tag[N*4];

 接下来,我们来说明一下这里使用的三个数组的含义:

a[N]用来录入数据,即我们录入数据是直接录到a[N]中;

ans[N*4]用来储存树每个节点的和,比如B节点对应的ans就是D的值加E的值

tag[N*4]用来存放lazy tag(懒标记),我的朋友,姑且不要被这个新名词吓到,作用上,他是用来存放树之间数据传输的数据的。如果一股脑把他的作用定义直接灌给你,我的朋友,直接劝退。所以给我时间好吗,接下来会在多个函数中反复提到他,你会慢慢了解的。这是线段树独特的优化!延迟更新,其实这个思想在其他算法中也有体现,代码不同但又相通之处,比如说并查集查询的路径压缩,说的有的神经质了我的朋友,但是不要担心,众所周知斜体字都不重要

树开的内存一定要是原线段的四倍!!多了会MLE,少了会RE,我也不知道为什么,老前辈的经验之谈。

接下来我们来看三个操作:

操作一:传输 translate:刚刚提到的懒标记,在这里展现!

void translate(ll p,ll l,ll r,ll k){
tag[p]+=k;
ans[p]+=k*(r-l+1);
}

 朋友,都到这地步了

不得不提这里的tag数组了是吗(不忍脸),欸嘿,你看传参这里,l和r分别是左和右的意思,这里的左右代表边界,代表真值,即原先段的端点边界,很遗憾,还得您费力上去看那张二叉树示意图, DEFG分别对应原线段就是1 2 3 4,对应的是数组a的下标。这里的k,即传输改变的值,是期望原线段每个端点加的数,我们用tag数组把他存在当前端点了,好牛。接下来,我们会把他作为新的k继续传承下去,直到传送到叶子节点。为什么我们把他称为懒标记呢,是因为我们做出修改时,并不是即时传递到叶子节点的,而是后来在查改过程中慢慢顺道传递过去的。懒不要紧,我的朋友,最后大家都会到达终点

操作二:上传 push_up:很形象的名字,将一个节点的左右儿子的新信息上传到父节点。比如节点A B C,push_up(A)就是把B和C的信息传递上去,代码也很简单。这个函数用途更像是回溯,不过不是将状态回溯到之前,而是把子节点更新的数据反刍给父节点

void push_up(ll p){
ans[p]=ans[ls(p)]+ans[rs(p)];
}

操作三:下传: push_down :上传的逆操作,但是他的用途相对更加广泛,因为数据传入是树根传入的,所以他会负责数据的更新,但不负责树数据的统一,所以会用到push_up往回传,这也是为什么说push_up很像回溯。(心无旁骛的深入吧少年!无需关心你的身后

这里tag传值传下去之后就要清零了,预防之后多次计算。如果硬说规律的话,translate传递到儿子的时候就要清空当前tag标记;

void push_down(ll p,ll l, ll r){
ll mid=(l+r)/2;
translate(ls(p),l,mid,tag[p]);
translate(rs(p),mid+1,r,tag[p]);
tag[p]=0;
}

到了这里你是汗流浃背还是游刃有余呢,相信强大的你一定可以拿下线段树。

喝口水,拉伸一下,或者最大音量外放一首振奋人心的歌不要扰民,让我们继续!!前进!!!! 

建树!咖啡说,写代码得有电脑,所以用树存储得有树。

void build_tree(ll p,ll l,ll r){
if(l==r){
ans[p]=a[l];
return;
}
ll mid=(l+r)/2;
build_tree(ls(p),l,mid);
build_tree(rs(p),mid+1,r);
push_up(p);
}

 好的好的,相信你对这里的递归有些手足无措,但是我的朋友,相信好你封装的函数!咖啡说,如果只需要写伪代码,那就不需要程序员了。我的朋友,你已经封装好了代码。接下来我们来分析这段代码:

首先,如果l=r,那么就说明该节点麾下的叶子节点只有一个,这不就玻璃镜照着清泉水 ——嘴里不说他心里都明白嘛,这个节点就是叶子节点,只有叶子节点是存储真值的,l=r,不妨让ans[p]=a[l],这里ans[p]代表的是树里的叶子节点,a[l]代表是原线段的对应点,言尽于此,往下看。

左右分别延伸之后,在所有递归结束之后,遇到了这个push_up,oh真是美妙,这样叶子节点的数据就会一层一层随着push_up传到树根了,从而刷满整个树。

 

 

下面 ,是更新操作:update,因为我们原线段的数据已经全部整理到树上了,所以a数组我们直接弃之不用,直接在树上进行更高效率的增删改查!刚才用的好好的数组转手就扔掉,我都不敢想你以后怎么对哥们 

void update(ll p,ll l,ll r,ll nl,ll nr,ll k){
if(nl<=l&&r<=nr){
translate(p,l,r,k);
return;
}
push_down(p,l,r);
ll mid=(l+r)/2;
if(nl<=mid) update(ls(p),l,mid,nl,nr,k);
if(mid<nr) update(rs(p),mid+1,r,nl,nr,k);
push_up(p);
}

参数栏的两个新面孔,nl,nr可以理解成new l,new r,修改的区域边界。k是修改的值。

细心的你似乎发现了,如果当前节点包含的区间完全包含于更新区间中,就直接把更改值translate到当前节点上,这个值不一定立即传输到叶子节点,这是递归的出口,并无下一步操作,那么传输的值其实存储在当前节点的tag中了,妙吧,妙啊,第一个写出线段树的人一定是天才吧! 左右延申之前先下传确保懒标记更新,左右延申之后一样最后通过push_up把数据更新到根节点。这里为什么没有说更新整个树,因为有一些叶子节点并未更新

 

接下来,我们写查找函数:query:同样是从根出发,然后根据边界寻找对应的节点,不过query本质上是求和,所以要有返回值了。

ll query(ll p,ll l,ll r,ll nl,ll nr){
ll temp=0;
if(nl<=l&&r<=nr){
return ans[p];}
push_down(p,l,r);
ll mid=(l+r)/2;
if(nl<=mid) temp+=query(ls(p),l,mid,nl,nr);
if(nr>mid) temp+=query(rs(p),mid,r,nl,nr);
return temp;
}

芜湖,这就是最后一个函数了!在欢呼之前, 我先解释一下这段代码,nl,nr是查询的区间边界。temp用来存储临时数据,存储返回值,如果当前区间被查询区间完全覆盖,那就返回当前节点的ans值,值得注意的是,在左右延申之前,我们进行了下传操作,这是防止有懒标记没有传递到要查询的子节点。最后返回temp,就是查询的区间求和。

到此,所有龙珠(子函数)已经集齐,是时候召唤神龙(主函数)了!

#include <iostream>
using namespace std;
using ll = long long;

const int N = 100005;
ll a[N], ans[N * 4], tag[N * 4];
ll n, m, x, y, k;

ll ls(ll p) { return p * 2; }
ll rs(ll p) { return p * 2 + 1; }

void translate(ll p, ll l, ll r, ll k) // 传输
{
  tag[p] += k;
  ans[p] += k * (r - l + 1);
}

void push_up(ll p) // 上传
{
  ans[p] = ans[ls(p)] + ans[rs(p)];
}

void push_down(ll p, ll l, ll r) // 下传
{
  ll mid = (l + r) / 2;
  translate(ls(p), l, mid, tag[p]);
  translate(rs(p), mid + 1, r, tag[p]);
  tag[p] = 0;
}

void build_tree(ll p, ll l, ll r) // 建树
{
  if (l == r)
  {
    ans[p] = a[l];
    return;
  }
  ll mid = (l + r) / 2;
  build_tree(ls(p), l, mid);
  build_tree(rs(p), mid + 1, r);
  push_up(p);
}
void update(ll p, ll l, ll r, ll nl, ll nr, ll k)
{ // 更新
  if (nl <= l && r <= nr)
  {
    translate(p, l, r, k);
    return;
  }
  push_down(p, l, r);
  ll mid = (l + r) / 2;
  if (nl <= mid)
    update(ls(p), l, mid, nl, nr, k);
  if (mid < nr)
    update(rs(p), mid + 1, r, nl, nr, k);
  push_up(p);
}
ll query(ll p, ll l, ll r, ll nl, ll nr)
{ // 查询
  ll temp = 0;
  if (nl <= l && r <= nr)
  {
    return ans[p];
  }
  push_down(p, l, r);
  ll mid = (l + r) / 2;
  if (nl <= mid)
    temp += query(ls(p), l, mid, nl, nr);
  if (nr > mid)
    temp += query(rs(p), mid + 1, r, nl, nr);
  return temp;
}

int main()
{
  int choice;
  cin >> n >> m;
  for (int i = 1; i <= n; i++)
    cin >> a[i]; // 这里注意下标是从1开始,便于操作
  build_tree(1, 1, n);
  while (m--)
  {
    cin >> choice;
    if (choice == 1)
    {
      cin >> x >> y >> k;
      update(1, 1, n, x, y, k);
    }
    else
    {
      cin >> x >> y;
      cout << query(1, 1, n, x, y) << endl;
    }
  }
  return 0;
}

完结撒花//花朵脸请升级你的csdn版本以显示最新表情 //

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值