【説明する】线段树
前言
终于是学完了线段树!根据多为dalao的博客以及悉心教导,终于掌握了一些基本的应用~
但是问题是:刚刚接触的朋友们甚至都不明白什么是线段树,所以更加不会写代码咯(蠢问题!)
所以先来扯一下线段树是一个什么样的东西呗:
如果实在看不懂的话,可以直接去
(Ps:这是两种不同方法哦,讲真会一种比较不错了哈~)
不容置疑,线段树是一种数据结构,数据结构在我看来就是优化的一种方式咯.
引例:
给出n个数,n<=一个数,和m个询问,每次询问区间[l,r]的和,并输出.
一般思路,先暴力。就挨个枚举呗。可是当这个数很大的时候呢,就容易超时.这时我们就需要想一个数据结构来优化我们的算法咯.
所以就出现了线段树这个鬼东西.
一.概念:
什么是线段树呢?通俗易懂!!!不就是说在一棵树上存着线段咯.(大概这样比较好理解?)
其实线段树是一棵二叉搜索树.
以前我们树上的每一个结点存的是当前结点的权值,但是现在,我们的每一个结点上存的是每一个区间的信息
(注意:这个信息可以是多种多样的!譬如可以是当前区间之内的权值之和,之差(我才不会说我不会之差),最大值,最小值等等一系列恶心人的东西)
1)线段树是一个高效处理动态区间的查询问题.
2)主要思想:二分.(较为模板?)
3)每个节点以结构体的方式存储,结构体包含以下几个信息:
区间左端点跟右端点(这两者必有)
这个区间要维护的信息(事实际情况而定,数目不等).
4)线段树的几种用法: 建树 单点查询 单点修改 区间查询 区间修改
5)线段树的基本结构:
如图,我们要对区间0--5进行一系列访问,结点1存0--5区间的信息,然后将这个区间二分为0--2和3--5,它的左孩子存0--2区间的信息,右孩子存3--5区间的信息,然后依次这样推下去....
6)特殊性质:
由上图可得:
(mid是大概位于中间的结点坐标)
1、每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]
2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质
二.线段树的几种基本操作:
在接下来讲到的结构体中以给出结构体为结构体(什么鬼)
在这里还需要引用一个东西就是懒标记!在结构体中名称为'f'.
ctrl略麻烦就自己手动看一下dalao的博客吧
Orz(我才不是链接╭(╯^╰)╮)
在这里仅仅贴出懒标记代码:
//懒标记下传 inline void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; //记住这里需要进行清空 //因为这个懒标记已经传下去了,如果不清0后面再用这个懒标记时会重复下传,所以需要清0 }
/* PS:接下来可能会出现到的inline仅仅是用于实现的关键字,不懂得请手动请教度娘~ */
struct Segment_Tree{ int l,r; //左右端点 int w; //该点的值(在不特别点出说明时,表示该区间的权值和) int f; //懒标记 }tree[M*4]; //需要开4倍空间,具体原因不明(M为给出点的数据范围)
1、建树
即建立一棵线段树
思路:
a.对于二分到的每一个结点,给它的左右端点确定范围.
b.如果是叶子节点,存储要维护的信息.
c.状态合并.
代码:
//(sum)建树 inline void build(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) //与if(ld == rd)等价 { scanf("%d",&tree[k].w); //建树输入的点的权值 return ; } int m = (ld + rd) >> 1; // (>> 1) 等价于 (/2) build(k*2,ld,m); //建左子树 build(k*2+1,m+1,rd); //建右子树 tree[k].w=tree[k*2].w+tree[k*2+1].w; //sum储存(视题目而定) }
注意!!!!
a.结构体要开4倍空间,为什么的话..(dalao说自己画一个[1,10]的线段树就懂了呢!),我才不会说我还是没有明白的!
b.千万不要漏了return语句,因为到了叶子节点不需要再继续递归了.
2、单点查询
即查询一个点的状态,设待查询点为x
思路:
与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点.如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子
代码:
//单点查询 inline void ask_point(int k) { if(tree[k].l == tree[k].r) { printf("%d",tree[k].w); //输出第x号元素 return ; } if(tree[k].f) down(k); //将懒标记进行下传,避免出现上面拥有懒标记但是没有进行下传而查询的时候查询错误这种情况 int m = (tree[k].l+tree[k].r) >> 1; if(x <= m) ask_point(k*2); //查询左孩子 else ask_point(k*2+1); }
3、单点修改,即更改某一个点的状态。
用引例中的例子,对第x个数加上y
思路:
结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态.
代码:
//单点修改 inline void change_point(int k) { if(tree[k].l == tree[k].r) { tree[k].w+=y; //y是要修改的数x所需要加上的数 return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(x <= m) change_point(k*2); //修改左孩子 else change_point(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; //状态合并,此结点的w=两个孩子的w之和 }
4、区间查询
即查询一段区间的状态,在例中为查询区间[x,y]的和
//区间查询 //就是求一个区间的和,其实就是将他的子区间的和加起来 inline void ask_Q(int k) { if(tree[k].l >= a && tree[k].r <= b) //a是区间的左端点,b是区间右端点 { ans+=tree[k].w; //如果当前区间是在给出区间范围之内,直接询问该区间的值 return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) ask_Q(k*2); if(b > m) ask_Q(k*2+1); }
5、区间修改
即修改一段连续区间的值,我们以给区间[a,b]的每个数都加y为例讲解
//区间修改 inline void change_Q(int k) { if(tree[k].l >= a && tree[k].r <= b) //当前区间的全部都对于要修改的区间有用 { //(r-1)+1是代表区间点的总数 tree[k].w+=(tree[k].r-tree[k].l+1) * y; tree[k].f+=y; //更新懒标记 return ; } if(tree[k].f) down(k); //懒标记进行下传(只有不满足上面的if条件才执行),所以一定会用到当前结点的子结点 int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) change_Q(k*2); if(b > m) change_Q(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; //更新区间状态 }
6、查询区间最大最小值
即找出[a,b]区间之内的最大(小)值
因为在上面我们所用的w都代表着当前区间和,而不是最大或者最小值,所以我们在进行查询之前也要重新进行建树操作!
1)查询最大最小值建树操作
//建树 [求区间最大(小)值] //最大 inline void build_maxx(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld + rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=max(tree[k*2].w,tree[k*2+1].w); //其实对于之前代码仅仅更改这一个地方就行啦~ } //最小 inline void build_minn(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld + rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=min(tree[k*2].w,tree[k*2+1].w); }
2)查询操作
//查询区间最大(小)值 //最大 inline void ask_Q_maxx(int k) { if(tree[k].l >= a && tree[k].r <= b) //表示当前在查询的区间之内 { ans=max(ans,tree[k].w); //找最大值更新答案 return ; } if(tree[k].f) down(k); //懒标记下传 int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) ask_point(k*2); if(b > m) ask_point(k*2+1); } //最小 inline void ask_Q_minn(int k) { if(tree[k].l >= a && tree[k].r <= b) //表示当前在查询的区间之内 { ans=min(ans,tree[k].w); //找最小值更新答案(与最小值查询相比仅仅修改了这里) return ; } if(tree[k].f) down(k); //懒标记下传 int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) ask_point(k*2); if(b > m) ask_point(k*2+1); }
三.小总结
完整代码如下呦
#include <cstdio> #include <iostream> using namespace std; const int M = 1e5 + 1; int n,p,a,b,m,x,y,ans; struct Segment_Tree { int l,r; //左右端点 int w; //该点的值 int f; //懒标记 } tree[M*4]; //需要开4倍空间,具体原因不明 int max(int a,int b) { return a > b ? a : b; //重新定义max函数 } int min(int a,int b) { return a < b ? a : b; //重新定义min函数 } /* PS:inline仅仅是用于实现的关键字,不懂得请手动请教度娘~ */ //(sum)建树 inline void build(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) //与if(ld == rd)等价 { scanf("%d",&tree[k].w); //建树输入的点的权值 return ; } int m = (ld + rd) >> 1; // (>> 1) 等价于 (/2) build(k*2,ld,m); //建左子树 build(k*2+1,m+1,rd); //建右子树 tree[k].w=tree[k*2].w+tree[k*2+1].w; //sum储存(视题目而定) } //建树 [求区间最大(小)值] //最大 inline void build_maxx(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld + rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=max(tree[k*2].w,tree[k*2+1].w); } //最小 inline void build_minn(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld + rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=min(tree[k*2].w,tree[k*2+1].w); } //懒标记下传 inline void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; //记住这里需要进行清空 //因为这个懒标记已经传下去了,如果不清0后面再用这个懒标记时会重复下传,所以需要清0 } //单点查询 inline void ask_point(int k) { if(tree[k].l == tree[k].r) { printf("%d",tree[k].w); //输出第x号元素 return ; } if(tree[k].f) down(k); //将懒标记进行下传,避免出现上面拥有懒标记但是没有进行下传而查询的时候查询错误这种情况 int m = (tree[k].l+tree[k].r) >> 1; if(x <= m) ask_point(k*2); //查询左孩子 else ask_point(k*2+1); } //单点修改 inline void change_point(int k) { if(tree[k].l == tree[k].r) { tree[k].w+=y; //y是要修改的数x所需要加上的数 return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(x <= m) change_point(k*2); //修改左孩子 else change_point(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; //状态合并,此结点的w=两个孩子的w之和 } //区间查询 //就是求一个区间的和,其实就是将他的子区间的和加起来 inline void ask_Q(int k) { if(tree[k].l >= a && tree[k].r <= b) //a是区间的左端点,b是区间右端点 { ans+=tree[k].w; //如果当前区间是在给出区间范围之内,直接询问该区间的值 return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) ask_Q(k*2); if(b > m) ask_Q(k*2+1); } //区间修改 inline void change_Q(int k) { if(tree[k].l >= a && tree[k].r <= b) //当前区间的全部都对于要修改的区间有用 { //(r-1)+1是代表区间点的总数 tree[k].w+=(tree[k].r-tree[k].l+1) * y; tree[k].f+=y; //更新懒标记 return ; } if(tree[k].f) down(k); //懒标记进行下传(只有不满足上面的if条件才执行),所以一定会用到当前结点的子结点 int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) change_Q(k*2); if(b > m) change_Q(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; //更新区间状态 } //查询区间最大(小)值 //最大 inline void ask_Q_maxx(int k) { if(tree[k].l >= a && tree[k].r <= b) //表示当前在查询的区间之内 { ans=max(ans,tree[k].w); //找最大值更新答案 return ; } if(tree[k].f) down(k); //懒标记下传 int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) ask_point(k*2); if(b > m) ask_point(k*2+1); } //最小 inline void ask_Q_minn(int k) { if(tree[k].l >= a && tree[k].r <= b) //表示当前在查询的区间之内 { ans=min(ans,tree[k].w); //找最小值更新答案(与最小值查询相比仅仅修改了这里) return ; } if(tree[k].f) down(k); //懒标记下传 int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) ask_point(k*2); if(b > m) ask_point(k*2+1); } int main() { scanf("%d",&n); //输入n个结点 build(1,1,n); //建树 scanf("%d",&m); //执行m次操作 for(int i=1; i<=m; i++) { scanf("%d",&p); ans=0; //必须要清空答案 if(p == 1) { scanf("%d",&x); ask_point(x); //单点查询并输出第x个数 printf("%d\n",ans); } else if(p == 2) { scanf("%d%d",&x,&y); //对第x个数进行修改+y change_point(1); //单点修改 } else if(p == 3) { scanf("%d%d",&a,&b); //区间的左右端点 ask_Q(1); //区间查询 printf("%d\n",ans); } else if(p == 4) { scanf("%d%d%d",&a,&b,&y); change_Q(1); //区间修改 } else if(p == 5) { scanf("%d%d",&a,&b); ask_Q_maxx(1); //查询区间最大值 printf("%d\n",ans); } else if(p == 6) { scanf("%d%d",&a,&b); ans=0x7fffffff; //记住要将ans赋最大值! ask_Q_minn(1); //查询区间最小值 printf("%d\n",ans); } } return 0; }
四.练手题(附加代码)
1.线段树练习1
(codevs 1080)
#include <iostream> #include <cstdio> using namespace std; const int M = 1e5; int n,m,p,ans; int x,y,a,b; struct A{ int l,r; int w; int f; }tree[M * 4 + 1]; inline void build(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld+rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; //需要进行清空 //因为这个懒标记已经传下去了,所以如果不清0后面再用这个懒标记时会重复下传,所以需要清0 } inline void c1(int k) //单点修改 { if(tree[k].l == tree[k].r) { tree[k].w+=y; tree[k].f+=y; return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(x <= m) c1(k*2); else c1(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void c2(int k) //区间查询 { if(tree[k].l >= a && tree[k].r <= b) { ans+=tree[k].w; return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) c2(k*2); if(b > m) c2(k*2+1); } int main() { scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d",&p); if(p == 1) { scanf("%d%d",&x,&y); c1(1); } else { ans=0; scanf("%d%d",&a,&b); c2(1); printf("%d\n",ans); } } return 0; }
2.线段树练习2
(codevs 1081)
#include <iostream> #include <cstdio> using namespace std; const int M = 1e5; int n,m,p; int x,y,a,b,ans; struct B{ int l,r; int w; int f; }tree[M * 4 + 1]; inline void build(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld+rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; //需要进行清空 //因为这个懒标记已经传下去了,所以如果不清0后面再用这个懒标记时会重复下传,所以需要清0 } inline void c1(int k) //区间修改 { if(tree[k].l >= a && tree[k].r <= b) { tree[k].w+=(tree[k].r-tree[k].l+1)*y; tree[k].f+=y; return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) c1(k*2); if(b > m) c1(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void c2(int k) { if(tree[k].l == tree[k].r) { ans=tree[k].w; return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(x <= m) c2(k*2); else c2(k*2+1); } int main() { scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d",&p); ans=0; if(p == 1) { scanf("%d%d%d",&a,&b,&y); c1(1); } else { scanf("%d",&x); c2(1); printf("%d\n",ans); } } return 0; }
3.线段树练习3
(注意开long long!!!)
(codevs 1082)
区间修改+区间查询
#include <iostream> #include <cstdio> using namespace std; typedef long long LL; //将long long更换一个代码较短的名字 const int M = 2e5; int n,m,p; int x,y,a,b; LL ans; //ans需要开long long!!! struct C{ LL l,r; //这里需要特别注意!!!因为数据特大!!!所以需要开成long long LL w; LL f; }tree[M * 4 + 1]; inline void build(int k,int ld,int rd) { tree[k].l=ld,tree[k].r=rd; if(tree[k].l == tree[k].r) { scanf("%d",&tree[k].w); return ; } int m = (ld+rd) >> 1; build(k*2,ld,m); build(k*2+1,m+1,rd); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; //需要进行清空 //因为这个懒标记已经传下去了,所以如果不清0后面再用这个懒标记时会重复下传,所以需要清0 } inline void c1(int k) //区间修改 { if(tree[k].l >= a && tree[k].r <= b) { tree[k].w+=(tree[k].r-tree[k].l+1)*y; tree[k].f+=y; return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) c1(k*2); if(b > m) c1(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void c2(int k) { if(tree[k].l >= a && tree[k].r <= b) { ans+=tree[k].w; return ; } if(tree[k].f) down(k); int m = (tree[k].l+tree[k].r) >> 1; if(a <= m) c2(k*2); if(b > m) c2(k*2+1); } int main() { scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d",&p); ans=0; if(p == 1) { scanf("%d%d%d",&a,&b,&y); c1(1); } else { scanf("%d%d",&a,&b); c2(1); printf("%lld\n",ans); //用lld输出!!! } } return 0; }