线段树初步

第一次了解线段树,虽然学长的讲解一下还是有点懵,后来在网上发现大家初次接触线段树都有类似的感觉。看来离了解各种算法和学好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;
}

第一次写博客当然是感觉写字比写代码简单,也就不厌其详地(为自己)写了很多注释。我觉得理解线段树的前提是要能理解一点递归法。这才是很多初学者难以接受线段树的主要原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值