线段树入门(一)

来源:互联网 发布:通货膨胀率 知乎 编辑:程序博客网 时间:2024/06/05 14:13

接触线段树前我们先看一道比较经典的题目。

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;//返回答案    }}

这样我们就解决了这道题了

原创粉丝点击