树状数组的简介和线段树基础操作

树状数组(用于每次只修改一个值的区间查询)

参考这个,还有这个,图是盗的。。。

这里A[] 是原数组,C[] 就是树状数组了。

由演变过程来看,这里运用了二分的思想。设节点编号为x,那么这个节点所管理的区间长度为 2^k (k为x二进制末尾0的个数)。所以 Cn = A(n - 2^k + 1) + ... + An

关于Lowbit(x) ,这里返回的是 x 用二进制表示时,最低位的1所表示的数,例如x的二进制1100100,返回的是100,就是十进制的4,也就是c[x]所管理的区间长度

感觉结合图跟模板不难懂

int n, m, a[MAXN], c[MAXN << 1];
//计算该点所管理的区间长度 2 ^ k
int Lowbit(int x)
{
    return x & (-x);
}
//修改
void change(int x, int num)
{
    while(x <= n)
    {
        c[x] += num;
        x += Lowbit(x);
    }
}
//查询
int sum(int x)
{
    int ans = 0;
    while(x > 0)
    {
        ans += c[x];
        x -= Lowbit(x);
    }
return ans; }

线段树

本文参考这篇(By 岩之痕)膜拜大佬

目录

1、线段树的简单操作(建树,点修改,查询)

2、区间修改(区间都加上 num, 区间都变成 num)

3、非递归线段树(此部分为原文加以排版以及我个人理解,写得真不错)

上图就是线段树的样子,给个简单的解释,例如这棵树是求区间和的话,那个 [1, 10]算的就是 [1, 10]的和

1、线段树的简单操作(建树,点修改,查询)

hdu1166

