树状数组 求区间最值、求区间和 详解

从代码开始

以下就是树状数组的核心代码。

int lowbit(int x)
{
	return x&-x;
}
void add(int i,int x)
{
	while(i<=n)
	{
		c[i]+=x;
		i+=lowbit(i);
	}
}
int get(int i)
{
	int ans=0;
	while(i)
	{
		ans+=c[i];
		i-=lowbit(i);
	}
	return ans;
}

简介

树状数组能维护区间最值、区间和,查询、增加都是 O ( log ⁡ n ) O(\log n) O(logn),十分常用。
基本思想:树状的分块前缀和(不是

分析代码

add部分(改变某数的值)

a d d add add 函数大概表示这样一个功能:
首先将一个数的最后一位 1 1 1 进一位(即 i + = l o w b i t ( i ) \color{red}i+=lowbit(i) i+=lowbit(i)部分,其中 l o w b i t ( i ) \color{red}lowbit(i) lowbit(i) 详见这里
然后将 c [ i ] \color{red}c[i] c[i] 加上要加的值(可参见下面的图,表示了每个 i + = l o w b i t ( i ) \color{red}i+=lowbit(i) i+=lowbit(i)后会到哪里)

在这里插入图片描述
比如,将 5 5 5加上 1 1 1就要将 6 6 6 8 8 8都加上1
在这里插入图片描述
所以,有什么用呢?

get部分(求 1 − i 1-i 1i所有数的和)

同理, g e t get get 函数表示这样一个功能:
首先将一个数的最后一位 1 1 1 变为 0 0 0
然后收集 c [ i ] \color{green}c[i] c[i] 的值(可参见下面的图,绿线表示了每个 i − = l o w b i t ( i ) \color{green}i-=lowbit(i) i=lowbit(i)后会回到哪里)
在这里插入图片描述
可以发现,如果求 n n n,就永远不会沿绿线到 > n >n >n的数,也不会回到 n n n的儿子( n n n的儿子的值已经加到 n n n中),所以,即可沿路求得 1 − n 1-n 1n所有数的和

如果换一种方式,可能会清晰一点。
在这里插入图片描述
其中,在外面的表示它已经加上了(包括了)里面的值。
例如:求 1 − 7 1-7 17的和 = 7 + ( 1 − 6 的和 ) = 7 + 6 [ 包括 5 ] + ( 1 − 4 的和 ) = 7 + 6 [ 包括 5 ] + 4 [ 包括 1 − 3 ] =7+(1-6的和)=7+6{\color{green}[包括5]}+(1-4的和)=7+6{\color{green}[包括5]}+4{\color{green}[包括1-3]} =7+(16的和)=7+6[包括5]+(14的和)=7+6[包括5]+4[包括13]

那么,代码如何实现?

再放一遍代码:

int lowbit(int x){return x&-x;}
void add(int i,int x)
{
	while(i<=n){c[i]+=x;i+=lowbit(i);}
}
int get(int i)
{
	int ans=0;
	while(i){ans+=c[i];i-=lowbit(i);}
	return ans;
}

**

初始数组:相当于 0 0 0加上初始值,只要用 a d d add add函数即可
将某个数增加几:用 a d d add add函数即可
返回一段和 : g e t get get函数可以求 1 − n 1-n 1n的和,因此只要 g e t ( r ) − g e t ( l − 1 ) = S U M ( 1 , r ) − S U M ( 1 , l − 1 ) = S U M ( l , r ) get(r)-get(l-1)=SUM(1,r)-SUM(1,l-1)=SUM(l,r) get(r)get(l1)=SUM(1,r)SUM(1,l1)=SUM(l,r)
返回一个数 :只要 g e t ( x ) − g e t ( x − 1 ) = a [ x ] get(x)-get(x-1)=a[x] get(x)get(x1)=a[x]
将一段增加几:类似差分,增加一段后还要减回来。 a d d ( r , x ) ; a d d ( l , − x ) add(r,x);add(l,-x) add(r,x);add(l,x)
在这里插入图片描述

比如,将 [ 2 , 5 ] [2,5] [2,5]增加 5 5 5,结果如上图,其中未被框起来的± 5 5 5是因为包括了要处理的数而被动加减。

如何维护区间最值?

思路:因为是大区间获取最值时会与小区间取 m a x max max,所以可以每次更新时只取儿子的 m a x max max,而不用照顾到父亲的值(与求和不同)

更改add函数 (但是叫updata更合适)

要与儿子们取max进行更新,但不能取全部的儿子,比如更新8时要更新7个,会达到 O ( n ) O(n) O(n)

在这里插入图片描述
比如对于更新 4 4 4 2 & 3 2\&3 2&3是真儿子,而 1 1 1已经被 2 2 2更新过,不用再更新(因为1一定 < = 2 <=2 <=2 )。
经过观察(别问我是怎么观察的),可得 n n n的真儿子为 n − 2 x [ x < = log ⁡ l o w b i t ( n ) ] n-2^x[x<=\log lowbit(n)] n2x[x<=loglowbit(n)],说人话,就是

  for(int t=1;t<lowbit(i);t<<=1)

所以就简单了。

void add(int i,int x)
{
    c[i]=x;
    for(int t=1;t<lowbit(i);t<<=1)c[i]=max(c[i],c[i-t]);
}

更改get函数

只要找 [ l , r ] [l,r] [l,r]中真儿子更新即可。

int get(int l,int r)
{
    int ans=a[r];
    while(l<=r)
    {
        ret=max(ret,a[r]);
        for(--r;r-l>=lowbit(r);r-=lowbit(r))ans=max(ans,c[r]);
    }
    return ret;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
树状数组可以用来维护一个序列的前缀和,从而实现快速区间查询。但是如果想要区间最大值或最小值,需要进行一些修改。 具体做法如下: 1. 定义一个数组 $maxn$,表示每个节点的最大值。 2. 在更新某个节点的值时,同时更新其父节点的最大值。 3. 在查询区间最大值时,从右往左遍历每个节点,找到第一个其右边的兄弟节点的最大值大于等于查询区间左端点的位置,然后从该兄弟节点开始往右遍历,找到第一个其左边的兄弟节点的最大值小于等于查询区间右端点的位置。这两个位置之间的节点的最大值即为查询区间的最大值。 示例代码: ```cpp const int N = 1e5; int a[N], c[N], maxn[N]; void update(int x, int v) { a[x] = v; while (x <= N) { c[x] += v; maxn[x] = max(maxn[x], v); x += x & -x; } } int query(int l, int r) { int res = INT_MIN; for (int i = r; i >= l; ) { if (i - (i & -i) + 1 >= l) { res = max(res, maxn[i]); i -= i & -i; } else { res = max(res, a[i]); i--; } } return res; } ``` 其中,$a$ 数组表示原始序列,$c$ 数组表示树状数组,$maxn$ 数组表示每个节点的最大值。在更新节点 $x$ 的值时,同时更新 $maxn$ 数组。在查询区间最大值时,从右往左遍历每个节点,找到第一个其右边的兄弟节点的最大值大于等于查询区间左端点的位置,然后从该兄弟节点开始往右遍历,找到第一个其左边的兄弟节点的最大值小于等于查询区间右端点的位置。这两个位置之间的节点的最大值即为查询区间的最大值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值