接触线段树前我们先看一道比较经典的题目。
HDU 1166 敌兵布阵
这道题要求我们对一个区间内的单个值进行修改,并查询一段区间的和。
对于修改,我们很容易暴力的修改,复杂度为O(1),但查询的复杂度就变成了O(n),总复杂度为O(n*m),由于n和m都很大,所以这样肯定会超时的。
对于区间和我们常见的操作有前缀和,用前缀和可以将查询操作的复杂度降为O(1),但修改操作的复杂度则会升到O(n)。总复杂度也没有改变。
当这两种方法都无法满足题目的需求时,我们可以考虑是否可以将修改和查询的复杂度均摊一下,使总复杂度降低。
线段树解决这个问题比较好的工具。
什么是线段树
线段树是一种用树形来维护线性数据的一种数据结构。
一 般的线段树上的每一个节点T[a , b],代表该节点维护了原数列[ a , b ]区间的信息。对于每一个节点他至少有三个信息:左端点,右端点,我们需要维护的信息(在本题中我们维护区间和)。
struct segtree
{
int l,r;//左右端点
long long sum;//要维护的信息
}tree[maxn<<2];//一般开4*n防止爆空间
建立线段树
由于线段树是一个二叉树,而且是一个平衡二叉树,如果当前结点的编号是i,左端点为L ,右端点为 R , 那么左儿子的 编号为 i*2 ,左端点为 L ,右端点为 (L + R)/2 ; 同理右儿子的 编号为 i*2+1,左端点为(L+R)/2 ,右端点为 R)。如果当前结点的左端点等于右端点,那么该节点就是叶子节点,直接在该节点赋值即可。显然线段树是递归定义的。
void build(int root,int l,int r)
{
tree[root].l = l;tree[root].r = r;//初始化左右端点
if(l==r)//为叶子节点,直接赋值
tree[root].sum = a[l];
else
{
int mid = (l+r)>>1;
build(root<<1,l,mid);//建立左子树
build(root<<1|1,mid+1,r);//右子树
push_up(root);//从下往上维护线段树
}
}
从下往上更新线段树
void push_up(int x)
{
tree[x].sum = tree[x<<1].sum + tree[x<<1|1].sum;//因题目不同而不同
}
单点更新
单点更新的方法很好理解,如果目标更新节点在左儿子里,去左儿子中查找;反之,在右儿子中。不断递归,知道找到需要维护的节点,更新它,然后从下往上维护线段树回来。这就是维护的过程,代码如下:
void update(int x,int q,long long val)
{
int L=tree[x].l,R=tree[x].r;
if(L==q&&R==q)//叶子节点,直接更新
tree[x].num = val;
int mid = (L+R)>>1;
if(mid>=q) update(x<<1,l,r,val);//在左子树中
if(q>mid) update(x<<1|1,l,r,val);//在右子树中
push_up(x);//从下往上维护线段树
}
区间查询
题目中让我们查询区间求和,不难想到如果当前结点的区间完全被目标区间包含,直接返回当前结点的sum值,否则判断是否在左子树,右子树中,然后分别对左子树和右子树进行查询。具体过程通过以下代码理解:
long long query(int x,int l,int r)
{
int L=tree[x].l,R=tree[x].r;
if(l<=L&&R<=r)//完全在查询区间内
return tree[x].sum;//直接返回值
else
{
long long ans = 0;
int mid = (L+R)>>1;
if(mid>=l) ans += query(x<<1,l,r);//在左子树中有一部分
if(r>mid) ans += query(x<<1|1,l,r);//在右子树中有一部分
return ans;//返回答案
}
}
这样我们就解决了这道题了