前言
线段树是用来维护一段区间某种操作的树形数据结构,由于设计到区间,成员节点中l,r表示区间[l,r]。对于线段树的构造实际上是利用了二分的思想,从而使操作降到log级。
数据结构
struct Node {
int l,r;
int sum;
};
Node LTree[maxn << 2];
表示每个节点维护[l,r]区间的operator(这里的操作是区间和),由于二分每个节点都有左右孩子,规模大小差不多是三倍N大,这里直接开4倍N。(这其实也可以动态分配内存,但是静态的简洁好写,减少BUG率)
建树
void Build (int l, int r, int idx) { //建立
LTree[idx].l = l;
LTree[idx].r = r;
if (l == r) { //到达叶子节点
LTree[idx].sum = val[l];
return ;
}
int mid = l + ((r - l) >> 1);
Build(l, mid, idx << 1); //递归建立左子树
Build(mid + 1, r, idx << 1 | 1); //递归建立右子树
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum; //左右孩子值更新完毕后把和传上来(PushUp)
}
后序遍历所有二分的区间,最后有个PushUp操作,其实就是把子区间的结果向上更新。
更新
void Update (int k, int num, int idx) { //更新
// LTree[idx].sum += num;
if (LTree[idx].l == k && LTree[idx].r == k) { //更新到了叶子节点
LTree[idx].sum += num;
return ;
}else if ( k <= (LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1)) ) { //k位置含于左区间
Update(k, num, idx << 1);
}else { //k位置含于右区间
Update(k, num, idx << 1 | 1);
}
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum;
}
更新这里可以先序更新也可以后序更新区间操作。
- 先序更新含义:
我要在单点更新数字,先把包含该点的区间更新完毕后再找那个还包含该点的子区间更新。 - 后序更新含义:
我要在单点更新数字,先把包含该点的子区间更新完毕后,再更加子区间的所有情况向上更新母区间。
查询
int Query (int l, int r, int idx) { //查询
if (l <= LTree[idx].l && r >= LTree[idx].r) { //查询的区间包含当前节点区间,这个节点的和全要
return LTree[idx].sum;
}else {
int mid = LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1);
if (r <= mid) { //完全在左区间中
return Query(l, r, idx << 1);
}else if (l > mid) { //完全在右区间中
return Query(l, r, idx << 1 | 1);
}else { //左右区间各占一部分
return Query(l, mid, idx << 1) + Query(mid + 1, r, idx << 1 | 1);
}
}
}
这里很容易写错的地方是查询的是[l,r]包含的子区间而不是哪些子区间包含[l,r]。比如当我发现r<=mid后很容易就写成Query(l, mid, idx << 1)。而正确的就类似于把要查询的区间分成m个子区间,分别从这些子区间得到贡献反馈过来。所以递归的出口就是:当发现节点的区间被[l,r]完全包含时我就把这个值取出来,即完成了这一小部分。
HDU1166–AC代码
#include <iostream>
#include <string>
using namespace std;
typedef long long LL;
const int maxn = (int)5e4+5;
struct Node {
int l,r;
int sum;
};
int val[maxn],n;
Node LTree[maxn << 2];
void Build (int l, int r, int idx) { //建立
LTree[idx].l = l;
LTree[idx].r = r;
if (l == r) { //到达叶子节点
LTree[idx].sum = val[l];
return ;
}
int mid = l + ((r - l) >> 1);
Build(l, mid, idx << 1); //递归建立左子树
Build(mid + 1, r, idx << 1 | 1); //递归建立右子树
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum; //左右孩子值更新完毕后把和传上来(PushUp)
}
void Update (int k, int num, int idx) { //更新
// LTree[idx].sum += num;
if (LTree[idx].l == k && LTree[idx].r == k) { //更新到了叶子节点
LTree[idx].sum += num;
return ;
}else if ( k <= (LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1)) ) { //k位置含于左区间
Update(k, num, idx << 1);
}else { //k位置含于右区间
Update(k, num, idx << 1 | 1);
}
LTree[idx].sum = LTree[idx << 1].sum + LTree[idx << 1 | 1].sum;
}
int Query (int l, int r, int idx) { //查询
if (l <= LTree[idx].l && r >= LTree[idx].r) { //查询的区间包含当前节点区间,这个节点的和全要
return LTree[idx].sum;
}else {
int mid = LTree[idx].l + ((LTree[idx].r - LTree[idx].l) >> 1);
if (r <= mid) { //完全在左区间中
return Query(l, r, idx << 1);
}else if (l > mid) { //完全在右区间中
return Query(l, r, idx << 1 | 1);
}else { //左右区间各占一部分
return Query(l, mid, idx << 1) + Query(mid + 1, r, idx << 1 | 1);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int T;
cin >> T;
for (int k = 1; k <= T; k++) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> val[i];
}
Build(1, n, 1); //建立线段树
string command;
int i,j;
cout << "Case " << k << ":\n";
while (cin >> command && command.at(0) != 'E') {
cin >> i >> j;
switch (command.at(0)) {
case 'A':
Update(i, j, 1);
break;
case 'S':
Update(i, -j, 1);
break;
case 'Q':
cout << Query(i, j, 1) << '\n';
break;
default:
break;
}
}
}
return 0;
}
总结
相比树状数组,线段树更容易理解,不过代码也比较难写,但是处理的问题范围比较大,但是常数也比较大。