今天主要学习了一下线段树,之前学算法的时候,一直觉得线段树很恶心,毕竟内心告诉我线段树的代码太长了,就算我现在学,比赛中也记不起来,但是在我学习了并查集,动态规划,图论等一系列算法之后,我觉得其实程序员的世界只有一个真理,就是无论什么问题也好,只要有对的思维,你就能磕磕绊绊把解决问题的代码给写出来,写多了以后,慢慢的你会不自主的优化自己的代码。
长话短说,开始我们今天的主题,线段树的学习。
首先我们应该明白线段树的几大问题。
第一,什么是线段树?
第二,如果你学会了线段树你可以用它来解决什么问题
第三,在明白了线段树的重要性后,若是觉得需要,我们就要知道,线段树的构成。
我觉得:
第一:线段树从逻辑上来说其实就是二叉树。
从物理上来说其实就是数组。
第二:线段树就我目前所学到的,具体来说如果我题目要我对一个数组的不同区间进行加减乘除,然后统计不同区间的和差,那么线段树会是一个不错的选择。
第三:线段树是树,既然是树,那么就是由结点构成的,那么首先我们要明白的就是在线段树中这些结点代表着的意义,其意义就是结点存储的区间的和,这里我们就可以知道对于线段树的每一个结点,它都应该包含三个变量:区间的左端点,区间的右端点,和区间的和,
typedef long long ll;
typedef struct node
{
ll z;//区间的左端点
ll y;//区间的右端点
ll value;//区间的和
}tree;
由此,我们已经大概的了解了结点。
那么对于一道题而言,我们所需要做的就只有三件事;
1.利用已经储存好结点值的数组对线段树进行构造 – build()
2.对区间的值进行修改的同时,保证线段树依旧是有效的–change
3.对数组的各个区间的和进行有效的查询-- - find()
以洛谷的P3368为例子
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某区间每一个数数加上 xx;
求出某一个数的值。
输入格式
第一行包含两个整数 NN、MM,分别表示该数列数字的个数和操作的总个数。
第二行包含 NN 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。
接下来 MM 行每行包含 22 或 44个整数,表示一个操作,具体如下:
操作 11: 格式:1 x y k 含义:将区间 [x,y][x,y] 内每个数加上 kk;
操作 22: 格式:2 x 含义:输出第 xx 个数的值。
具体代码的代码实现和原理在注释里面已经说清楚在这里插入
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
typedef struct node
{
ll zuo,you;
ll sign;
ll value;
}tree;
const int N=5000000;
ll val[N];
ll n,m;
tree shu[N*4];
void ready(ll dian)// 让其孩子节点准备好
{
//准备好分两部分 一部分时value 另一部分是sign
//第一部分 value 更新左右节点的value
shu[dian*2].value+=shu[dian].sign*(shu[dian*2].you-shu[dian*2].zuo+1);
shu[dian*2+1].value+=shu[dian].sign*(shu[dian*2+1].you-shu[dian*2+1].zuo+1);
//第二部分 sign 左右孩子节点继承其sign 并消除根节点的sign
shu[dian*2].sign+=shu[dian].sign;
shu[dian*2+1].sign+=shu[dian].sign;
shu[dian].sign=0;
}
void build(ll dian,ll z,ll y)
{
ll mid;
shu[dian].zuo=z;shu[dian].you=y;
shu[dian].sign=0;
if(z==y){ shu[dian].value=val[z];
return;}
mid=(z+y)/2;
build(dian*2,z,mid);// 左节点
build(dian*2+1,mid+1,y);//右节点
shu[dian].value=shu[dian*2].value+shu[dian*2+1].value;
}
void change(ll dian,ll z,ll y,ll chan)
{
if(shu[dian].zuo>=z&&shu[dian].you<=y)//如果发生覆盖的情况
{
shu[dian].value+=chan*(shu[dian].you-shu[dian].zuo+1);shu[dian].sign+=chan; return ;
}
//如果没有发生覆盖,则只有两种情况有交集和没交集
if(shu[dian].sign!=0) //已经有标记了,那么我们需要对其孩子节点传入标记,因为我们需要往下遍历;
{
ready(dian);
}
//在孩子节点做好准备后,往下搜索
if(shu[dian*2].you>=z) change(dian*2,z,y,chan);//看是否跟左孩子的节点代表的区间有交集,若有往下遍历
if(shu[dian*2+1].zuo<=y) change(dian*2+1,z,y,chan);// 看是否跟有孩子的节代表的区间有交集,若有往下遍历
//当孩子节点做好更新后,对根节点更新,因为查询时从上到下
shu[dian].value=shu[dian*2].value+shu[dian*2+1].value;
}
ll find(ll dian,ll z,ll y) // 返回区间的和【z ,y】
{
// 若覆盖则不用向下遍历了,此节点就代表这个区间的和
if(shu[dian].zuo>=z&&shu[dian].you<=y) {return shu[dian].value;}
//如果要要向下查询跟更新一样,其孩子节点需要准备好即让他变成最终状态,判断其是否准备好就看根节点是否有sign是否大于0,如果大于零就说明没准备好,就需要我们向下传递sign,手动的帮他准备好,当他准备好了,我们就可以向下白遍历
if(shu[dian].sign!=0) {ready(dian);}
//孩子节点准备后之后,就需要检查一下我们要查询的集合和那个孩子节点所代表的区间有交集,若有交集我们就往那个有交集的子节点发展
ll sum=0;
if(shu[dian*2].you>=z) sum+=find(dian*2,z,y);
if(shu[dian*2+1].zuo<=y) sum+=find(dian*2+1,z,y);
return sum;
}
int main()
{
std::ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>val[i];
build(1,1,n);
ll p,a,b,c;
ll zhi;
for(int i=1;i<=m;i++)
{
cin>>p;
if(p==1){cin>>a>>b>>c;change(1,a,b,c);}
if(p==2){cin>>a; zhi=find(1,a,a);cout<<zhi<<endl;}
}
}
```若是有那么一点点收获,恳请给个赞