[算法专项] 树状数组

6537 字
33 分钟
[算法专项] 树状数组

树状数组#

树状数组 - OI Wiki

算法讲解108【扩展】树状数组原理、扩展、代码详解_哔哩哔哩_bilibili

算法讲解109【扩展】树状数组相关题目_哔哩哔哩_bilibili

更好的本文阅读体验

算法思想#

树状数组核心思想:用二进制分解的思想,将一个数组的前缀和单点更新操作优化到 O(log⁡n)。树状数组利用二进制最低位lowbit)来组织数据,每个节点 tree[i] 维护原数组区间 (i - lowbit(i), i] 的信息。更新时沿着索引增加 lowbit 的方向向上修改;查询前缀和时沿着索引减少 lowbit 的方向向下累加。本质是用空间换时间,将线性操作转化为树形结构的跳跃式聚合。

归并排序核心思想:基于分治的经典排序算法。将数组不断二分为左右两个子数组,直到每个子数组长度为 1(天然有序);然后通过两路归并,将两个已排序的子数组合并成一个更大的有序数组。合并时,依次比较两个子数组的头部元素,将较小的放入临时数组,直到所有元素归并完成。归并排序保证 O(nlogn) 的时间复杂度,且是稳定排序

[单点修改] + [区间查询]#

树状数组提供了 [单点修改] + [区间查询] 的一种方法

对于求一段前缀和a[1…7],最朴素的方法是a1a_1+a2a_2+···a7a_7

然而,若已知A=a[1…4],B=[5…6],C=[7…7],那么这段前缀和就等于A+B+C

显然的第二种方法只需要知道三个已知数

总能够将一段前缀[1…n]拆成不多于lognlog_n段区间,使得这些区间的信息是已知的;只需要合并这些区间就能得到前缀和的答案

在树状数组中这些区间的划分依赖于当前数的二进制到该二进制去掉最低位的1后再加1后得到的区间

区间查询#

对于88,其二进制位01011000,其管辖的范围为01011000~01010001,即81~88

其实现方法为x&-x

那么对于求a1a_1+a2a_2+···a7a_7,按照上面的划分规则,b7管辖的是a7;b6管辖的是a5、a6;b4管辖的是a1~a4;那么所求即为b7 + b6 + b4

a[1]..a[x]的和
def getsum(x): # a[1]..a[x]的和
ans = 0
while x > 0:
ans = ans + c[x]
x = x - lowbit(x)
return ans

单点修改#

只需要遍历并修改管辖了a[x]的所有点即可

单点修改
def add(x, k):
while x <= n: # 不能越界
c[x] = c[x] + k
x = x + lowbit(x)

模板-洛谷P3374#

问题描述#

如题,已知一个数列,你需要进行下面两种操作:

  • 将某一个数加上 x;
  • 求出某区间每一个数的和。

第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:

  • 1 x k 含义:将第 x 个数加上 k;
  • 2 x y 含义:输出区间 [x,y] 内每个数的和。

树状数组#

