树状数组学习笔记

前言:

这理论上是一篇复习的笔记,不会讲的很细,待到补测完再来完善。

概述&定义

先用一张图表示树状数组:

我们定义 aia_i 表示数组元素,cic_i 表示 [ai2x+1,ai][a_{i-2^x+1},a_i] 之间数的总和,其中 xx 表示为 ii 在二进制下末尾 00 的个数。

比如,66 在二进制下表示为 110110c6c_6 表示 [a621+1,a6][a_{6-2^1+1},a_6] 之间的和。

实现

  • 显然的,对于一个 ii,我们要求它二进制下末尾 00 的个数,使用 lowbitlowbit 函数。
int lowbit(int x)
{
	return x&(-x);
}

通过这个函数,我们就可以求出 xx 的上下级,即 x+lowbit(x)x+lowbit(x)xlowbit(x)x-lowbit(x)

  • 单点修改

观察上面的那个图,很容易想到,如果我要修改 xx 的值,那么对应的,他的上级的 cic_i 的值也会改变,而刚才我们讲过求 xx 的上下级,所以直接对他的上级进行修改即可。

void update(int x,int val)
{
	for(int i=x;i<=n;i+=lowbit(i))
		c[i]+=val;
}
  1. 单点修改单点查询
  • 单点查询

对于一个点,在树状数组中,我们仅可以查询他的前缀和,如果我要查询 aia_i 的前缀和,那就是不断查询 cxlowit(x)c_{x-lowit(x)} 的值并且相加。

int ask(int x)
{
	int sum=0;
	for(int i=x;i>0;i-=lowbit(i))
		sum+=c[i];
	return sum;
}
  • 区间修改&区间查询

先考虑一个弱化问题,区间修改单点查询

这可以维护一个 dd (差分数组)。

而区间修改就体现在差分上,假如对 [l,r][l,r] 加上 xx,那就是在 ll 上加上 xx,在 r+1r+1 上减去 xx,再跑一次前缀和就可以得到 ii 这个位置的数值。

先考虑如何区间查询,用 s1(x)s1(x) 表示 i=1xdi\sum_{i=1}^x d_{i}

假设我们要查询 [l,r][l,r]aia_i 的和,显然是:

i=lrai\sum_{i=l}^r a_{i}

先考虑预处理出差分数组 dd

i=lrj=1idj\sum_{i=l}^r \sum_{j=1}^i d_{j} (rl+1)i=1l1di+i=lr(ri+1)×di(r-l+1)\sum_{i=1}^{l-1}d_i+\sum_{i=l}^{r}(r-i+1)\times d_i (rl+1)s1(l1)+(r+1)i=lrdii=lrdi×i(r-l+1) s1(l-1)+(r+1)\sum_{i=l}^r d_i-\sum_{i=l}^r d_i \times i (rl+1)s1(l1)+(r+1)+(r+1)(s1(r)s1(l1))i=lrdi×i(r-l+1) s1(l-1)+(r+1)+(r+1)(s1(r)-s1(l-1))-\sum_{i=l}^r d_i \times i (r+1)×s1(r)l×s1(l1)i=lrdi×i(r+1)\times s1(r) -l\times s1(l-1)-\sum_{i=l}^r d_i \times i

s2(x)s2(x) 表示 i=lrdi×i\sum_{i=l}^r d_i \times i

所以最后就是:

(r+1)×s1(r)l×s1(l1)s2(r)+s2(l1)(r+1)\times s1(r) -l\times s1(l-1)-s2(r)+s2(l-1)

对于 s1s1 这个BIT,显然可以对 llxx,对 r+1r+1 减去 xx 即可。

对于 s2s2 来说,就是对于 llx×lx \times l,对于 r+1r+1 减去 (r+1)×x(r+1)\times x (感性理解下,因为要维护的是 di×id_i \times i 因为是单点修改,所以如上即可。)

  • 区间查询最值

用处不大,静态不如 ST 表好写,动态不如线段树,不讲。

  • 树状数组求逆序对

对于逆序对,我们考虑 ai>aja_i>a_j,并且 i<ji<j 称之为逆序对。

我们不妨开一个值域大小的树状数组,对于当前的数来说,我们对他单点修改加上 11,表示这个数出现了一次。

假设当前的数为 aja_j1ij1\le i \leq j。首先对于所有的 aia_i 来说,必然有 i<ji<j,我们再统计树状数组中 jj 的前缀和,也就是表示说在 aja_j 之前出现过的,且比他要小的数的出现次数,那么这一些数势必是 ai<aja_i<a_j,且 i<ji<j 的。那么对于前 jj 个数来说,统计出了比 aja_j 小的,剩下的势必就是满足 i<j,ai>aji<j,a_i>a_j的了,那以 aja_j 为结尾的逆序对数量就是 jsumjj-sum_{j}

当然,开一个值域范围的树状数组可能会有一点问题,就是当值域太大时,我们需要离散化。

假设有两组数 1,2,31,2,31,2,10001,2,1000,其实对于以 10001000 为末尾的逆序对和以 33 为末尾的逆序对是一样的。所以我们通过离散化来表示数与数之间的相对大小关系即可。

具体做法就是先对这个数组进行排序,然后赋值他相对的排名即可。

  • 树状数组优化最长不下降子序列

和逆序对是如出一辙的。

首先你要知道有一个 n2n^2dpdp

然后我们在这个 dpdp 上要枚举前面的数,如果比他小就更新现在的值。

所以我们现在考虑优化这个过程。

我们开一个值域大小的树状数组,表示在 aia_i 小的数字出现次数为结尾的最长不下降子序列的长度。注意是边遍历一边更新,不能预处理,原因是目的要找 [1,i][1,i] 中出现的,不然如果在 [i+1,n][i+1,n] 中有 [1,ai][1,a_i] 的数就寄了。进来一个数就是单点修改,然后用 log(n)log(n) 的时间找到最大值加一,单点修改即可。

upd in :2023.2.6 大力修改了一下树状数组区间修改,加上树状数组优化最长不下降子序列。

upd in:2023.2.7 修改了一点错误。

posted @   June_Failure  阅读(4)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示