第一次了解线段树,虽然学长的讲解一下还是有点懵,后来在网上发现大家初次接触线段树都有类似的感觉。看来离了解各种算法和学好ACM还是有不小差距,继续加油!
一、什么是线段树
线段树叫“区间”更容易理解,线段树区间的起点终点通常都是整数。
这么看来:线段树其实就是一个简单的二叉树!特别在于,线段树的根是总线段的长度(区间),然后二分法把这棵树切成一次一次均匀地划分开,当然区间也越来越小……最后,每个树叶代表只有自己的一个数字的区间,线段树就完成了!
一张很直观的图:
从这张图中,我们也可以知道线段树的区间划分方法。比如(6,9)区间,划分方法就是先找到一个中间整数mid = (6 + 9)/2 = 7,(向下取整),然后就可以自然而然地把(6,9)分成(6, 7)和(7, 9)两个区间,从图上可以看到,它们是这个区间的左右结点。
说到这里,线段树就已经简单介绍完了。但可能你还是不明白:线段树到底有干什么用?
简单来说就是:线段树是一种数据结构,具有很多优良的性质!在编程时,解决一些问题就可以先表示成它的形式,又因为它的性质优良,可以简化运算(不那么暴力导致超时严重),或者把非常抽象的题目变得直观起来!
二、线段树的模板
刚才说了线段树是一种数据结构。因此在编程的时候,使用线段树也可以看成使用一个模板。
怎么看呢?我总结了,除了主函数,线段树一般要包括三个“特征”函数:第一个是build函数,用来构造整个线段树(比如刚才上图里那个)第二个是query函数,用来指定查找线段上一个小区间(依题目而定,比如求区间内所有数的和或者最大值)最后是update函数,用来对线段树进行“点”变化(就是修改特定的一个树叶和它影响到的其他部分,线段树的性质决定修改可以很快速!)
接下来,我要拿出学长给我们参考的线段树祖传模板和大家分享一下,不过在之前,我们还需要再看一次之前的那张线段树图。
我这次给所有位置都按顺序标记了下标,因为是标准二叉树所以:左结点下标 = 父结点下标*2,右结点下标 = 父结点下标 * 2 + 1。这是线段树很重要的性质。这意味着接下来模板中我们可以很轻松找到任何结点的左右子树!
模板是用线段树记录区间所有数的和。大家通过读这个模板代码,可以学到线段树的性质和基本使用方法。不太明白也可以记下来,之后只要用到线段树,都与这个模板万变不离其宗。
#include <iostream>
#include <stdio.h>
using namespace std;
typedef long long ll;
#define maxn 10 //元素总个数
int Sum[maxn << 2];//Sum求和,开四倍空间
int A[maxn+1], n;//存原数组下标[1,n]
//a<<x表示a的乘以x个2,a>>x表示a除以x个2
//PushUp函数更新节点信息,这里是求和
void PushUp(int rt) { Sum[rt] = Sum[rt << 1] + Sum[rt << 1 | 1]; }
//Build函数建立线段树
void Build(int l, int r, int rt) { //[l,r]表示当前节点区间,rt表示当前节点的实际存储位置
if (l == r) {//若到达叶节点
Sum[rt] = A[l];//存储A数组的值
return;
}
int m = (l + r) >> 1;
//左右递归
Build(l, m, rt << 1);
Build(m + 1, r, rt << 1 | 1);
//更新信息
PushUp(rt);
}
//假设A[L]+=C:
void Update(int L, int C, int l, int r, int rt) {//[l,r]表示当前区间,rt是当前节点编号//l,r表示当前节点区间,rt表示当前节点编号
if (l == r) {//到达叶节点,修改叶节点的值
Sum[rt] += C;
return;
}
int m = (l + r) >> 1;
//根据条件判断往左子树调用还是往右
if (L <= m) Update(L, C, l, m, rt << 1);
else Update(L, C, m + 1, r, rt << 1 | 1);
PushUp(rt);//子节点的信息更新了,所以本节点也要更新信息
}
int Query(int L, int R, int l, int r, int rt) {//[L,R]表示操作区间,[l,r]表示当前区间,rt:当前节点编号
if (L <= l && r <= R)
{
//在区间内直接返回
return Sum[rt];
}
int m = (l + r) >> 1;
//左子区间:[l,m] 右子区间:[m+1,r] 求和区间:[L,R]
//累加答案
int ANS = 0;
if (L <= m) ANS += Query(L, R, l, m, rt << 1);//左子区间与[L,R]有重叠,递归
if (R > m) ANS += Query(L, R, m + 1, r, rt << 1 | 1); //右子区间与[L,R]有重叠,递归
return ANS;
}
int main()
{
for (int i = 1; i <= maxn; i++)
A[i] = i ;
Build(1, maxn, 1);
cout << Query(3,5,1,maxn,1) << endl;
cout << Query(2, 5, 1, maxn, 1) << endl;
Update(2, 6, 1, maxn, 1);
cout << Query(3, 5, 1, maxn, 1) << endl;
cout << Query(2, 5, 1, maxn, 1) << endl;
system("pause");
return 0;
}
其中>>和<<是二进制运算符,也给出几个例子好了:(其实就相当于简单的乘除2)
3 << 1 = 3 * 2 = 6; 3 >> 1 = 3 / 2 = 1; 3 << 1 | 1 = 3 * 2 + 1 = 7;
三、简单例题
典型例题POJ3264 http://poj.org/problem?id=3264
一个模板会让你看不出来是模板,先去好好看看这道简单的线段树题的题干,然后再看这个代码……诶,为什么感觉好熟悉呢!!
这个是用线段树求区间的最大最小值。
例题代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 1000010
int n,a[N];
int ans_x, ans_y;
struct node
{
int r,l;
int maxx,minn;
}s[N<<2];
void build(int l, int r, int n)
{
s[n].l = l;
s[n].r = r;
s[n].maxx = 0;
s[n].minn = N;
if(l == r)
{
s[n].maxx = s[n].minn = a[l];
return;
}
int mid = (l + r) >> 1;//自上而下看,1需要先定义2、3,并根据本题要求收集两者中最值,递归!
build(l, mid, n<<1);//定义2,2中又会定义4、5..
build(mid + 1, r, n<<1|1);//定义3,3中又会定义6、7..
s[n].maxx = max(s[n<<1].maxx, s[n<<1|1].maxx);
s[n].minn = min(s[n<<1].minn, s[n<<1|1].minn);
//最终可以得到大小所有区间的最大最小值
}
void query(int l, int r, int n)//交互查询
{
if(s[n].l == l && s[n].r == r)//为了直观假设只有123。自上而下看,总区间的左右如果就是查询的左右,赋值即可
{
ans_x = max(ans_x, s[n].maxx);//考虑递归下去有左有右,因此保存能取到的最大值
ans_y = min(ans_y, s[n].minn);
return;
}
int mid = (s[n].l + s[n].r) >> 1;//区间不一致,树向下一层
if(r <= mid)//右端点小于中值,都在左边算,注意改变n
query(l, r, n<<1);
else if(l > mid)//都在右边算,同理
query(l, r, n<<1|1);
else//有左有右
{
query(l, mid, n<<1);
query(mid+1, r, n<<1|1);
}
}
int main()
{
int m,t,i,x,y;
while(~scanf("%d%d",&m,&t))
{
for(i = 1; i <= m; i ++)
scanf("%d",&a[i]);
build(1,m,1);//通过a[i]给叶赋值,建立线段树完成,得到每个区间的最大最小值
while(t--)
{
scanf("%d%d",&x,&y);
ans_x = 0;
ans_y = N;
query(x,y,1);//求啊求
printf("%d\n",ans_x-ans_y);
}
}
return 0;
}
第一次写博客当然是感觉写字比写代码简单,也就不厌其详地(为自己)写了很多注释。我觉得理解线段树的前提是要能理解一点递归法。这才是很多初学者难以接受线段树的主要原因。