洛谷传送门:线段树模板
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 k。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x,y] 内每个数加上 k。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),对数级的优化,这真是个棒极了的结构,所以我们要学习。
那么接下来,让我们逐步来解开线段树神秘的面纱。
考虑到零基础的同学,先来展示一下完全二叉树的结构:
这里 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版本以显示最新表情 //