树状数组(Fenwick Tree)是一种用于高效计算前缀和的数据结构,具有较小的内存占用和较快的查询、更新操作。它广泛应用于解决一维数组的区间查询问题。
树状数组的原理基于二进制的思想。假设有一个长度为n的数组A,树状数组就是用一个长度为n的辅助数组C来模拟A数组的前缀和。数组C的索引i表示原数组A的前i个元素的和,数组C的值表示A数组对应前缀的和。
树状数组的核心操作有两个:区间和查询和单点更新。
区间和查询:给定一个区间[l, r],要求计算出原数组A[l, r]的和。使用树状数组的查询操作如下:
1. 初始化一个变量sum为0。
2. 从r开始,将r的最低位的1置为0(即r = r - (r & -r)),并将sum加上C[r]的值。
3. 重复步骤2,直到r为0。
4. 从l开始,将l的最低位的1置为0(即l = l - (l & -l)),并将sum减去C[l]的值。
5. 重复步骤4,直到l为0。
6. 返回sum。
单点更新:给定一个索引i和一个增量delta,要求将原数组A[i]的值加上delta。使用树状数组的更新操作如下:
1. 从i开始,将i的最低位的1加上delta(即i = i + (i & -i))。
2. 重复步骤1,直到i大于数组长度n。
通过这两个核心操作,可以高效地实现对原数组的区间查询和单点更新。
需要注意的是,树状数组的索引从1开始,因此在使用时需要对原始数据进行适当的处理。同时,树状数组只能处理非负数据,对于负数的处理需要进行适当的转换或者使用其他数据结构。
例题1:求每个数在数组中的逆序数和总逆序数
给定一个 1∼N 的随机排列,要求一次只能交换相邻两个数,那么最少需要交换多少次才可以使数列按照从小到大排列呢?
请你求出一个待排序序列的最少交换次数和对应的逆序数列
输入格式
第一行一个整数 N。
第二行一个 1∼N的排列。
输出格式
第一行输出逆序数列,数之间用空格隔开。
第二行输出最少交换次数。
数据范围
1≤N≤1000
输入样例:
8
4 8 2 7 5 6 1 3
输出样例:
6 2 5 0 2 2 1 0
18
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n;
int a[N], tr[N];
int f[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int ask(int x)
{
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = 1; i <= n; i ++)
{
int y = a[i];
f[y] = ask(n) - ask(y);
add(y, 1);
}
int res = 0;
for(int i = 1; i <= n; i ++)
{
cout << f[i] << " ";
res += f[i];
}
cout << endl << res;
return 0;
}
1、f[N]数组存储的是所有在i前面,比a[i]小的数据
2、样例解释:,第一个的逆序对为什么为6,因为求的是就是数值为1的逆序对数量,在1前面有6个数比1大,所以逆序对为6
3、
例题2:逆序对的扩展
在完成了分配任务之后,西部 314 来到了楼兰古城的西部。
相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V
),一个部落崇拜铁锹(∧
),他们分别用 V
和 ∧
的形状来代表各自部落的图腾。
西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n 个点,经测量发现这 n 个点的水平位置和竖直位置是两两不同的。
西部 314314 认为这幅壁画所包含的信息与这 n 个点的相对位置有关,因此不妨设坐标分别为 (1,y1),(2,y2),…,(n,yn),其中 y1∼yn 是 1 到 n 的一个排列。
西部 314 打算研究这幅壁画中包含着多少个图腾。
如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi>yj,yj<yk,则称这三个点构成 V
图腾;
如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi<yj,yj>yk,则称这三个点构成 ∧
图腾;
西部 314 想知道,这 n个点中两个部落图腾的数目。
因此,你需要编写一个程序来求出 V
的个数和 ∧
的个数。
输入格式
第一行一个数 n。
第二行是 n个数,分别代表 y1,y2,…,yn。
输出格式
两个数,中间用空格隔开,依次为 V
的个数和 ∧
的个数。
数据范围
对于所有数据,n≤200000,且输出答案不会超过 int64。
y1∼yn是 1到 n 的一个排列。
样例输入
5
1 5 3 2 4
输出样例:
3 4
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int n;
int a[N], tr[N];
int f[N];
int g[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
LL ask(int x)
{
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++)
{
int y = a[i];
f[i] = ask(y - 1);
g[i] = ask(n) - ask(y);
add(y, 1);
}
memset(tr, 0, sizeof tr);
LL resV = 0, resA = 0;
for(int i = n; i; i --)
{
int y = a[i];
resV += (LL)g[i] * (ask(n) - ask(y));
resA += (LL)f[i] * ask(y - 1);
add(y, 1);
}
cout << resV << " " << resA << endl;
return 0;
}
1、求所以V
的个数和 ∧
的个数,而且严格y1∼yn是 1到 n 的一个排列,所以可以不用离散化处理,求v的个数,所以求出每一个数,左边比它的大的和右边都比它大,然后相乘在全部相加既可,求∧
的个数也是如此的思路
2、从左到右扫描一遍,f[i]存储的是当前比a[i]小的集合,g[i]存储的是当前比a[i]大的集合,然后建树
3、重新初始化,然后从从右到左扫描一遍
4、树状数组求逆序对,让我们知道了如何在一个序列中计算每个数后面有多少个数比它小,因此我们可以通过这个性质来做一些事情
‘v’图腾求法
倒序扫描序列a,利用树状数组求出每个a[i]后面有几个数比它大记录为g[i]
正序扫描序列a,利用树状数组求出每个a[i]前面有几个数比它大,记录为r[i]
’^’图腾求法
倒序扫描序列a,利用树状数组求出每个a[i]后面有几个数比它小,记录为g[i]
正序扫描序列a,利用树状数组求出每个a[i]前面有几个数比它小,记录为f[i]
例题三:区间和和单点修改
给定 n个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b]的连续和。
输入格式
第一行包含两个整数 n和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。
数列从 1 开始计数。
输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。
数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N], tree[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for(int i = x; i <= n; i += lowbit(i)) tree[i] +=c;
}
int ask(int x)
{
int res = 0;
for(int i = x; i; i-= lowbit(i)) res += tree[i];
return res;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++) add(i, a[i]);
while(m --)
{
int k, x, y;
scanf("%d%d%d", &k, &x, &y);
if(k == 0)
{
cout << ask(y) - ask(x - 1) << endl;
}
else add(x, y);
}
return 0;
}
解析:1、因为是单点修改,所以正常建树,把每个数据压入就行
2、求区间和,从r到l - 1求的就是区间l - r 的和
样例四:区间修改和单点查询
给定长度为 N 的数列 A,然后输入 M 行操作指令。
第一类指令形如 C l r d
,表示把数列中第 l∼r 个数都加 d。
第二类指令形如 Q x
,表示询问数列中第 x 个数的值。
对于每个询问,输出一个整数表示答案。
输入格式
第一行包含两个整数 N 和 M。
第二行包含 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤10^5,
|d|≤10000,
|A[i]|≤10^9
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2
输出样例:
4
1
2
5
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
int a[N], tr[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for(int i = x;i <= n; i += lowbit(i)) tr[i] += c;
}
int ask(int x)
{
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++) add(i, a[i] - a[i - 1]);
while(m --)
{
char op[2];
scanf("%s", op);
if(*op == 'C')
{
int l, r, d;
scanf("%d%d%d", &l, &r, &d);
add(l, d), add(r + 1, -d);
}
else
{
int x;
cin >> x;
cout << ask(x) << endl;
}
}
return 0;
}
1、因为是区间修改,所以我们可以使用差分来降低时间复杂度
2、区间修改,所以我们是两个端点修改就可以了
样例五:区间查询和区间修改
既可以树状数组,也可以使用线段树,但是这里就使用树状数组来做
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d
,表示把 A[l],A[l+1],…,A[r] 都加上 d。Q l r
,表示询问数列中第 l∼r个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤10^5,
|d|≤10000,
|A[i]|≤10^9
输入样例:
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
输出样例:
4
55
9
15
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
int a[N];
LL tr1[N], tr2[N];
int lowbit(int x)
{
return x & -x;
}
void add(LL tr[], LL x, LL c)
{
for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
LL ask(LL tr[], LL x)
{
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
LL get_sum(LL x)
{
return ask(tr1, x) * (x + 1) - ask(tr2, x);
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++)
{
LL b = a[i] - a[i - 1];
add(tr1, i, b);
add(tr2, i, b * i);
}
while(m --)
{
char op[2];
scanf("%s", op);
if(*op == 'Q')
{
int l, r;
scanf("%d%d", &l, &r);
cout << get_sum(r) - get_sum(l - 1) << endl;;
}
else
{
int l, r, d;
scanf("%d%d%d", &l, &r, &d);
add(tr1, l, d), add(tr2, l, l * d);
add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
}
}
return 0;
}
1、因为是区间修改,所以还是使用差分来做,这里维护两个树状数组,tr1存储的是修改区间的数组,tr2存储的是区间的数值