写在前面
树状数组
- 树状数组和线段树用于区间查找(如同前缀和)和单点修改。不同之处是前缀和只能求区间和,而树状数组和线段树还可以动态修改单点值。
- 关于使用树状数组,脑袋里一定要有一个树状数组图像,细节操作(比如lowbit)不必理会。
- 树状数组必记3个函数——Lowbit函数(第x个数的上/下一个相关数是x ± (x & -x)),Add函数(在下标为x的地方加上v),Query函数(查询第1项到第x项的前缀和)。
线段树
- 关于线段树,一定要记住下面的图像。其中tr数组中每一个元素记录一个区间范围以及该区间的一个属性(区间和/区间最值等),因此tr元素应该具有三个属性:区间的左,右边界和区间的属性。
- 线段树有4个关键函数:PushUp函数, Build函数,Query函数和Modify函数。
- 每一个函数都有一个参数u,表示访问tr数组中第u个元素。
- Build函数的作用是建立所有树节点,并且规定每个结点所管辖的范围。同时还可以初始化一部分结点的属性,一定要先使用Build函数,然后才能进行后续操作。当然我们可以让Build函数只具有规定结点管辖范围的能力。
- PushUp函数一般用于Build函数和Modify函数中,当子结点的值改变后,需要通过该函数即时更新父结点的值。
- Modify函数的作用是给第x个加上v。
- Query函数的作用是查询区间left到right的某个属性(区间和/最值等)
- tr数组要开a数组的4倍!(别问为什么)
- 总体来说,线段树比树状数组的应用范围更广,但是难度较大,同时运行效率较低。我在代码中会标识出易错提醒。
Acwing 1264. 动态求连续区间和
树状数组法:
#include<iostream>
using namespace std;
const int N = 100010;
int a[N], tr[N];
int n, m;
int Lowbit(int x) {
return x & -x;
}
void Add(int x, int v) {
for (int i = x; i <= n; i += Lowbit(i))
tr[i] += v;
}
int Query(int x) { //查询前x项数的和(包括第x项)
int res = 0;
for (int i = x; i; i -= Lowbit(i))
res += tr[i];
return res;
}
int main() {
cin >> n >> m;
//树状数组的值一般是从1开始
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
Add(i, a[i]);
}
int k, a, b;
while (m--) {
scanf("%d%d%d", &k, &a, &b);
if (!k) printf("%d\n", Query(b) - Query(a - 1));
else Add(a, b);
}
return 0;
}
线段树法:
#include<iostream>
using namespace std;
const int N = 100010;
int a[N];
struct Node {
int l, r, s;
}tr[4 * N];
void PushUp(int u) {
tr[u].s = tr[u << 1].s + tr[u << 1 | 1].s;
}
void Build(int u, int left, int right) {
if (left == right) {
tr[u] = { left, right, a[left] }; //易错
return;
}
tr[u] = { left, right }; //易错
int mid = left + right >> 1;
Build(u << 1, left, mid), Build(u << 1 | 1, mid + 1, right); //易错
PushUp(u); //易错
}
int Query(int u, int left, int right) {
if (left <= tr[u].l && right >= tr[u].r)
return tr[u].s;
int sum = 0, mid = tr[u].l + tr[u].r >> 1;
if (left <= mid) sum += Query(u << 1, left, right); //易错
if (right > mid) sum += Query(u << 1 | 1, left, right); //易错
return sum;
}
void Modify(int u, int x, int v) {
if (tr[u].l == tr[u].r) {
tr[u].s += v;
return;
}
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) Modify(u << 1, x, v); //易错
else Modify(u << 1 | 1, x, v); //易错
PushUp(u); //易错
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
Build(1, 1, N);
int k, a, b;
while (m--) {
scanf("%d%d%d", &k, &a, &b);
if (!k) printf("%d\n", Query(1, a, b));
else Modify(1, a, b);
}
return 0;
}
Acwing 788. 逆序对的数量
#include<iostream>
using namespace std;
const int N = 1000100;
int tr[N];
int Lowbit(int x) {
return x & -x;
}
void Add(int x, int v) {
for (int i = x; i <= N; i += Lowbit(i))
tr[i] += v;
}
int Query(int x) {
int res = 0;
for (int i = x; i; i -= Lowbit(i))
res += tr[i];
return res;
}
int main() {
int n, tmp;
long long res = 0;
cin >> n;
for (int i = 0; i < n; i++) {
scanf("%d", &tmp);
Add(tmp, 1);
res += Query(N) - Query(tmp);
}
cout << res << endl;
return 0;
}
Acwing 1270. 数列区间最大值
Acwing 1270. 数列区间最大值
这道题只能用线段树解决!
#include<iostream>
using namespace std;
const int N = 100010;
int a[N];
struct Node {
int left, right, maxv;
}tr[4 * N];
void pushup(int u) {
tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv);
}
void build(int u, int left, int right) {
if (left == right) {
tr[u] = { left, right, a[left] };
return;
}
tr[u] = { left, right };
int mid = left + right >> 1;
build(u << 1, left, mid), build(u << 1 | 1, mid + 1, right);
pushup(u);
}
int query(int u, int left, int right) {
if (left <= tr[u].left && right >= tr[u].right)
return tr[u].maxv;
int mid = tr[u].left + tr[u].right >> 1, maxv = 0;
if (left <= mid) maxv = query(u << 1, left, right);
if (right > mid) maxv = max(maxv, query(u << 1 | 1, left, right));
return maxv;
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
build(1, 1, n);
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", query(1, a, b));
}
return 0;
}