树状数组
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
# 单点修改
def update(self, idx, val):
while idx <= self.length:
self.tree[idx] += val
idx += self.lowbit(idx)
# 区间求和
def query(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
n,m = map(int,input().split())
a = [0] + list(map(int,input().split()))
# 初始化树状数组
bit = FenwickTree(n)
for i in range(1,n+1):
bit.update(i,a[i])
for _ in range(m):
op,x,y = map(int,input().split())
if op == 1:
bit.update(x,y)
else:
print(bit.query(y) - bit.query(x-1))

洛谷P16799逆元+维护两个树状数组#

题目#

小蓝正在帮助老师统计实验数据。

一共有 nn 次实验,第 ii 次实验的参数为 ii,对应的实验数据为 aia_i。为了分析实验参数与实验数据之间的关系,小蓝需要多次查询某个连续区间内实验数据的平均值。为了衡量实验结果的稳定性,他还需要查询该区间内实验数据的方差。

此外,小蓝可能会重新进行某一次实验。若重新进行了第 kk 次实验,则原来的 aka_k 会被替换为新的实验数据。

对于一次区间查询 [l,r][l, r],记区间长度为 len=rl+1\textit{len} = r - l + 1,区间平均值为

aˉ=i=lrailen.\bar{a} = \frac{\sum_{i=l}^r a_i}{\textit{len}}.

本题中,区间方差定义为

Var=i=lr(aiaˉ)2.\mathrm{Var} = \sum_{i=l}^r (a_i - \bar{a})^2.

你需要支持两类操作:

  • 查询区间 [l,r][l, r] 的平均值与方差;
  • 将某个位置 kk 的实验数据修改为 xx

由于答案可能是有理数,为避免精度误差,所有查询结果均在模 998244353998244353 的意义下输出。

具体地,设某个答案为有理数 xx。将 xx 表示为最简分数

x=pq,x = \frac{p}{q},

其中 ppqq 为整数,q>0q > 0,且 gcd(p,q)=1\gcd(p, q) = 1。本题保证 qq998244353998244353 互质。

请输出一个整数 yy,满足

0y<998244353,ypq1(mod998244353),\begin{aligned} 0 \le y < 998244353, y \equiv p \cdot q^{-1} \pmod{998244353}, \end{aligned}

这里的 q1q^{-1} 表示 qq 在模 998244353998244353 意义下的乘法逆元。

第一行包含两个正整数 n,mn, m,分别表示实验次数和操作次数。

第二行包含 nn 个整数 a1,a2,,ana_1, a_2, \ldots, a_n,表示初始实验数据。

接下来 mm 行,每行描述一个操作,格式为以下两种之一:

  • 1 l r:查询区间 [l,r][l, r] 内实验数据的平均值和方差;
  • 2 k x:将 aka_k 修改为 xx

逆元+维护两个树状数组#

平均值

aˉ=ailen\bar{a} = \frac{\sum a_i}{len}

方差

Var=(aiaˉ)2=ai22aˉai+lenaˉ2Var = \sum (a_i - \bar{a})^2 = \sum a_i^2 - 2\bar{a}\sum a_i + len \cdot \bar{a}^2

代入 aˉ=S1len\bar{a} = \frac{S_1}{len}

Var=ai22S1lenS1+lenS12len2=S22S12len+S12len=S2S12lenVar = \sum a_i^2 - 2 \cdot \frac{S_1}{len} \cdot S_1 + len \cdot \frac{S_1^2}{len^2} = S_2 - \frac{2S_1^2}{len} + \frac{S_1^2}{len} = S_2 - \frac{S_1^2}{len}

其中 S1=aiS_1 = \sum a_iS2=ai2S_2 = \sum a_i^2

所以在模意义下:

aˉ=S1len1(mod998244353)\bar{a} = S_1 \cdot len^{-1} \pmod{998244353}Var=S2S12len1(mod998244353)Var = S_2 - S_1^2 \cdot len^{-1} \pmod{998244353}

需要支持:

  1. 单点修改akxa_k \to x,更新 S1S_1S2S_2
  2. 区间查询:查询 [l,r][l, r]S1S_1S2S_2

用两个树状数组维护:

  • bit1:维护 ai\sum a_i
  • bit2:维护 ai2\sum a_i^2

逆元+维护两个树状数组
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 998244353;
long long mod_pow(long long a, long long b) {
long long res = 1;
while (b) {
if (b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
struct Fenwick {
int n;
vector<long long> tree;
Fenwick(int n) : n(n), tree(n + 1, 0) {}
void add(int idx, long long val) {
val = (val % MOD + MOD) % MOD;
for (; idx <= n; idx += idx & -idx) {
tree[idx] = (tree[idx] + val) % MOD;
}
}
long long sum(int idx) {
long long res = 0;
for (; idx > 0; idx -= idx & -idx) {
res = (res + tree[idx]) % MOD;
}
return res;
}
long long range_sum(int l, int r) {
return (sum(r) - sum(l - 1) + MOD) % MOD;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
Fenwick bit1(n), bit2(n);
vector<long long> a(n + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> a[i];
bit1.add(i, a[i]);
bit2.add(i, a[i] * a[i] % MOD);
}
// 预计算逆元
vector<long long> inv(n + 1);
for (int i = 1; i <= n; i++) {
inv[i] = mod_pow(i, MOD - 2);
}
while (m--) {
int op;
cin >> op;
if (op == 1) {
int l, r;
cin >> l >> r;
long long len = r - l + 1;
long long inv_len = inv[len];
long long S1 = bit1.range_sum(l, r);
long long S2 = bit2.range_sum(l, r);
// 平均值 = S1 / len
long long avg = S1 * inv_len % MOD;
// 方差 = S2 - S1^2 / len
long long var = (S2 - S1 * S1 % MOD * inv_len % MOD + MOD) % MOD;
cout << avg << ' ' << var << '\n';
} else {
int k;
long long x;
cin >> k >> x;
long long old = a[k];
if (old == x) continue;
// 更新 S1
bit1.add(k, x - old);
// 更新 S2:x^2 - old^2
long long diff = (x * x % MOD - old * old % MOD + MOD) % MOD;
bit2.add(k, diff);
a[k] = x;
}
}
return 0;
}

1. 逆元预计算
vector<long long> inv(n + 1);
for (int i = 1; i <= n; i++) {
inv[i] = mod_pow(i, MOD - 2);
}

区间长度最大 3×1053 \times 10^5,预计算所有逆元避免每次快速幂。

更新时:

2. 模运算
bit1.add(k, x - old); // diff 可能为负,add 内部处理
bit2.add(k, (x*x - old*old) % MOD);

用费马小定理:inv = mod_pow(x, MOD-2)(MOD 是质数)


MT2142#

题目#

给一个长度为nn的排列pp,找一个元素,使得从排列中取出这个元素以后排列的records最多。 一个record是一个元素aiai满足:对于每个正整数 1≤j<i1≤j<i, ai>ajai>aj。

树状数组#

满足题述要求的元素等价于 其是前缀数组中的最大值

用一个is_数组维护该值在原数组中是否为前缀最大值

维护树状数组,从左到右查询当前值的前面是否有大于他的数,如果等于0,则表明其是前缀最大值;如果等于1,则表明删去这个最大值后,该位置会变为前缀最大值,用一个数组维护因为删除前面的最大值而导致的该位置满足条件数的增加(过程维护前缀最大值,便于对其进行增加)

枚举删除的位置,删除后的满足体条件的数 =

原来的 records - 被删元素是否为 record + 删除后新增的 records 数量

树状数组
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
def add(self, idx, val):
while idx <= self.length:
self.tree[idx] += val
idx += self.lowbit(idx)
def query(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
def main():
n = int(input())
p = list(map(int,input().split()))
is_ = [False]*n
g = [0]*n
mx = -1
mx_idx = -1
base = 0
bit = FenwickTree(n)
for i,x in enumerate(p):
get = i - bit.query(x)
if get == 0:
is_[i] = True
base += 1
elif get == 1:
g[mx_idx] += 1
if x > mx:
mx = x
mx_idx = i
bit.add(x,1)
cur = -1
ans = n+1
for i in range(n):
ggain = base - (1 if is_[i] else 0) + g[i]
if ggain > cur or (ggain == cur and p[i] < ans):
ans = p[i]
cur = ggain
print(ans)
if __name__ == "__main__":
main()

[范围增加] + [单点查询]#

数组下标均从1开始;

对于a = [3,2,7,6,2,5]

其差分数组 b = [3,-1,5,-1,-4,3]

那么a[i] = b[1] + b[2] + ··· b[i-1] + b[i]

限制要在2~5位置上+3,add(2,5,3)

针对操作后的差分数组b=[3,2,5,-1,-4,0](在ll处+3,r+1,r+1处 -3)

那么a[i] = b[1] + b[2] + ··· b[i-1] + b[i]

那么在树状数组中实现范围增加,只需要针对差分数组b:add(l,+3),add(r+1,-3)

实现单点查询即为求l的和

洛谷P3368#

题目#

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 x;
  2. 求出某一个数的值。

第一行包含两个整数 N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含 N 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 M 行每行包含 2 或 4 个整数,表示一个操作,具体如下:

操作 1: 格式:1 x y k 含义:将区间 [x,y] 内每个数加上 k;

操作 2: 格式:2 x 含义:输出第 x 个数的值。

树状数组#

[范围增加] + [单点查询]
#include <bits/stdc++.h>
using namespace std;
class FenwickTree {
int n;
vector<long long> tree;
public:
FenwickTree(int n) : n(n), tree(n + 1, 0) {}
void add(int idx, long long val) {
for (; idx <= n; idx += idx & -idx) {
tree[idx] += val;
}
}
long long sum(int idx) {
long long res = 0;
for (; idx > 0; idx -= idx & -idx) {
res += tree[idx];
}
return res;
}
};
int main(){
int n,m;
cin>>n>>m;
FenwickTree bit(n);
int pre = 0;
for(int i = 1;i <= n;i++){
int cur;
cin >> cur;
bit.add(i,cur - pre);
pre = cur;
}
while(m--){
int op;
cin >> op;
if(op == 1){
int x,y;
cin >> x >> y;
long long k;
cin >> k;
bit.add(x,k);
bit.add(y+1,-k);
}
else{
int x;
cin >> x;
cout << bit.sum(x) << endl;
}
}
return 0;
}

[范围增加] + [范围查询]#

在[范围增加] + [单点查询]的基础上,对于范围查询query(l,r)

针对query(k):原数组1~k的累加和 =A1+A2++Ak A_1 + A_2 + ··· +A_k = D1+(D1+D2)++(D1+D2++Dk)D_1 + (D_1+ D_2) + ··· + (D_1 + D_2 + ··· + D_k)

即:ki=1kDii=1k(i1)Dik * \sum_{i=1}^{k} D_i - \sum_{i=1}^{k} (i-1)*D_i

用两个树状数组分别维护这两个信息

Tree1 :

bit1.add(x, k);``bit1.add(y + 1, -k);

Tree2:

bit2.add(x, 1LL * x * k);``bit2.add(y + 1, -1LL * (y + 1) * k);

prefix(r) = (r+1) * sum(bit1, r) - sum(bit2, r)

洛谷P3372#

题目#

如题,已知一个数列 {ai},你需要进行下面两种操作:

  1. 将某区间每一个数加上 k。
  2. 求出某区间每一个数的和。

第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数 ai,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 [x,y] 内每个数加上 k。
  2. 2 x y:输出区间 [x,y] 内每个数的和。

树状数组#

[范围增加] + [范围查询]
#include <bits/stdc++.h>
using namespace std;
class FenwickTree {
long long n;
vector<long long> tree;
public:
FenwickTree(long long n) : n(n), tree(n + 1, 0) {}
void add(long long idx, long long val) {
for (; idx <= n; idx += idx & -idx) {
tree[idx] += val;
}
}
long long sum(long long idx) {
long long res = 0;
for (; idx > 0; idx -= idx & -idx) {
res += tree[idx];
}
return res;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
long long n, m;
cin >> n >> m;
FenwickTree bit1(n); // 维护 b[i]
FenwickTree bit2(n); // 维护 i * b[i]
// 构建差分数组
long long pre = 0;
for (long long i = 1; i <= n; i++) {
long long cur;
cin >> cur;
long long diff = cur - pre; // b[i] = a[i] - a[i-1]
bit1.add(i, diff); // b[i] 加入 bit1
bit2.add(i, 1LL * i * diff); // i * b[i] 加入 bit2
pre = cur;
}
while (m--) {
long long op;
cin >> op;
if (op == 1) {
long long x, y;
long long k;
cin >> x >> y >> k;
// bit1 更新:b[x] += k, b[y+1] -= k
bit1.add(x, k);
bit1.add(y + 1, -k);
// bit2 更新:x*b[x] += x*k, (y+1)*b[y+1] -= (y+1)*k
bit2.add(x, 1LL * x * k);
bit2.add(y + 1, -1LL * (y + 1) * k);
} else {
long long x, y;
cin >> x >> y;
// prefix(r) = (r+1) * sum(bit1, r) - sum(bit2, r)
auto prefix = [&](long long idx) -> long long {
return 1LL * (idx + 1) * bit1.sum(idx) - bit2.sum(idx);
};
long long ans = prefix(y) - prefix(x - 1);
cout << ans << '\n';
}
}
return 0;
}

[逆序对]#

洛谷P1908#

题目#

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 ai>aj 且 i<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

第一行,一个数 n,表示序列中有 n 个数。

第二行 n 个数,表示给定的序列。序列中每个数字不超过 109。

输出序列中逆序对的数目。

归并排序#

归并
def merge(a,l,r,tmp):
if l >= r:
return 0
mid = (l + r)//2
ans = merge(a,l,mid,tmp) + merge(a,mid+1,r,tmp)
# 合并
i,j,k = l,mid+1,l
while i <= mid and j <= r:
if a[i] <= a[j]:
tmp[k] = a[i]
i += 1
else:
ans += mid - i + 1
tmp[k] = a[j]
j += 1
k += 1
while i <= mid:
tmp[k] = a[i]
i += 1
k += 1
while j <= r:
tmp[k] = a[j]
j += 1
k += 1
for i in range(l,r+1):
a[i] = tmp[i]
return ans
def main():
n = int(input())
a = list(map(int,input().split()))
tmp = [0]*n
print(merge(a,0,n-1,tmp))
if __name__ == "__main__":
main()

树状数组#

从后往前遍历

统计≤当前数-1的数的频率,并更新该数的词频

此频率可以通过树状数组long查询

对数组进行[离散化]

树状数组
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
def update(self, idx, val):
while idx <= self.length:
self.tree[idx] += val
idx += self.lowbit(idx)
def query(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
def main():
n = int(input())
a = list(map(int,input().split()))
# 离散化
a_sort = sorted(set(a))
rank = {v: i + 1 for i,v in enumerate(a_sort)}
bit = FenwickTree(n)
ans = 0
# 逆序遍历
for i in range(n - 1,-1,-1):
r = rank[a[i]]
ans += bit.query(r - 1)
bit.update(r,1)
print((ans))
if __name__ == "__main__":
main()

力扣493#

题目#

给定一个数组nums,如果i < jnums[i] > 2*nums[j]我们就将(i, j)称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

树状数组#

与逆序对的唯一区别在于树状数组中加入的计数频数是2*a[i]

后续遍历查找≤当前值-1的频数

树状数组
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
def update(self, idx, val):
while idx <= self.length:
self.tree[idx] += val
idx += self.lowbit(idx)
def query(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
class Solution:
def reversePairs(self, a: List[int]) -> int:
n = len(a)
b = []
for x in a:
b.append(x)
b.append(2 * x)
b_sort = sorted(set(b))
rank = {v: i + 1 for i, v in enumerate(b_sort)}
bit = FenwickTree(len(b_sort))
ans = 0
# 从右向左遍历
for i in range(n - 1, -1, -1):
# 查询已经访问过的2*a[j] < a[i] 的个数
# 即查询排名 < rank[a[i]] 的所有元素
ans += bit.query(rank[a[i]] - 1)
# 将 2*a[i] 加入树状数组
bit.update(rank[2 * a[i]], 1)
return ans

MT3095树状数组+卢卡斯定理#

题目#

给定一个长为n的数字序列,现有m次操作,每一次操作一个区间[l,r],使得区间反转,问操作后的数量逆序对的奇偶性

树状数组#

反转区间只改变内部的逆序对数量,不改变外部的逆序对

对于区间[l,r],若其反转前有x个逆序对,反转后则有c(len,2) - x个逆序对

因此反转后的逆序对总数 = 原逆序对奇偶性^C(len,2)的奇偶性

(即相同为偶数0,相异为奇1)

原逆序对的奇偶性可以通过树状数组求解

C(len,2)的奇偶性取决于 n mod 4

数学定理 Frobenius 定理 https://www.wolai.com/f1RVTvxPUeRLGmEYat76En#iJKcTVDFpEGJXSk2Rz97rv

卢卡斯定理:对于质数 pC(n, k) mod p 等于 n 和 k 在 p 进制下逐位组合数的乘积。

当 p = 2 时:

  • C(n, k) 为奇数 ⇔ k 的二进制位是 n 的二进制位的子集(即 k & (n-k) == 0
  • 等价于 k & (n-k) == 0

对于 k = 2

  • C(n, 2) 为奇数 ⇔ 2 & (n-2) == 0
  • 即 n-2 的二进制最低位为 0 ⇔ n-2 是偶数 ⇔ n 是偶数
  • 再进一步:n = 4k+2 或 4k+3 时成立

卢卡斯定理将大组合数模质数分解为小组合数逐位相乘,当 p=2 时,C(n,k) 为奇数当且仅当 k 的二进制位是 n 的二进制位的子集。

树状数组+卢卡斯定理
import sys
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
def update(self, idx, val):
while idx <= self.length:
self.tree[idx] += val
idx += self.lowbit(idx)
def query(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
def get_parity(a):
"""返回逆序对数量的奇偶性:0 偶,1 奇"""
n = len(a)
a_sort = sorted(set(a))
rank = {v: i + 1 for i, v in enumerate(a_sort)}
bit = FenwickTree(n)
ans = 0
for i in range(n - 1, -1, -1):
r = rank[a[i]]
ans += bit.query(r - 1)
bit.update(r, 1)
return ans & 1
def main():
input = sys.stdin.readline
n = int(input())
p = list(map(int, input().split()))
parity = get_parity(p)
m = int(input())
for _ in range(m):
l, r = map(int, input().split())
length = r - l + 1
# C(length, 2) 为奇数 ⇔ length % 4 == 2 或 3
if length % 4 == 2 or length % 4 == 3:
parity ^= 1
print(parity)
if __name__ == "__main__":
main()

[上升三元组的数量]#

洛谷P1637#

题目#

Erwin 最近对一种叫 thair 的东西巨感兴趣。。。

在含有 n 个整数的序列 a1,a2,…,an 中,三个数被称作thair当且仅当 i<j<k 且 ai<aj<ak。

求一个序列中 thair 的个数。

开始一行一个正整数 n,

以后一行 n 个整数 a1,a2,…,an。

一行一个整数表示 thair 的个数。

树状数组#

用两个数组数组维护

第一个树状数组表示枚举到位置i有多少个小于x

第二个树状数组表示枚举到位置i有多少个二元上升序列的第二个元素小于x

树状数组
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
def add(self, idx, val):
while idx <= self.length:
self.tree[idx] += val
idx += self.lowbit(idx)
def query(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
def main():
n = int(input())
a = list(map(int,input().split()))
bit1 = FenwickTree(n)
bit2 = FenwickTree(n)
a_sort = sorted(a)
rank = {v:i+1 for i,v in enumerate(a_sort)}
ans = 0
for i in range(n):
r = rank[a[i]]
# 查询当前结尾三元组的数量
ans += bit2.query(r-1)
# 更新以当前值做结尾升序一元组的数量
bit1.add(r,1)
# 更新以当前值做结尾升序二元组的数量
bit2.add(r,bit1.query(r-1))
print(ans)
if __name__ == "__main__":
main()

[离线查询]#

洛谷P1972#

题目#

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答……因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

第一行一个正整数 n,表示项链长度。 第二行 n 个正整数 ai,表示项链中第 i 个贝壳的种类。

第三行一个整数 m,表示 HH 询问的个数。 接下来 m 行,每行两个整数 l,r,表示询问的区间。

离线查询#

记录每个查询,并将其按照左边界进行升序排序

用一个map记录每种贝壳最后出现的次数

顺序遍历排序后的查询,对于当前的[l,r]区间,用树状数组维护记录贝壳最后出现的位置;

  • 对于每个数字,只在它最新出现的位置维护 1
  • 当数字再次出现时,先移除旧位置的 1,再在新位置加 1
  • 这样保证任意时刻,每个数字只被计数一次
离线查询
// 记录颜色最后出现的位置
unordered_map<int,int> last;
// 离线查询
int idx = 0;
for(auto [l,r,qid] : q){
while(idx < r){
int x = a[idx];
if(last.count(x)){
bit.update(last[x],-1);
}
last[x] = idx + 1;
bit.update(last[x],1);
idx++;
}
ans[qid] = bit.query(r) - bit.query(l - 1);
}
树状数组+离线查询
#include <bits/stdc++.h>
using namespace std;
class FenwickTree {
private:
int length;
vector<int> tree;
int lowbit(int x) {
return x & -x;
}
public:
FenwickTree(int n) : length(n), tree(n + 1, 0) {}
void update(int idx, int val) {
while (idx <= length) {
tree[idx] += val;
idx += lowbit(idx);
}
}
int query(int idx) {
int res = 0;
while (idx > 0) {
res += tree[idx];
idx -= lowbit(idx);
}
return res;
}
};
int main(){
int n;
cin >> n;
vector<int> a(n);
for(int i = 0;i < n;i++){
cin >> a[i];
}
int m;
cin >> m;
vector<tuple<int,int,int>> q;
for(int i = 0;i < m;i++){
int l,r;
cin >> l >> r;
q.push_back({l,r,i});
}
// 按照右端点排序
sort(q.begin(), q.end(),
[](const auto& x, const auto& y) {
return get<1>(x) < get<1>(y);
});
FenwickTree bit(n);
vector<int> ans(m);
// 记录颜色最后出现的位置
unordered_map<int,int> last;
// 离线查询
int idx = 0;
for(auto [l,r,qid] : q){
while(idx < r){
int x = a[idx];
if(last.count(x)){
bit.update(last[x],-1);
}
last[x] = idx + 1;
bit.update(last[x],1);
idx++;
}
ans[qid] = bit.query(r) - bit.query(l - 1);
}
for(int i = 0;i < m;i++){
cout << ans[i] << '\n';
}
}

MT3066#

题目·偶数个数的异或和#

给定一个含有nn个数的序列A(A1,A2,A3……An)有m组询问。

每组询问的形式为x,y代表查询[x,y]区间(第x个数到第y个数)中出现次数为偶数的数的异或和,如果没有出现次数为偶数的数,输出0(比如区间中出现次数为偶数的数有3 7 8,那么3^7^8就是询问的结果)。

离线查询+异或#

设区间内每个数出现次数为 cnt[x]

  • 所有数的异或和 = ⊕_{x} (cnt[x] % 2 == 1 ? x : 0)
  • 所有不同数的异或和 = ⊕_{x} x

出现偶数次的数的异或和 = ⊕_{x} (cnt[x] % 2 == 0 ? x : 0)

因为:所有不同数 = 奇数次的数 ∪ 偶数次的数

所以:偶数次的数异或 = 所有不同数异或 ⊕ 奇数次的数异或 = 所有不同数异或 ⊕ 所有数异或

py
import sys
class FenwickTree:
def __init__(self, length):
self.length = length
self.tree = [0] * (length + 1)
def lowbit(self, x):
return x & -x
def update(self, idx, val):
while idx <= self.length:
self.tree[idx] ^= val
idx += self.lowbit(idx)
def query(self, idx):
res = 0
while idx > 0:
res ^= self.tree[idx]
idx -= self.lowbit(idx)
return res
def range_xor(self, l, r):
return self.query(r) ^ self.query(l - 1)
def main():
input = sys.stdin.readline
n = int(input())
a = [0] + list(map(int, input().split()))
m = int(input())
q = []
for i in range(m):
x, y = map(int, input().split())
q.append([x, y, i])
# 按右端点排序
q.sort(key=lambda q: q[1])
# 前缀异或(区间所有数的异或和)
pre_xor = [0] * (n + 1)
for i in range(1, n + 1):
pre_xor[i] = pre_xor[i - 1] ^ a[i]
# 离线查询
last = {}
ans = [0]*m
bit = FenwickTree(n)
idx = 1
for l,r,p in q:
while idx <= r:
x = a[idx]
if x in last:
bit.update(last[x],x)
last[x] = idx
bit.update(idx,x)
idx += 1
# 区间不同数的异或和
distinct_xor = bit.range_xor(l, r)
# 区间所有数的异或和
all_xor = pre_xor[r] ^ pre_xor[l - 1]
# 出现偶数次的数的异或 = 不同数异或 ^ 所有数异或
ans[p] = distinct_xor ^ all_xor
print('\n'.join(map(str, ans)))
if __name__ == "__main__":
main()
c++
#include <bits/stdc++.h>
using namespace std;
class FenwickTree {
private:
int n;
vector<int> tree;
public:
FenwickTree(int n) : n(n), tree(n + 1, 0) {}
void update(int idx, int val) {
while (idx <= n) {
tree[idx] ^= val;
idx += idx & -idx;
}
}
int query(int idx) {
int res = 0;
while (idx > 0) {
res ^= tree[idx];
idx -= idx & -idx;
}
return res;
}
int range_xor(int l, int r) {
return query(r) ^ query(l - 1);
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int m;
cin >> m;
// 存储查询:{l, r, idx}
vector<array<int, 3>> queries;
for (int i = 0; i < m; i++) {
int l, r;
cin >> l >> r;
queries.push_back({l, r, i});
}
// 按右端点排序
sort(queries.begin(), queries.end(), [](const array<int, 3>& A, const array<int, 3>& B) {
return A[1] < B[1];
});
// 前缀异或(区间所有数的异或和)
vector<int> pre_xor(n + 1, 0);
for (int i = 1; i <= n; i++) {
pre_xor[i] = pre_xor[i - 1] ^ a[i];
}
// 离线处理
unordered_map<int, int> last;
vector<int> ans(m, 0);
FenwickTree bit(n);
int idx = 1;
for (auto &q : queries) {
int l = q[0], r = q[1], qi = q[2];
// 扩展右指针到 r
while (idx <= r) {
int x = a[idx];
auto it = last.find(x);
if (it != last.end()) {
bit.update(it->second, x); // 消除旧位置的贡献
}
last[x] = idx;
bit.update(idx, x); // 加入新位置
idx++;
}
// 区间不同数的异或和
int distinct_xor = bit.range_xor(l, r);
// 区间所有数的异或和
int all_xor = pre_xor[r] ^ pre_xor[l - 1];
// 出现偶数次的数的异或 = 不同数异或 ^ 所有数异或
ans[qi] = distinct_xor ^ all_xor;
}
for (int i = 0; i < m; i++) {
cout << ans[i] << '\n';
}
return 0;
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

[算法专项] 树状数组
http://blog.7a7a68.xyz/posts/铜牌算法树状数组/
作者
JulY
发布于
2026-08-18
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
JulY
愿你明日如绚丽之花.
公告
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
76
分类
4
标签
20
总字数
160,526
运行时长
0
最后活动
0 天前

目录