敲了四个小时的线段树qwq,终于算是把前两个比较基础的线段树板子给弄完了qwq
线段树一
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 kkk。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 n,mn, mn,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 nnn 个用空格分隔的整数,其中第 iii 个数字表示数列第 iii 项的初始值。
接下来 mmm 行每行包含 333 或 444 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x,y][x, y][x,y] 内每个数加上 kkk。2 x y
:输出区间 [x,y][x, y][x,y] 内每个数的和。
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
输入输出样例
5 5 1 5 4 2 3 2 2 4 1 2 3 2 2 3 4 1 1 5 1 2 1 4
11 8 20
说明/提示
对于 30%30\%30% 的数据:n≤8n le 8n≤8,m≤10m le 10m≤10。
对于 70%70\%70% 的数据:n≤103n le {10}^3n≤103,m≤104m le {10}^4m≤104。
对于 100%100\%100% 的数据:1≤n,m≤1051 le n, m le {10}^51≤n,m≤105。
保证任意时刻数列中任意元素的和在 [−263,263)[-2^{63}, 2^{63})[−263,263) 内。
【样例解释】
题解转自:
https://www.luogu.com.cn/problem/solution/P3372
一、简介线段树
pspsps: _此处以询问区间和为例。实际上线段树可以处理很多符合结合律的操作。(比如说加法,a[1]+a[2]+a[3]+a[4]=(a[1]+a[2])+(a[3]+a[4]))
线段树之所以称为“树”,是因为其具有树的结构特性。线段树由于本身是专门用来处理区间问题的(包括RMQRMQRMQ、RSQRSQRSQ问题等。
图片来源于互联网。
对于每一个子节点而言,都表示整个序列中的一段子区间;对于每个叶子节点而言,都表示序列中的单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息则是他的每一个子节点信息的整合。
有没有觉得很熟悉?对,线段树就是分块思想的树化,或者说是对于信息处理的二进制化——用于达到O(logn)O(logn)O(logn)级别的处理速度,logloglog以222为底。(其实以几为底都只不过是个常数,可忽略)。而分块的思想,则是可以用一句话总结为:通过将整个序列分为有穷个小块,对于要查询的一段区间,总是可以整合成kkk个所分块与mmm个单个元素的信息的并(0<=k,m<=n)(0<=k,m<=sqrt{n})(0<=k,m<=n
)。但普通的分块不能高效率地解决很多问题,所以作为logloglog级别的数据结构,线段树应运而生。
Extra Tipsmathcal{Extra Tips}Extra Tips
其实,虽然线段树的时间效率要高于分块但是实际上分块的总合并次数不会超过nsqrt{n}n
但是线段树在最坏情况下的合并次数显然是要大于这个时间效率的qwqqwqqwq。
但是毕竟也只是一个很大的常数而已
HoweverHoweverHowever,虽说如此,分块的应用范围还是要广于线段树的,因为虽然线段树好像很快,但是它只能维护带有结合律的信息,比如区间max/minmax/minmax/min、sumsumsum、xorxorxor之类的,但是不带有结合律的信息就不能维护(且看下文分解);而分块则灵活得多,可以维护很多别的东西,因为实际上分块的本质就是优雅的暴力qwq。
其实越暴力的算法可以支持的操作就越多、功能性就越强呐!你看n2n^2n2的暴力几乎什么都可以维护
二、逐步分析线段树的构造实现
1、建树与维护
由于二叉树的自身特性,对于每个父亲节点的编号iii,他的两个儿子的编号分别是2i2i2i和2i+12i+12i+1,所以我们考虑写两个O(1)O(1)O(1)的取儿子函数:
int n;
int ans[MAXN*4];
inline int ls(int p){return p<<1;}//左儿子
inline int rs(int p){return p<<1|1;}//右儿子
Extra Tipsmathcal{Extra Tips}Extra Tips
1、此处的inlineinlineinline可以有效防止无需入栈的信息入栈,节省时间和空间。
2、二进制位左移一位代表着数值∗2*2∗2,而如果左移完之后再或上111,由于左移完之后最后一位二进制位上一定会是000,所以∣1|1∣1等价于+1+1+1。
用二进制运算不是为了装X,相信我,会快的!
那么根据线段树的服务对象,可以得到线段树的维护:
void push_up_sum(int p){
t[p]=t[lc(p)]+t[rc(p)];
}// 向上不断维护区间操作
void push_up_min(int p){//max and min
t[p]=min(t[lc(p)],t[rc(p)]);
//t[p]=max(t[lc(p)],t[rc(p)]);
}
此处一定要注意,pushuppush uppushup操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。(这其实是正确性的证明啦)
呐,我们在这儿就能看出来,实际上pushuppush_uppushup是在合并两个子节点的信息,所以需要信息满足结合律!
那么对于建树,由于二叉树自身的父子节点之间的可传递关系,所以可以考虑递归建树(emmmmemmmmemmmm之前好像不小心剧透了qwqqwqqwq),并且在建树的同时,我们应该维护父子节点的关系:
void build(ll p,ll l,ll r)
{
if(l==r){ans[p]=a[l];return ;}
//如果左右区间相同,那么必然是叶子节点啦,只有叶子节点是被真实赋值的
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
//此处由于我们采用的是二叉树,所以对于整个结构来说,可以用二分来降低复杂度,否则树形结构则没有什么明显的优化
push_up(p);
//此处由于我们是要通过子节点来维护父亲节点,所以pushup的位置应当是在回溯时。
}
2、接下来谈区间修改
为什么不讨论单点修改呢qwqqwqqwq?因为其实很显然,单点修改就是区间修改的一个子问题而已,即区间长度为111时进行的区间修改操作罢了qwqqwqqwq
那么对于区间操作,我们考虑引入一个名叫“lazylazylazy tagtagtag”(懒标记)的东西——之所以称其“lazylazylazy”,是因为原本区间修改需要通过先改变叶子节点的值,然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达O(nlogn)O(nlogn)O(nlogn)的级别。但当我们引入了懒标记之后,区间更新的期望复杂度就降到了O(logn)O(logn)O(logn)的级别且甚至会更低.
(1)首先先来从分块思想上解释如何区间修改:
分块的思想是通过将整个序列分为有穷个小块,对于要查询的一段区间,总是可以整合成kkk个所分块与mmm个单个元素的信息的并(0<=k,m<=logn)(0<=k,m<=logn)(0<=k,m<=logn)(小小修改了一下的上面的前言qwqqwqqwq)
那么我们可以反过来思考这个问题:对于一个要修改的、长度为lll的区间来说,总是可以看做由一个长度为222^logloglog(⌊n⌋lfloor{n} floor{}⌊n⌋)和剩下的元素(或者小区间组成)。那么我们就可以先将其拆分成线段树上节点所示的区间,之后分开处理:
如果单个元素被包含就只改变自己,如果整个区间被包含就修改整个区间
其实好像这个在分块里不是特别简单地实现,但是在线段树里,无论是元素还是区间都是线段树上的一个节点,所以我们不需要区分区间还是元素,加个判断就好。
(2)懒标记的正确打开方式
首先,懒标记的作用是记录每次、每个节点要更新的值,也就是deltadeltadelta,但线段树的优点不在于全记录(全记录依然很慢qwq),而在于传递式记录:
** 整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果四环以内只修改了自己的话,那就只改变自己。**
After m{After}After that m{tha}tthat,如果我们采用上述的优化方式的话,我们就需要在每次区间的查询修改时pushdownpushdownpushdown一次,以免重复或者冲突或者爆炸qwqqwqqwq
那么对于pushdownpushdownpushdown而言,其实就是纯粹的pushuppushuppushup的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导lazylazylazy tagtagtag
那么问题来了:怎么传导pushdownpushdownpushdown呢?这里很有意思,开始回溯时执行pushuppushuppushup,因为是向上传导信息;那我们如果要让它向下更新,就调整顺序,在向下递归的时候pushdownpushdownpushdown不就好惹~qwqqwqqwq:
inline void f(ll p,ll l,ll r,ll k)
{
tag[p]=tag[p]+k;
ans[p]=ans[p]+k*(r-l+1);
//由于是这个区间统一改变,所以ans数组要加元素个数次啦
}
//我们可以认识到,f函数的唯一目的,就是记录当前节点所代表的区间
inline void push_down(ll p,ll l,ll r)
{
ll mid=(l+r)>>1;
f(ls(p),l,mid,tag[p]);
f(rs(p),mid+1,r,tag[p]);
tag[p]=0;
//每次更新两个儿子节点。以此不断向下传递
}
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
{
//nl,nr为要修改的区间
//l,r,p为当前节点所存储的区间以及节点的编号
if(nl<=l&&r<=nr)
{
ans[p]+=k*(r-l+1);
tag[p]+=k;
return ;
}
push_down(p,l,r);
//回溯之前(也可以说是下一次递归之前,因为没有递归就没有回溯)
//由于是在回溯之前不断向下传递,所以自然每个节点都可以更新到
ll mid=(l+r)>>1;
if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
//回溯之后
}
对于复杂度而言,由于完全二叉树的深度不超过lognlognlogn,那么单点修改显然是O(logn)O(logn)O(logn)的,区间修改的话,由于我们的这个区间至多分lognlognlogn个子区间,对于每个子区间的查询是O(1)O(1)O(1)的,所以复杂度自然是O(logn)O(logn)O(logn)不过带一点常数
3、那么对于区间查询
没什么好说的,由于是信息的整合,所以还是要用到分块思想,我实在是不想再码一遍了qwqqwqqwq
ll query(ll q_x,ll q_y,ll l,ll r,ll p)
{
ll res=0;
if(q_x<=l&&r<=q_y)return ans[p];
ll mid=(l+r)>>1;
push_down(p,l,r);
if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
return res;
}
最后贴高清无码的标程:
(还有,输入大数据一定不要用不加优化的cin/cout啊)
#include<bits/stdc++.h> using namespace std; typedef unsigned long long ll; int n,m; ll ans[502000],tag[502000],tree[502000],a[502000]; void pushup(ll p) { tree[p]=tree[2*p]+tree[2*p+1]; } void build(ll l,ll r,ll p) { tag[p]=0; if(l==r) { tree[p]=a[l]; return; } ll mid=(l+r)/2; build(l,mid,2*p); build(mid+1,r,2*p+1); pushup(p); } void addtag(ll p,ll l,ll r,ll k) { tag[p]+=k; tree[p]+=k*(r-l+1); } void tagdown(ll p,ll l,ll r) { ll mid=(l+r)/2; addtag(2*p,l,mid,tag[p]); addtag(2*p+1,mid+1,r,tag[p]); tag[p]=0; } void add(ll x,ll y,ll l,ll r,ll p,ll k) { if(x<=l&&y>=r) { tree[p]+=k*(r-l+1); tag[p]+=k; return; } tagdown(p,l,r); ll mid=(l+r)/2; if(x<=mid) add(x,y,l,mid,2*p,k); if(y>mid) add(x,y,mid+1,r,2*p+1,k); pushup(p); } ll find(ll x,ll y,ll l,ll r,ll p) { ll rest=0; if(x<=l&&y>=r) return tree[p]; ll mid=(l+r)/2; tagdown(p,l,r); if(x<=mid) rest+=find(x,y,l,mid,2*p); if(y>mid) rest+=find(x,y,mid+1,r,2*p+1); return rest; } int main() { cin>>n>>m; for(ll i=1;i<=n;i++) cin>>a[i]; build(1,n,1); while(m--) { ll opt; cin>>opt; if(opt==1) { ll a,b,c; cin>>a>>b>>c; add(a,b,1,n,1,c); } else{ ll a,b; cin>>a>>b; cout<<find(a,b,1,n,1)<<' '; } } return 0; }
线段树二
题目描述
如题,已知一个数列,你需要进行下面三种操作:
-
将某区间每一个数乘上 xxx
-
将某区间每一个数加上 xxx
-
求出某区间每一个数的和
输入格式
第一行包含三个整数 n,m,pn,m,pn,m,p,分别表示该数列数字的个数、操作的总个数和模数。
第二行包含 nnn 个用空格分隔的整数,其中第 iii 个数字表示数列第 iii 项的初始值。
接下来 mmm 行每行包含若干个整数,表示一个操作,具体如下:
操作 111: 格式:1 x y k
含义:将区间 [x,y][x,y][x,y] 内每个数乘上 kkk
操作 222: 格式:2 x y k
含义:将区间 [x,y][x,y][x,y] 内每个数加上 kkk
操作 333: 格式:3 x y
含义:输出区间 [x,y][x,y][x,y] 内每个数的和对 ppp 取模所得的结果
输出格式
输出包含若干行整数,即为所有操作 333 的结果。
输入输出样例
5 5 38 1 5 4 2 3 2 1 4 1 3 2 5 1 2 4 2 2 3 5 5 3 1 4
17 2
说明/提示
【数据范围】
对于 30%30\%30% 的数据:n≤8n le 8n≤8,m≤10m le 10m≤10
对于 70%70\%70% 的数据:n≤103n le 10^3 n≤103,m≤104 m le 10^4m≤104
对于 100%100\%100% 的数据:n≤105 n le 10^5n≤105,m≤105 m le 10^5m≤105
除样例外,p=571373p = 571373p=571373
(数据已经过加强^_^)
样例说明:
故输出应为 171717、222( 40 mod 38=240 mod 38 = 240mod38=2 )
题解转自:https://www.luogu.com.cn/problem/solution/P3373
这篇题解能帮助我什么?
1.这篇题解是帮助大家理解先乘后加的,很多人抄完题解就走啦并没有理解为什么要先乘后加。
2.这篇题解的代码与线段树1十分相像,可以帮助大家调试。
3.(附加功能)本篇题解力求轻松幽默老少皆宜。
前言:
所谓先乘后加就是在做乘法的时候把加法标记也乘上这个数,在后面做加法的时候直接加就行了。
先乘后加可(金)好(坷)啦(垃)好处都有啥?谁说对了就给他!
先乘后加可(金)好(坷)啦(垃),一题能顶两题啦!
先乘后加可(金)好(坷)啦(垃),NOIP一千八!
先乘后加啦,时间不⽩撒;先加后乘呀,撒了也⽩搭
先乘后加可(金)好(坷)啦(垃),不费时~!不怕Wa~!
出题人,真不傻!时间给了他,对竞赛体验危害大,绝不能给他!
线段树2毒(不)瘤(发)啊(达),我们都要切(支)掉(援)他。先乘后加,毒(你)瘤(们)数(日)据(本)别~!想~!啦~!
锹黑板:
首先我们回忆一下线段树1的加法标记他其实是打在父亲节点上的标记儿子加多少的,打完标记的同时父亲的sum其实已经加上了add∗lenadd*lenadd∗len
那我们回到这道题我们发现题目要求在加数的同时还要区间乘
比如现在有3个数1,2,31,2,31,2,3
1~3(1)
/
1~2(2) 3(3)
/
1(4) 2(5)
我们先给1~3加上2,画个小小小小的图,节点后面的括号代表节点下标
所以
t[1].add+=2;t[1].add+=2;t[1].add+=2;
t[1].sum+=((3−1)+1)∗2;t[1].sum+=((3-1)+1)*2;t[1].sum+=((3−1)+1)∗2;
我们再给1~3乘上3
所以
t[1].mu∗=3;t[1].mu*=3;t[1].mu∗=3;
我们再给1~3加上4,那是不是先加再乘
t[1].add+=4;t[1].add+=4;t[1].add+=4;
obviously我们发现不能先加:
操作2之后的式子是:
sum=(a[1]+2)*3+(a[2]+2)*3+(a[3]+2)*3;
如果直接加:
式子是:
sum=(a[1]+2+4)*3+(a[2]+2+4)*3+(a[3]+2+4)*3;
=(a[1]+2)*3+4*3+(a[2]+2)*3+4*3+(a[3]+2)*3+4*3;
我们发现这和
sum=(a[1]+2)*3+4+(a[2]+2)*3+4+(a[3]+2)*3+4;
并不等价
而要等价必须这样
sum=(a[1]+2+4/3)*3+(a[2]+2+4/3)*3+(a[3]+2+4/3)*3;
我们发现这样就成了实数运算了,还有可能除成无限小数
而先乘后加:
sum=(a[1]*3+2*3+4)+(a[2]*3+2*3+4)+(a[3]*3+2*3+4);
嗯 老铁没毛病~~~
#include<bits/stdc++.h> using namespace std; typedef long long ll; struct node{ ll tree,a,jia,cheng; }t[5020000]; ll n,m,q; void pushup(ll p) { t[p].tree =(t[2*p].tree +t[2*p+1].tree )%q; } void build(ll l,ll r,ll p) { t[p].jia =0; t[p].cheng =1; if(l==r) { t[p].tree =t[l].a%q ; return; } ll mid=(l+r)/2; build(l,mid,2*p); build(mid+1,r,2*p+1); pushup(p); } void addtag(ll l,ll r,ll p,ll cheng,ll jia) { /*t[p].tree =t[p].tree *t[p].cheng +(r-l+1)*t[p].jia ;*/ t[p].cheng =(t[p].cheng *cheng)%q; t[p].jia =(t[p].jia *cheng+jia )%q; t[p].tree =(t[p].tree */*t[p].*/cheng +(r-l+1)*/*t[p].*/jia%q)%q ; } void tagdown(ll p,ll l,ll r) { ll mid=(l+r)/2; addtag(l,mid,2*p,t[p].cheng ,t[p].jia ); addtag(mid+1,r,2*p+1,t[p].cheng ,t[p].jia ); t[p].cheng =1; t[p].jia =0; } void cheng(ll x,ll y,ll l,ll r,ll p,ll k) { if(x<=l&&y>=r) { t[p].cheng =(t[p].cheng *k)%q; t[p].jia =(t[p].jia *k)%q; t[p].tree =(t[p].tree *k)%q; return; } tagdown(p,l,r); ll mid=(l+r)/2; if(x<=mid) cheng(x,y,l,mid,2*p,k); if(y>mid) cheng(x,y,mid+1,r,2*p+1,k); pushup(p); } void jia(ll x,ll y,ll l,ll r,ll p,ll k) { if(x<=l&&y>=r) { t[p].jia=(t[p].jia +k)%q; t[p].tree =(t[p].tree +(r-l+1)*k )%q; return; } tagdown(p,l,r); ll mid=(l+r)/2; if(x<=mid) jia(x,y,l,mid,2*p,k); if(y>mid) jia(x,y,mid+1,r,2*p+1,k); pushup(p); } ll find(ll x,ll y,ll l,ll r,ll p) { ll re=0; if(x<=l&&y>=r) { return t[p].tree ; } tagdown(p,l,r); ll mid=(l+r)/2; if(x<=mid) re=(re+find(x,y,l,mid,2*p))%q; if(y>mid) re=(re+find(x,y,mid+1,r,2*p+1))%q; return re; } int main() { cin>>n>>m>>q; for(int i=1;i<=n;i++) cin>>t[i].a ; build(1,n,1); while(m--) { ll opt;ll x,y,k; cin>>opt; if(opt==1) { cin>>x>>y>>k; cheng(x,y,1,n,1,k); } else if(opt==2) { cin>>x>>y>>k; jia(x,y,1,n,1,k); } else { cin>>x>>y; cout<<find(x,y,1,n,1)<<' '; } /*for(int i=1;i<=n;i++) { cout<<t[i].cheng <<" "<<t[i].jia <<" "<<t[i].tree <<endl; cout<<"----"<<endl; } cout<<"#########"<<endl;*/ } return 0; }
最后,代码来自勤劳可爱的我呐(*╹▽╹*)
---------end----------