ACM-数据结构-线段树I

在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: 131072K
Total Submissions: 73751 Accepted: 22726
Case 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 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

Sample Output

4
55
9
15

Hint

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

Source

题意:

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

分析:

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

源代码:

#include <cstdio>
#include <cstring>

#define lid (id << 1)
#define rid (id << 1 | 1)
#define LL long long

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];

// 向上更新
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和树状数组一样,线段树也可以扩展到二维,详细信息可以去这里了解()。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值