ACM-数据结构-线段树I

来源:互联网 发布:中北大学软件工程学院 编辑:程序博客网 时间:2024/06/06 18:15

在ACM竞赛中,线段树是一种特殊的数据结构,总的来说它支持两种操作,一是更新,二是查询。当然,不使用线段树也能完成这两种操作,此时一般为线性复杂度O(n),所以使用它的目的其实也就是优化时间复杂度,更新和查询操作的复杂度都下降到了o(logn)。

其次,说说线段树的实现思想。总体上看,线段树将整个区间不断二分,直到区间上下界重合,最终形成一棵树,然后给每一个子区间从上到下、从左到右标上号,从这一步看来,线段树和一般的二叉树十分相似,唯一的区别就是线段树中的每一个节点保存的是一段区间的信息,所以整个线段树便可以用一个数组保存。接着就是最重要的一步,为每一个标上号的节点设计一个信息保存结构,用于存储和更新我们感兴趣的相关信息,比如和、极值等。最后,就是如何动态的去维护这些信息了。比如对于数组a[1,5,4,1,6],它有5个元素,由其建立的线段树及其维护的区间信息如下如下:


下面说说线段树解决的一般问题,针对于更新和查找操作,无非可以分为以下几类:1、点更新,查询区间;2、区间更新,查询点;3、区间更新,查询区间。

最后就需要具体编程来实现线段树这一数据结构了。虽然看起来线段树的用法可以有很多变形,比如其维护的信息就可以有多种选择,但是毕竟线段树支持的功能就只有更新和查询,所以一般的编写流程还是比较固定的。

1、设计线段树节点的数据结构,保存需要维护的区间信息

线段树的每一个节点都保存了一段区间信息,这一步就需要明确哪一些信息是自己感兴趣的,当然可能需要维护的信息不止一种,所以一般使用结构体来实现。还有一个问题就是对于一个有n个元素的数组,其线段树的节点有多少个呐,由于线段树将n不断分成两半,所以仔细分析一下可知,线段树大概不会超过4*n个节点。

const int MAXN = 1e5 + 5;struct SegTree{    int l, r;             // 当前节点对应的区间    LL lazy;              // 用于区间更新时使用    LL sum, maxs, mins;   // 需要维护的区间信息    inline int len() {return r-l+1;}}tree[4*MAXN];

2、接下来考虑建立线段树接的过程

建立线段树的方法,一般有三种:1、采用点更新的方式,每读入一个数据就更新一个;2、以范围为依据一直递归到叶子节点,过程中不断建立节点;3、另外一种自底向上的写法,速度最快。这里使用第二种,一般来说效率也不错了。这一步还需要理解两个步骤,一个就是递归,另一个就是线段树左右节点的确定。递归建树的过程容易理解,父节点、左右节点之间关系的确定的话,按照前面的标号规则,不难发现第i个节点的左孩子是第2*i个节点,右孩子是第2*i+1个节点。这里仅以维护区间和信息为例,其它信息的维护类似。

// 向上更新void PushUp(int id){    // 更新区间和信息    tree[id].sum = tree[lid].sum + tree[rid].sum;    // 也可以更新其它信息    // ......}

// 递归建树,调用build(1,1,n)即可void build(int id, int bl, int br){    // 到达叶子节点,输入初始化信息    if(bl == br)    {        // 维护区间和信息        scanf("%lld", &tree[id].sum);        // 也可以维护其它信息        // ......    }    else    {        int mid = (bl + br) >> 1;        // 递归建立左子树        build(lid, bl, mid);        // 递归建立右子树        build(rid, mid+1, br);        // 将信息更新给上层节点        PushUp(id);    }    // 维护区间范围信息    tree[id].l = bl;    tree[id].r = br;    tree[id].lazy = 0;}

3、实现更新节点信息操作

更新操作比较复杂,也比较繁琐,按线段树的设计功能来说,其大致可以分为下面几类:

1) 单点更新,这一类更新操作比较简单,几乎不用其它特殊的辅助标记就能完成

