记录下我学习线段树的全部过程,希望各位看官发现错误能及时指出并且告诉我更多有关线段树的知识,万分感谢!
“线段树是竞赛出题人最喜欢考核的高级数据结构,熟练掌握线段树,标志着脱离了初级学习阶段,进入了中高级学习阶段” (狠狠期待一波!)
学习线段树之前先理解下,线段树可以理解为"分治法思想+二叉树结构+Lazy—Tag技术“
等会会解释,先记住线段树算法就是由这三种基础算法组成而来。
这是一个线段[1,10]的线段树结构
线段树顾名思义就是把一个完整的数组”分而治之“ ————> 分治法
特征:大区间的解可以从小区间的解合并而来
应用:
1、求区间最值问题 如是最值问题,上图中每一个椭圆形里面就放入它子树的最小值
2、求区间和问题 如是区间和问题,就放入所有的和即可
二叉树结构:就是一个线段二分二分再二分......(上面图已经贼清晰了)
综上所述,我们"仅"需要六步函数
1、build()构建树
2、push_up() 自下向上传递区间值(比如说上图中把[1,1] [2,2]线段值传给[1,2])
3、query() 查询 (比如想查询[1,6]那肯定需要找到[1,5]和[6,6]这两个节点啦)
4、update() 区间修改,每次改变[L,R]的值,当然需要一个专门的函数来修改
5、addtag() Lazy-Tag技术:比如每次我们修改[4,5]这个线段,我们只需要修改一次[4,5]这个线段,再往上传值就可以了,而不用继续往下搜索[4,4] [5,5] 但是我们需要在这个点上面添加一个tag = d这个标记
6、push_down() 由于第addtag这个函数,当我们修改[5,6]的时候,单独要进入[5,5],但是它没有被修改怎么办?(因为之前修改的是 [4,5])所以当我们再次来到这个节点的时候,就把这个tag标记分配给它的两个子树
函数这么多啊? 我要四了。。。。
没办法,毕竟这是高级数据结构。硬着头皮也要啃下来...
那么让我们拆开一个一个来解析吧...
第一步、build()函数构建这颗树
首先,我们一般用数组来表示二叉树(因为有很多结论嘛,而且省空间)
某节点p
左儿子 = 2*p;
右儿子 = 2*p + 1; (没有问题吧qaq)
//根节点是tree[1]
int tree[N<<2];
int ls(int p){return p<<1;}
int rs(int p){return p<<1|1;} //也可以 (p<<1) + 1;
其次我们来构建这颗树吧
void push_up(int p){
tree[p] = tree[ls(p)] + tree[rs(p)];//求区间和,让节点=左儿子+右儿子
//tree[p] = min(tree[ls(p)],tree[rs(p)]); //求最小值
}
void build(int p,int pl,int pr){
//如果深搜到底部,就让叶子节点等于这个值
if(pl==pr) {tree[p] = a[pl];return;}
//第二步,分而治之,把这个线段,分成[pl,mid] [mid+1,pr]这两个部分
int mid = (pl + pr) >> 1;
build(ls(p),pl,mid);
build(rs(p),mid+1,pr);
//自底向上传值
push_up(p);
}
突然想起来push_up已经被包含在build函数里面了,
所以直接上吧
第二步,query()查询函数来找到我们想要的区间和(或者最小值)
int query(int L,int R,int p,int pl,int pr){
if(L<=pl&&pr<=R) return tree[p];
//完全覆盖,不懂的话我举个栗子,我们想知道[1,6]这个区间和,当我们到了[1,5]这个节点直接返回即可
//然后再返回一个[6,6]节点
//分而治之
int mid = (pl+pr) >> 1;
if(L<=mid) res += query(L,R,ls(p),pl,mid); //举个例子,我们想要[4,5],
if(R>mid) res += query(L,R,rs(p),mid+1,pr);//[1,5]被分成了[1,3]和[4,5]显然我们只需要后者
return res;
}
我尽量把每段都解释在注释里面了,如果有不懂的,看一下应该就理解了吧?。。
第三步,创建update()等修改区间函数
void addtag(int q,int ql,int qr,int k)
{
tag[q] += k; //标记这个点有个tag标记为k,意味着它所有的儿子都被修改过了
tree[q] += (k*(qr-ql+1)); //[1,5]每个数字+1,这个节点+5
}
void push_down(int q,int ql,int qr)
{
if(tag[q]){ //如果被标记,就把这个标记给它的两个儿子
int mid = (ql+qr)>>1;
addtag(q,ql,mid,tag[q]);
addtag(q,mid+1,qr,tag[q]);
tag[q] = 0; //既然把这个标记给了它两个儿子,那它身上的标记就可以解除为0了
}
}
void update(int L,int R,int q,int ql,int qr,int k)
{
//函数功能,深搜找到所有被完全覆盖的区间
if(L<=ql&&R>=qr){
addtag(q,ql,qr,k);
return;
}
//如果不能覆盖,我们要判断,它有没有tag标记?
push_down(q,ql,qr);
//分而治之
int mid = (ql+qr)>>1;
if(L<=mid) update(L,R,ls(q),ql,mid);
if(R>mid) update(L,R,rs(q),mid+1,qr);
//因为这个值通过push_down里面的addtag值发生了改变,所以需要用push_up函数把值往上传递
push_up(q);
}
终于结束了,那么我把完整的代码再写一遍吧,
记住是六个函数,首先是build构建一棵树(初始化),
然后需要自下向上传值用push_up传值,
然后我们询问问题肯定深搜所有被完全覆盖的线段,建造query()函数,
然后我们修改区间的值的时候,构建update()函数,其中包含了push_down()以及addtag()两个小函数
void push_up(int q)
{
tree[q] = tree[ls(q)] + tree[rs(q)];
}
void build(int q,int ql,int qr)
{
if(ql==qr){tree[q]==a[ql];return;}
int mid = (ql+qr)>>1;
build(ls(q),ql,mid);
build(rs(q),mid+1,qr);
push_up(q);
}
void push_down(int q,int ql,int qr)
{
if(tag[q]){
int mid = (ql+qr)>>1;
addtag(ls(q),ql,mid,tag[q]);
addtag(rs(q),mid+1,qr,tag[q]);
tag[q] = 0;
}
}
int query(int L,int R,int q,int ql,int qr)
{
if(L<=ql&&R>=qr){
return tree[q];
}
push_down(q,ql,qr);
long long ans = 0;
int mid = (ql+qr)>>1;
if(L<=mid) ans += query(L,R,ls(q),ql,mid);
if(R>mid) ans += query(L,R,qs(q),mid+1,qr);
return ans;
}
void addtag(int q,int ql,int qr,int k)
{
tag[q] += k;
tree[q] += k*(qr-ql+1);
}
void update(int L,int R,int q,int ql,int qr,int k)
{
if(L<=pl&&R>=pr){
addtag(q,ql,qr,k);
return;
}
push_down(q,ql,qr);
int mid = (ql+qr) >> 1;
if(L<=mid) update(L,R,ls(q),ql,mid,k);
if(R>mid) update(L,R,rs(q),mid+1,qr,k);
push_up(q);
}
那么学习一个算法最最最最重要的时刻,当然是用来刷题呀。
One
题意:
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 k。
- 求出某区间每一个数的和。
第一行包含两个整数 n,mn,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 nn 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。
接下来 mm 行每行包含 33 或 44 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x,y][x,y] 内每个数加上 kk。2 x y
:输出区间 [x,y][x,y] 内每个数的和。
思路:这道题目,一碰到的时候,我想用树状数组做,因为我昨天刚刚做了一道树状数组的题目,收获还蛮大的-。-就是这是一道很简单的线段树模板题,没什么好说的了。
AC代码如下:
#include <iostream>
using namespace std;
const int N = 1e5+10;
int a[N];
long long tree[N<<2];
long long tag[N<<2];
int ls(int q){return q<<1;}
int rs(int q){return q<<1|1;}
void push_up(int q)
{
tree[q] = tree[ls(q)] + tree[rs(q)];
}
void build(int q,int ql,int qr)
{
if(ql==qr){
tree[q] = a[ql];
return;
}
int mid = (ql+qr ) >> 1;
build(ls(q),ql,mid);
build(rs(q),mid+1,qr);
push_up(q);
}
void addtag(int q,int ql,int qr,int k)
{
tag[q] += k;
tree[q] += k*(qr-ql+1);
}
void push_down(int q,int ql,int qr){
if(tag[q]){
int mid = (ql+qr) >> 1;
addtag(ls(q),ql,mid,tag[q]);
addtag(rs(q),mid+1,qr,tag[q]);
tag[q] = 0;
}
}
long long query(int L,int R,int q,int ql,int qr)
{
//深搜找到所有被包含的线段
if(L<=ql&&R>=qr){
return tree[q];
}
push_down(q,ql,qr);
int mid = (ql+qr) >> 1;
long long ans =0 ;
if(L<=mid) ans += query(L,R,ls(q),ql,mid);
if(R>mid) ans += query(L,R,rs(q),mid+1,qr);
return ans;
}
void update(int L,int R,int q,int ql,int qr,int k)
{
if(L<=ql&&R>=qr){
addtag(q,ql,qr,k);
return;
}
push_down(q,ql,qr);
int mid = (ql+qr) >> 1;
if(L<=mid) update(L,R,ls(q),ql,mid,k);
if(R>mid) update(L,R,rs(q),mid+1,qr,k);
push_up(q);
}
int main()
{
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
build(1,1,n);
while(m--){
int q,R,L;
long long k;
scanf("%d",&q);
if(q==1){
scanf("%d%d%lld",&L,&R,&k);
update(L,R,1,1,n,k);
}else{
scanf("%d%d",&L,&R);
cout << query(L,R,1,1,n) << endl;
}
}
return 0;
}
总结 : 最基本的线段树,就是模板~
Two
题意:输入包含多个测试用例,以EOF结束。
对于每个测试用例,第一行包含一个整数 N,表示有 N 艘邪恶的战舰排成一行。(1 <= N <= 100000)
第二行包含 N 个整数 Ei,表示从行的开始到结束每艘战舰的耐久值。你可以假设所有耐久值的总和小于 263。
下一行包含一个整数 M,表示操作和查询的数量。(1 <= M <= 100000)
接下来的 M 行,每行包含三个整数 T, X 和 Y。T=0 表示秘密武器的操作,它将减少 X 到 Y 之间(包括)的战舰的耐久值。T=1 表示指挥官的查询,询问 X 到 Y 之间(包括)的战舰耐久值之和。
思路:一开始拿到这道题,看到它修改每个值是进行sqrt开平方操作,所以我们只需要在update函数里面每次遇到ql==qr的时候进行 tree[q] = sqrt(tree[q]);所以可以减去lazy-tap这个操作优化。只需要判断tree[q] 是否等于 qr-ql+1 如果相等则直接返回即可。(因为区间里面都是1,无需修改)
另外一个思路,是把求最大值和求前缀和合并了,建立了两个线段树,
易错点:1、X和Y可能是X>Y,所以这时候要交换X,Y
2、不仅tree是long long型的,a[i]也是long long型的,我就是因为改long long的时候scanf("%d",&a[i])没有改成%lld,一直在WA......
#include <iostream>
using namespace std;
int n;
#include <cmath>
const int N = 1e5+10;
long long a[N];
long long tree[N<<2];
int ls(int q){return q<<1;}
int rs(int q){return q<<1 | 1;}
void push_up(int q)
{
tree[q] = tree[ls(q)] + tree[rs(q)];
}
void build(int q,int ql,int qr)
{
if(ql==qr){tree[q]=a[ql];return;}
int mid = (ql+qr) / 2;
build(ls(q),ql,mid);
build(rs(q),mid+1,qr);
push_up(q);
}
void update(int L,int R,int q,int ql,int qr)
{
//减少L~R的值
if(ql>R||qr<L||tree[q]==qr-ql+1) return;
if(ql==qr) return void(tree[q]=sqrt(tree[q]));
int mid = (ql+qr) / 2;
if(L<=mid) update(L,R,ls(q),ql,mid);
if(R>mid) update(L,R,rs(q),mid+1,qr);
push_up(q);
}
long long query(int L,int R,int q,int ql,int qr)
{
if(L<=ql&&R>=qr){
return tree[q];
}
int mid = (ql+qr) / 2;
long long sum = 0;
if(L<=mid) sum += query(L,R,ls(q),ql,mid);
if(R>mid) sum += query(L,R,rs(q),mid+1,qr);
return sum;
}
int main()
{
int T = 1;
while(scanf("%d",&n)!=EOF)
{
cout << "Case #"<<T<<":"<<endl;
T++;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
int m;cin>>m;
build(1,1,n);
while(m--)
{
int t,x,y;scanf("%d%d%d",&t,&x,&y);
if(x>y){
swap(x,y);
}
if(t==0){//减少x~y的耐久
update(x,y,1,1,n);
}else{//查询x~y的和
cout << query(x,y,1,1,n) << endl;
}
}
cout << endl;
}
return 0;
}
总结 : 小小的把两个运用结合在一起了,比如求最小值和求和,两者结合,就算是这道题的精髓~
Three
题意:
Yuanfang对下面的问题感到困惑:
有n个整数,a1, a2, …, an。它们的初始值都为0。有四种操作。
操作1:将c添加到ax和ay之间的每个数字(包括ax和ayk<---ak+c,其中k = x,x+1,…,y。
操作2:将c乘以ax和ay之间的每个数字。换句话说,进行转换ak<---ak×c,其中k = x,x+1,…,y。
操作3:将ax和ay之间的数字更改为c。换句话说,进行转换ak<---c,其中k = x,x+1,…,y。
操作4:获取ax和ay之间数字的p次幂之和。换句话说,获取axp+ax+1p+…+ay p的结果。
Yuanfang不知道该怎么做。所以他想要请你帮忙。
输入
不超过10个测试用例。
对于每个案例,第一行包含两个数字n和m,表示有n个整数和m个操作。1 <= n, m <= 100,000。
接下来的m行中,每行包含一个操作。操作1到3的格式为:"1 x y c"或"2 x y c"或"3 x y c"。操作4的格式为:"4 x y p"。(1 <= x <= y <= n, 1 <= c <= 10,000, 1 <= p <= 3)
输入以0 0结束。
输出
对于每个操作4,输出一个表示结果的整数,每行一个。答案可能很大。你只需要在除以10007后取余数。
思路:看了许久,最后还是根据大佬的做法写了写,大佬的两种方法。
总而言之,第一种是标准做法,是三重标记来做这道题。第二种做法有点取巧,没有被卡。是当某个区间的值全部相同的时候,那么这个区间+与*与=很快就能得出答案,如果q>=3的时候,可能第二种方法更好,(第二种方法就跟上面那道题有异曲同工之妙)所以可以先看第二种方法再去看第一种方法...(因为第一种我看了半个下午,捂脸🤦)
代码我就不放了,如果有需求可以直接去看大佬的代码-。-
不过我们可以学习到对懒标记的顺序以及时刻变化有深刻的理解
以及加强了第二种题目中对多次方以及开平方的求和问题...
Four