//建树,rt代表根,这里不懂的话手动结合上图模拟,从根开始
void Build(int l, int r, int rt)
{
    if(l == r)
    {
        scanf("%d", &sum[rt]);
        return ;
    }
    int m = (l + r) >> 1;
    Build(l, m, rt << 1);
    Build(m + 1, r, rt << 1 | 1);
    //更新sum
    sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
//修改,设原数组为 a ,即为 a[x] += num
void Update(int x, int num, int l, int r, int rt)
{
    if(l == r)
    {
        sum[rt] += num;
        return ;
    }
    int m = (l + r) >> 1;
    //看 x 在左区间还是右区间
    if(x <= m)
        Update(x, num, l, m, rt << 1);
    else
        Update(x, num, m + 1, r, rt << 1 | 1);
    sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
//查询
int Query(int L, int R, int l, int r, int rt)
{
    if(L <= l && r <= R)
        return sum[rt];
    int m = (l + r) >> 1;
    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);
    return ans;
}

上题题解

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cmath>
#include <sstream>
#include <algorithm>
#include <set>
#include <map>
#include <vector>
#include <queue>
#include <iomanip>
#include <stack>

using namespace std;

typedef long long LL;
const int INF = 0x3f3f3f3f;
const int MAXN = 50005;
const int MOD = 1e9 + 7;

#define MemI(x) memset(x, -1, sizeof(x))
#define Mem0(x) memset(x, 0, sizeof(x))
#define MemM(x) memset(x, 0x3f, sizeof(x))
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
int sum[MAXN << 2];
//建树,rt代表根,这里不懂的话手动结合上图模拟,从根开始
void Build(int l, int r, int rt)
{
    if(l == r)
    {
        scanf("%d", &sum[rt]);
        return ;
    }
    int m = (l + r) >> 1;
    Build(l, m, rt << 1);
    Build(m + 1, r, rt << 1 | 1);
    //更新sum
    sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
//修改,设原数组为 a ,即为 a[x] += num
void Update(int x, int num, int l, int r, int rt)
{
    if(l == r)
    {
        sum[rt] += num;
        return ;
    }
    int m = (l + r) >> 1;
    //看 x 在左区间还是右区间
    if(x <= m)
        Update(x, num, l, m, rt << 1);
    else
        Update(x, num, m + 1, r, rt << 1 | 1);
    sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
//查询
int Query(int L, int R, int l, int r, int rt)
{
    if(L <= l && r <= R)
        return sum[rt];
    int m = (l + r) >> 1;
    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);
    return ans;
}

int main()
{
    int T;
    scanf("%d", &T);
    for(int cas = 1;cas <= T;++cas)
    {
        printf("Case %d:
", cas);
        int n;
        scanf("%d", &n);
        Build(1, n, 1);
        char s[15];
        while(1)
        {
            scanf("%s", s);
            int x, y;
            if(s[0] == 'A')
            {
                scanf("%d%d", &x, &y);
                Update(x, y, 1, n, 1);
            }
            else if(s[0] == 'S')
            {
                scanf("%d%d", &x, &y);
                Update(x, -y, 1, n, 1);
            }
                else if(s[0] == 'Q')
            {
                scanf("%d%d", &x, &y);
                printf("%d
", Query(x, y, 1, n, 1));
            }
            else
                break;
        }
    }
    return 0;
}
View Code

后面一些线段树的具体题目,链接就不贴了。

2、区间修改(区间都变成 num,区间都加上num)

(1)区间都变成num

我这里定义了一个 book[] 数组作为标记, book[] 也是线段树,在这里存的是修改的值,则代表某一区间的 sum[x] = book[x] * (r - l + 1),这里基本上book[] 就照这个公式存值。如果 book[x] 被标记了,就代表该区间的值已经改变了,同时我们把 sum[x] 的值进行修正,但是!此时该区间的子区间的值没有被修正(对查询操作没有影响,已经得到所要区间的值sum[x])!之后,如果遍历该区间已经被标记了的话,就把该区间的两个子区间进行标记,同时去掉改区间的标记。(延迟下推,直到遍历到该区间才下推)

void Update(int L, int R, int num, int l, int r, int rt)
{
    if(L <= l && r <= R)
    {
        sum[rt] = num * (r - l + 1);
        book[rt] = num;
        return ;
    }
    int m = (l + r) >> 1;
    //看这个区间有没有被标记,延迟下推
    PushDown(rt, m - l + 1, r - m);
    if(L <= m)
        Update(L, R, num, lson);
    if(R > m)
        Update(L, R, num, rson);
    PushUp(rt);
}

void PushDown(int rt, int ln, int rn)
{
    if(book[rt])
    {
        //标记下推到该区间的两个子区间,同时删除该标记
        book[rt << 1] = book[rt << 1 | 1] = book[rt];
        sum[rt << 1] = ln * book[rt << 1];
        sum[rt << 1 | 1] = rn * book[rt << 1 | 1];
        book[rt] = 0;
    }
}

(2)区间都加上num

这个跟上面类似,上述代码稍微修改一下即可,此时book[] 代表的是增加值

POJ-3468

跟上面模板的不同:PushUp() 里面用的是 +=;

Update()里面用的是 +=;

Query()里面加了PushDown(),不加会错,因为查询的时候可能会下推标记区间

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cmath>
#include <sstream>
#include <algorithm>
#include <set>
#include <map>
#include <vector>
#include <queue>
#include <iomanip>
#include <stack>

using namespace std;

typedef long long LL;
const int INF = 0x3f3f3f3f;
const int MAXN = 100005;
const int MOD = 1e9 + 7;

#define MemI(x) memset(x, -1, sizeof(x))
#define Mem0(x) memset(x, 0, sizeof(x))
#define MemM(x) memset(x, 0x3f, sizeof(x))
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1

LL sum[MAXN << 2], book[MAXN << 2];
void PushUp(int rt)
{
    sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}

void PushDown(int rt, int ln, int rn)
{
    if(book[rt])
    {
        book[rt << 1] += book[rt];
        book[rt << 1 | 1] += book[rt];
        sum[rt << 1] += book[rt] * ln;
        sum[rt << 1 | 1] += book[rt] * rn;
        book[rt] = 0;
    }
}

void Build(int l, int r, int rt)
{
    if(l == r)
    {
        scanf("%lld", &sum[rt]);
        return ;
    }
    int m = (l + r) >> 1;
    Build(lson);
    Build(rson);
    PushUp(rt);
}

void Update(int L, int R, int num, int l, int r, int rt)
{
    if(L <= l && r <= R)
    {
        sum[rt] += num * (r - l + 1);
        book[rt] += num;
        return ;
    }
    int m = (l + r) >> 1;
    PushDown(rt, m - l + 1, r - m);
    if(L <= m)
        Update(L, R, num, lson);
    if(R > m)
        Update(L, R, num, rson);
    PushUp(rt);
}

LL Query(int L, int R, int l, int r, int rt)
{
    if(L <= l && r <= R)
        return sum[rt];
    int m = (l + r) >> 1;
    LL ans = 0;
    PushDown(rt, m - l + 1, r - m);
    if(L <= m)
        ans += Query(L, R, lson);
    if(R > m)
        ans += Query(L, R, rson);
    return ans;
}

int main()
{
    Mem0(book);
    Mem0(sum);
    int n, q;
    scanf("%d%d", &n, &q);
    Build(1, n, 1);
    char c[5];
    int l, r, num;
    while(q--)
    {
        scanf("%s%d%d", &c, &l, &r);
        if(c[0] == 'Q')
            printf("%lld
", Query(l, r, 1, n, 1));
        else
        {
            scanf("%d", &num);
            Update(l, r, num, 1, n, 1);
        }
    }
    return 0;
}
View Code

这里再贴出参考博客的解释,他的 Add[] 如同我的 book[]


线段树的区间修改也是将区间分成子区间,但是要加一个标记,称作懒惰标记。

标记的含义:
本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。
即,如果要给一个区间的所有值都加上1,那么,实际上并没有给这个区间的所有值都加上1,而是打个标记,记下来,这个节点所包含的区间需要加1.打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。
 
标记有相对标记和绝对标记之分:
相对标记是将区间的所有数+a之类的操作,标记之间可以共存,跟打标记的顺序无关(跟顺序无关才是重点)。
所以,可以在区间修改的时候不下推标记,留到查询的时候再下推。
      注意:如果区间修改时不下推标记,那么PushUp函数中,必须考虑本节点的标记。
                 而如果所有操作都下推标记,那么PushUp函数可以不考虑本节点的标记,因为本节点的标记一定已经被下推了(也就是对本节点无效了)
绝对标记是将区间的所有数变成a之类的操作,打标记的顺序直接影响结果,
所以这种标记在区间修改的时候必须下推旧标记,不然会出错。
 
注意,有多个标记的时候,标记下推的顺序也很重要,错误的下推顺序可能会导致错误。
 
之所以要区分两种标记,是因为非递归线段树只能维护相对标记。
因为非递归线段树是自底向上直接修改分成的每个子区间,所以根本做不到在区间修改的时候下推标记。
非递归线段树一般不下推标记,而是自下而上求答案的过程中,根据标记更新答案。
感谢几位网友指出了我的错误。
我说相对标记在Update时可以不下推,这一点是对的,但是原来的代码是错误的。
因为原来的代码中,PushUP函数是没有考虑本节点的Add值的,如果Update时下推标记,那么PushUp的时候,节点的Add值一定为零,所以不需要考虑Add。
但是,如果Update时暂时不下推标记的话,那么PushUp函数就必须考虑本节点的Add值,否则会导致错误。
为了简便,上面函数中,PushUp函数没有考虑Add标记。所以无论是相对标记还是绝对标记,在更新信息的时候,
到达的每个节点都必须调用PushDown函数来下推标记,另外,代码中,点修改函数中没有PushDown函数,因为这里假设只有点修改一种操作,
如果题目中是点修改和区间修改混合的话,那么点修改中也需要PushDown。

 3、非递归线段树(此部分为原文加以排版以及我个人理解。。。)

非递归的思路很巧妙,思路以及部分代码实现 来自  清华大学 张昆玮 《统计的力量》 ,有兴趣可以去找来看。
非递归的实现,代码简单(尤其是点修改和区间查询),速度快,建树简单,遍历元素简单。总之能非递归就非递归吧。
不过,要支持区间修改的话,代码会变得复杂,所以区间修改的时候还是要取舍。有个特例,如果区间修改,但是只需要
在所有操作结束之后,一次性下推所有标记,然后求结果,这样的话,非递归写起来也是很方便的。
下面先讲思路,再讲实现。

(1)点修改:

非递归的思想总的来说就是自底向上进行各种操作。回忆递归线段树的点修改,首先由根节点1向下递归,找到对应的叶节点,然后,修改叶节点的值,再向上返回,在函数返回的过程中,更新路径上的节点的统计信息。而非递归线段树的思路是,如果可以直接找到叶节点,那么就可以直接从叶节点向上更新,而一个节点找父节点是很容易的,编号除以2再下取整就行了。
那么,如何可以直接找到叶节点呢?非递归线段树扩充了普通线段树(假设元素数量为n),使得所有非叶结点都有两个子结点且叶子结点都在同一层。
来观察一下扩充后的性质:
可以注意到红色和黑色数字的差是固定的,如果事先算出这个差值,就可以直接找到叶节点。
注意:区分3个概念:原数组下标,线段树中的下标和存储下标。
原数组下标,是指,需要维护统计信息(比如区间求和)的数组的下标,这里都默认下标从1开始(一般用A数组表示)
线段树下标,是指,加入线段树中某个位置的下标,比如,原数组中的第一个数,一般会加入到线段树中的第二个位置,
为什么要这么做,后面会讲。
存储下标,是指该元素所在的叶节点的编号,即实际存储的位置。
【在上面的图片中,红色为原数组下标,黑色为存储下标】
有了这3个概念,下面开始讲区间查询。

(2)点修改下的区间查询:

首先,区间的划分没有变,现在关键是如何直接找到被分成的区间。原来是递归查找,判断左右子区间跟[L,R]是否有交点,
若有交点则向下递归。现在要非递归实现,这就是巧妙之处,见下图,以查询[3,11]为例子。
 
 
其实,容易发现,紫色部分的变化,跟原来分析线段树的区间分解的时候是一样的规则,图中多的蓝色是什么意思呢?
首先注意到,蓝色节点刚好在紫色节点的两端。
回忆一下,原来线段树在区间逐层被替代的过程中,哪些节点被留了下来?最左侧的节点,若为其父节点的右子节点,则留下。
最右侧的节点,若为其父节点的左子节点则留下。那么对于包裹着紫色的蓝色节点来看,刚好相反。
比如,以左侧的的蓝色为例,若该节点是其父节点的右子节点,就证明它右侧的那个紫色节点不会留下,会被其父替代,所以没必要在这一步计算,若该节点是其父节点的左子节点,就证明它右侧的那个紫色节点会留在这一层,所以必须在此刻计算,否则以后都不会再计算这个节点了。这样逐层上去,容易发现,对于左侧的蓝色节点来说,只要它是左子节点,那么就要计算对应的右子节点。同理,对于右侧的蓝色节点,只要它是右子节点,就需要计算它对应的左子节点。这个计算一直持续到左右蓝色节点的父亲为同一个的时候,才停止。于是,区间查询,其实就是两个蓝色节点一路向上走,在路径上更新答案。这样,区间修改就变成了两条同时向根走的链,明显复杂度O(log2(n))。并且可以非递归实现。
至此,区间查询也解决了,可以直接找到所有分解成的区间。
但是有一个问题,如果要查询[1,5]怎么办?[1]左边可是没地方可以放置蓝色节点了。
问题的解决办法简单粗暴,原数组的1到n就不存在线段树的1到n了,而是存在线段树的2到n+1,
而开始要建立一颗有n+2个元素的树,空出第一个和最后一个元素的空间。
现在来讲如何对线段树进行扩充。(个人笔记:这里的扩充具体是扩充至满二叉树,空余部分0补充)
再来看这个二叉树,令N=8;注意到,该树可以存8个元素,并且[1..7]是非叶节点,[8..15]是叶节点。
也就是说,左下角为N的二叉树,可以存N个元素,并且[1..N-1]是非叶节点,[N..2N-1]是叶节点。
并且,线段树下标+N-1=存储下标 (还记不记得原来对三个下标的定义)
这时,这个线段树存在两段坐标映射:
原数组下标+1=线段树下标
线段树下标+N-1=存储下标 
联立方程得到:原数组下标+N=存储下标
于是从原数组下标到存储下标的转换及其简单。
下一个问题:N怎么确定?
上面提到了,N的含义之一是,这棵树可以存N个元素,也就是说N必须大于等于n+2
于是,N的定义,N是大于等于n+2的,某个2的次方。

(3)区间修改下的区间查询:

方法之一:如果题目许可,可以直接打上标记,最后一次下推所有标记,然后就可以遍历叶节点来获取信息。
方法之二:如果题目查询跟修改混在一起,那么,采用标记永久化思想。也就是,不下推标记。
递归线段树是在查询区间的时候下推标记,使得到达每个子区间的时候,Sum已经是正确值。
非递归没法这么做,非递归是从下往上,遇到标记就更新答案。
这题是Add标记,一个区间Add标记表示这个区间所有元素都需要增加Add
Add含义不变,Add仍然表示本节点的Sum已经更新完毕,但是子节点的Sum仍需要更新.
现在就是如何在查询的时候根据标记更新答案。
观察下图:
左边的蓝色节点从下往上走,在蓝色节点到达[1,4]时,注意到,左边蓝色节点之前计算过的所有节点(即[3,4])都是目前蓝色节点的子节点也就是说,当前蓝色节点的Add是要影响这个节点已经计算过的所有数。多用一个变量来记录这个蓝色节点已经计算过多少个数,根据个数以及当前蓝色节点的Add,来更新最终答案。
更新完答案之后,再加上[5,8]的答案,同时当前蓝色节点计算过的个数要+4(因为[5,8]里有4个数)
然后当这个节点到达[1,8]节点时,可以更新[1,8]的Add.
这里,本来左右蓝色节点相遇之后就不再需要计算了,但是由于有了Add标记,左右蓝色节点的公共祖先上的Add标记会影响目前的所有数,所以还需要一路向上查询到根,沿路根据Add更新答案。

(4)区间修改:

这里讲完了查询,再来讲讲修改
修改的时候,给某个区间的Add加上了C,这个区间的子区间向上查询时,会经过这个节点,也就是会计算这个Add,但是
如果路径经过这个区间的父节点,就不会计算这个节点的Add,也就会出错。这里其实跟递归线段树一样,改了某个区间的Add
仍需要向上更新所有包含这个区间的Sum,来保持上面所有节点的正确性。

(5)非递归实现

以下以维护数列区间和的线段树为例,演示最基本的非递归线段树代码。

(0)定义

#define maxn 100007
int A[maxn],n,N;//原数组,n为原数组元素个数 ,N为扩充元素个数 
int Sum[maxn<<2];//区间和 
int Add[maxn<<2];//懒惰标记 

(1)建树:

void Build(int n){
    //计算N的值 
    N=1;while(N < n+2) N <<= 1;
    //更新叶节点 
    for(int i=1;i<=n;++i) Sum[N+i]=A[i];//原数组下标+N=存储下标
    //更新非叶节点 
    for(int i=N-1;i>0;--i){
        //更新所有非叶节点的统计信息 
        Sum[i]=Sum[i<<1]+Sum[i<<1|1];
        //清空所有非叶节点的Add标记 
        Add[i]=0;
    }
} 

(2)点修改:

void Update(int L,int C){
    for(int s=N+L;s;s>>=1){
        Sum[s]+=C;
    }
} 

(3)点修改下的区间查询:

求A[L..R]的和(点修改没有使用Add所以不需要考虑)
代码非常简洁,也不难理解,
s和t分别代表之前的论述中的左右蓝色节点,其余的代码根据之前的论述应该很容易看懂了。
s^t^1 在s和t的父亲相同时值为0,终止循环。
两个if是判断s和t分别是左子节点还是右子节点,根据需要来计算Sum
int Query(int L,int R){
    int ANS=0;
    for(int s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1){
        if(~s&1) ANS+=Sum[s^1];
        if( t&1) ANS+=Sum[t^1];
    }
    return ANS;
} 

(4)区间修改:

A[L..R]+=C
void Update(int L,int R,int C){
    int s,t,Ln=0,Rn=0,x=1;
    //Ln:  s一路走来已经包含了几个数
    //Rn:  t一路走来已经包含了几个数
    //x:   本层每个节点包含几个数
    for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
        //更新Sum
        Sum[s]+=C*Ln;
        Sum[t]+=C*Rn;
        //处理Add
        if(~s&1) Add[s^1]+=C,Sum[s^1]+=C*x,Ln+=x;
        if( t&1) Add[t^1]+=C,Sum[t^1]+=C*x,Rn+=x;
    }
    //更新上层Sum
    for(;s;s>>=1,t>>=1){
        Sum[s]+=C*Ln;
        Sum[t]+=C*Rn;
    } 
}

(5)区间修改下的区间查询:

求A[L..R]的和
int Query(int L,int R){
    int s,t,Ln=0,Rn=0,x=1;
    int ANS=0;
    for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
        //根据标记更新 
        if(Add[s]) ANS+=Add[s]*Ln;
        if(Add[t]) ANS+=Add[t]*Rn;
        //常规求和 
        if(~s&1) ANS+=Sum[s^1],Ln+=x;
        if( t&1) ANS+=Sum[t^1],Rn+=x; 
    }
    //处理上层标记
    for(;s;s>>=1,t>>=1){
        ANS+=Add[s]*Ln;
        ANS+=Add[t]*Rn;
    }
    return ANS;
}

以下为个人做题练手

hdu1754

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cmath>
#include <queue>
#include <iomanip>
#include <stack>
#include <algorithm>
#include <vector>
#include <functional>

using namespace std;

typedef long long LL;
#define FIL(Array, len, num) fill(Array, Array + len, num)
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
const double PI = 3.1415926;
const double E = 2.1718281828;
const int MAXN = 200005;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;

inline int read()
{
    int x = 0, f = 1;char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
    return x * f;
}

//推算都是由子叶开始推向根
//cnt 线段树下标
int n, cnt;
int sum[MAXN << 2];
void Build()
{
    cnt = 1;
    //根据原数组下标  + cnt = 存储下标
    while(cnt < n + 2)
        cnt <<= 1;
    for(int i = 1;i <= n;++i)
        scanf("%d", &sum[cnt + i]);
    for(int i = cnt - 1;i > 0;--i)
        sum[i] = max(sum[i << 1], sum[i << 1 | 1]);
}

void Update(int L, int C)
{
    sum[cnt + L] = C;
    for(int i = (cnt + L) >> 1;i > 0;i >>= 1)
        sum[i] = max(sum[i << 1], sum[i << 1 | 1]);
}

int Query(int L, int R)
{
    int ret = 0;
    //想过把左右蓝色边界去掉,但是是不可行的
    //例如取得区间是[3, 6],跟取区间[3, 4]的一样的结束条件
    for(int l = cnt + L - 1, r = cnt + R + 1;l ^ r ^ 1;l >>= 1, r >>= 1)
    {
        if(~ l & 1) ret = max(ret, sum[l ^ 1]);
        if(r & 1) ret = max(ret, sum[r ^ 1]);
    }
    return ret;
}

int main()
{
    int m;
    while(scanf("%d%d", &n, &m) != EOF)
    {
        Build();
        while(m--)
        {
            char c[10];
            int a, b;
            scanf("%s%d%d", &c, &a, &b);
            if(!strcmp(c, "Q"))
                printf("%d
", Query(a, b));
            else
                Update(a, b);
        }
    }
    return 0;
}
View Code
现在所有的不幸都是以前不努力造成的。。。
原文地址:https://www.cnblogs.com/shuizhidao/p/9581188.html