// 单点更新操作,将a[pos]的值改为v,调用update1(1,pos,v)即可void update1(int id, int pos, LL v){    int l = tree[id].l;    int r = tree[id].r;    // 到达叶子节点,更新信息    if(l == r)    {        // 进行修改值操作        tree[id].sum = v;        // 也可进行其它更新操作        // ......    }    else    {        // 判断需要更新的节点所处区间        int m = (l+r) >> 1;        // 向左子树走        if(pos <= m) update1(lid, pos, v);        // 向右子树走        else update1(rid, pos, v);        // 将信息更新给上层节点        PushUp(id);    }}
2) 区间更新,这一种操作比较复杂,不能使用线段树的原始定义直接进行更新操作,否则如果给出的更新区间接近于原始数组的长度,线段树的更新复杂度将退化到大于O(n),因为每一个叶子节点都被更新了。为了保持严格O(logn)的时间复杂度,这一步操作将引入一个特殊的标记,称为懒标记,也叫延迟标记,它的意思就是说当更新区间的时候,当满足当前节点的区间完全包含被更新区间的时候,就在当前节点上进行一个懒操作记录必要的信息,然后更新操作就不再继续向下进行了,即用父节点代为保存子节点的信息,借此已达到优化时间的目的。那如何保证下层节点维护信息的正确性呐,其实只要在当需要递归处理下层节点的时候,此时将当前节点的标记分解,传递给两个子节点进行计算即可。



// 向下更新void PushDown(int id){    if(!tree[id].lazy) return ;    // 更新懒标记、区间和信息    tree[lid].lazy += tree[id].lazy;    tree[rid].lazy += tree[id].lazy;    tree[lid].sum += tree[id].lazy * tree[lid].len();    tree[rid].sum += tree[id].lazy * tree[rid].len();    tree[id].lazy = 0;    // 也可以更新其它信息    // ......}

// 区间更新操作,将a[ul]-a[ur]的值增加v,调用update2(1,ul,ur,v)即可void update2(int id, int ul, int ur, LL v){    int l = tree[id].l;    int r = tree[id].r;    // 到达完全包含更新区间的节点,更新信息    if(l==ul && r==ur)    {        // 进行增加值操作        tree[id].lazy += v;        tree[id].sum += v * tree[id].len();        // 也可进行其它更新操作        // ......    }    else    {        // 向下更新信息        PushDown(id);        int mid = (l + r) >> 1;        // 向左子树走        if(ur <= mid) update2(lid, ul, ur, v);        // 向右子树走        else if(ul > mid) update2(rid, ul, ur, v);        // 向左、右子树走        else        {            update2(lid, ul, mid, v);            update2(rid, mid+1, ur, v);        }        // 向上更新信息        PushUp(id);    }}


4、最后实现查询操作

查询操作比较简单,只需要递归找到对应区间的节点即可。但是需要注意的是,如果同时还进行的是区间更新操作,那么此时查询操作也需要对懒标记进行处理。

// 查询操作,返回a[ql]-a[qr]的和,调用query(1,ql,qr)即可LL query(int id, int ql, int qr){    int l = tree[id].l;    int r = tree[id].r;    // 当前节点的区间完全包含在查询区间中,则返回节点区间和信息    if(l==ql && r==qr)    {        // 也可以返回其它信息        // ......        return tree[id].sum;    }    // 向下更新数据,有懒标记时有用    PushDown(id);    int mid = (l + r) >> 1;    // 向左子树走    if(qr <= mid) return query(lid, ql, qr);    // 向右子树走    if(ql > mid) return query(rid, ql, qr);    // 向左、右子树走    return query(lid, ql, mid) + query(rid, mid+1, qr);}

以一道例题为例,演示线段树的用法,POJ:3468,时空转移(点击打开链接),题目如下:

A Simple Problem with Integers
Time Limit: 5000MS Memory Limit: 131072KTotal Submissions: 73751 Accepted: 22726Case Time Limit: 2000MS

Description

You have N integers, A1A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.

Input

The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1A2, ... , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
"C a b c" means adding c to each of AaAa+1, ... , Ab. -10000 ≤ c ≤ 10000.
"Q a b" means querying the sum of AaAa+1, ... , Ab.

Output

You need to answer all Q commands in order. One answer in a line.

Sample Input

10 51 2 3 4 5 6 7 8 9 10Q 4 4Q 1 10Q 2 4C 3 6 3Q 2 4

Sample Output

455915

Hint

The sums may exceed the range of 32-bit integers.

Source

POJ Monthly--2007.11.25, Yang Yi

题意:

给出一个数组,对其有两种操作,一是将某范围内的元素都加上一个值,二是查询某范围内的元素和。

分析:

数组长度,以及操作次数都比较大,所以这是标准的线段树区间更新、查询区间和。

源代码:

#include <cstdio>#include <cstring>#define lid (id << 1)#define rid (id << 1 | 1)#define LL long longconst int MAXN = 1e5 + 5;struct SegTree{    int l, r;             // 当前节点对应的区间    LL lazy;              // 用于区间更新时使用    LL sum, maxs, mins;   // 需要维护的区间信息    inline int len() {return r-l+1;}}tree[4*MAXN];// 向上更新void PushUp(int id){    // 更新区间和信息    tree[id].sum = tree[lid].sum + tree[rid].sum;    // 也可以更新其它信息    // ......}// 向下更新void PushDown(int id){    if(!tree[id].lazy) return ;    // 更新懒标记、区间和信息    tree[lid].lazy += tree[id].lazy;    tree[rid].lazy += tree[id].lazy;    tree[lid].sum += tree[id].lazy * tree[lid].len();    tree[rid].sum += tree[id].lazy * tree[rid].len();    tree[id].lazy = 0;    // 也可以更新其它信息    // ......}// 递归建树,调用build(1,1,n)即可void build(int id, int bl, int br){    // 到达叶子节点,输入初始化信息    if(bl == br)    {        // 维护区间和信息        scanf("%lld", &tree[id].sum);        // 也可以维护其它信息        // ......    }    else    {        int mid = (bl + br) >> 1;        // 递归建立左子树        build(lid, bl, mid);        // 递归建立右子树        build(rid, mid+1, br);        // 将信息更新给上层节点        PushUp(id);    }    // 维护区间范围信息    tree[id].l = bl;    tree[id].r = br;    tree[id].lazy = 0;}// 单点更新操作,将a[pos]的值增加v,调用update1(1,pos,v)即可void update1(int id, int pos, LL v){    int l = tree[id].l;    int r = tree[id].r;    // 到达叶子节点,更新信息    if(l == r)    {        // 进行增加值操作        tree[id].sum += v;        // 也可进行其它更新操作        // ......    }    else    {        // 判断需要更新的节点所处区间        int m = (l+r) >> 1;        // 向左子树走        if(pos <= m) update1(lid, pos, v);        // 向右子树走        else update1(rid, pos, v);        // 将信息更新给上层节点        PushUp(id);    }}// 区间更新操作,将a[ul]-a[ur]的值增加v,调用update2(1,ul,ur,v)即可void update2(int id, int ul, int ur, LL v){    int l = tree[id].l;    int r = tree[id].r;    // 到达完全包含更新区间的节点,更新信息    if(l==ul && r==ur)    {        // 进行增加值操作        tree[id].lazy += v;        tree[id].sum += v * tree[id].len();        // 也可进行其它更新操作        // ......    }    else    {        // 向下更新信息,下放懒标记        PushDown(id);        int mid = (l + r) >> 1;        // 向左子树走        if(ur <= mid) update2(lid, ul, ur, v);        // 向右子树走        else if(ul > mid) update2(rid, ul, ur, v);        // 向左、右子树走        else        {            update2(lid, ul, mid, v);            update2(rid, mid+1, ur, v);        }        // 向上更新信息        PushUp(id);    }}// 查询操作,返回a[ql]-a[qr]的和,调用query(1,ql,qr)即可LL query(int id, int ql, int qr){    int l = tree[id].l;    int r = tree[id].r;    // 当前节点的区间完全包含在查询区间中,则返回节点区间和信息    if(l==ql && r==qr)    {        // 也可以返回其它信息        // ......        return tree[id].sum;    }    // 向下更新数据,有懒标记时有用    PushDown(id);    int mid = (l + r) >> 1;    // 向左子树走    if(qr <= mid) return query(lid, ql, qr);    // 向右子树走    if(ql > mid) return query(rid, ql, qr);    // 向左、右子树走    return query(lid, ql, mid) + query(rid, mid+1, qr);}int main(){//freopen("sample.txt", "r", stdin);    LL n, m;    while(~scanf("%lld%lld", &n, &m))    {        build(1, 1, n);        while(m--)        {            char op[2];            int a, b, c;            scanf("%s", op);            if(op[0] == 'Q')            {                scanf("%d%d", &a, &b);                printf("%lld\n", query(1, a, b));            }            else            {                scanf("%d%d%d", &a, &b, &c);                update2(1, a, b, c);            }        }    }    return 0;}

这里讨论的线段树是一维情况下的,但是i和树状数组一样,线段树也可以扩展到二维,详细信息可以去这里了解()。

0 0
原创粉丝点击