开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情
概念
线段树的概念:线段树的本质是一种二叉树。
将每个区间分解成 和 (其中Mid = )这里的 表示不大于X的整数,【即向下取整】一直到 "左儿子 == 右儿子"
假设线段树的开始区间为[1, n] 那么线段树最大高度为 : 即时间复杂度为:
线段树的作用
对于区间(或者线段)的修改、维护,从 的时间复杂度变成 。
线段树的储存
因为线段树是一棵二叉树,并且除了最后一层,是一颗满二叉树,且,堆也是除了最后一层是一棵满二叉树, 所以一般情况下:用堆(一维数组)来存满整棵树。 存树的时候:
用一维数组存线段树需要开4n的空间:
- 如果开始的区间长度为n,那么最终叶节点一定为n个(倒数第二层最多有n个点)
- 整棵树(除了最后一层)有2n-1个点
- (倒数第二层最多有n个点时)最后一层是倒数第二层的两倍,则最多有2n个点
- 最坏的情况下,整棵树有4n-1个结点,那么我们存线段树的时候,要开4倍空间,即如果区间开始长度为n,那么存树的这个数组就要开4n的空间。
解题步骤
线段树日常操作:
定义
const int N = 数组长度;
int num[节点数4倍空间];//节点数
struct Node{
int l, r;//左端点,右端点
int val;//区间[l, r]的最大值
}tr[N << 2];
由子节点的信息来计算父节点的信息:pushup()
父节点是两个子节点的和,一直往上传值,就能使根节点是所有人的个数
void pushup(int cur){
tr[cur].val = tr[cur << 1].val + tr[cur << 1 | 1].val;
}
建树:build()
- cur代表当前节点
- 初始化:建树的初始值 "tr[cur].l"、"tr[cur].r"、"tr[cur].val"
- 判断叶节点(l==r):如果到达叶节点就把人数赋给该节点,然后return
- 否则求一下当前区间的中点
- 如果不是叶节点,就一直递归找左右孩子,直到找到叶节点进行传值
- 递归建立左边区间和右边区间
- 最后将子节点的人数进行汇总,给父节点
void build(int cur, int l, int r){
tr[cur].l = l, tr[cur].r = r, tr[cur].val = 0;
if(l == r) {
tr[cur].val = num[l];
return;
}
int mid = l + r >> 1;
build(cur << 1, l, mid);
build(cur << 1 | 1, mid + 1, r);
pushup(cur);
}
查询:query()
- [l, r]查询区间,cur代表当前线段树里面的端点。
- 树中节点,已经被完全包含在[l, r]中了,返回当前值
- 如果不能完全包含,就去找它的左右孩子
- 求和(判断与左、右边有交集)
- 这里要划分:qr<=mid 和 qr>mid,因为划分的区间是[l,mid][mid+1,r],所以要用>而不能=
int query(int cur, int ql, int qr) {
int l = tr[cur].l, r = tr[cur].r;
if(ql <= l && qr >= r) return tr[cur].val;
int mid = l + r >> 1;
int val = 0;
if (ql <= mid) val += query(cur << 1, ql, qr);
if (qr > mid) val += query(cur << 1 | 1, ql, qr);
return val;
}
单点修改 modify()
modify() 跟build()差不多,一直找叶节点,找到之后,修改 具体步骤:
//cur代表当前线段树里面的端点。tar代表要修改的位置,即目标位置
void modify(int cur, int tar, int val) {
int l = tr[cur].l, r = tr[cur].r;
//如果当前节点就是叶节点,那么直接修改就可以了
if (l == r) {
tr[cur].val = tr[cur].val + val;
return;
}
int mid = l + r >> 1;
if (tar <= mid) {
modify (cur << 1, tar, val);
} else {
modify (cur << 1 | 1, tar, val);
}
//递归完之后,要更新到父节点,pushup就是子节点更新父节点的信息
pushup(cur);
}
模板题 (HDU 1166)
一个人养水仙花,每盆水仙花都有一个价值,水仙花是排成一行。有三个操作:有时某盆水仙花的价值会上升,有时某盆水仙花的价值会下降。有时他想知道某段连续的水仙花的价值之和是多少,你能快速地告诉她结果吗?
输入格式
第一行一个整数 ,表示有 组测试数据。
每组测试数据的第一行为一个正整数 ,表示有N盆水仙花。
接下来有 个正整数,第 个正整数 表示第i盆水仙花的初始价值。
接下来每行有一条命令,命令有4种形式:
(1) , 和 为正整数,表示第 盆水仙花价值增加
(2) , 和 为正整数,表示第 盆水仙花价值减少
(3) , 和 为正整数,,表示询问第 盆水仙花到第 盆水仙花的价值之和
(4),表示结束,这条命令在每组数据最后出现 每组数据的命令不超过40000条
输出格式
对于第 组数据,首先输出"Case i:"和回车。
对于每个"Query i j"命令,输出第 盆水仙花到第 盆水仙花的美观值之和。
Sample Input 1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End
Sample Output
Case 1:
6
33
59
汇总代码(题目的AC代码)
const int N = 50010;
int num[N * 4];
//定义
struct Node{
int l, r;//左端点,右端点
int val;//区间[l, r]的最大值
}tr[N << 2];
//子节点的信息来计算父节点的信息
void pushup(int cur){
tr[cur].val = tr[cur << 1].val + tr[cur << 1 | 1].val;
}
//建树
void build(int cur, int l, int r){
tr[cur].l = l, tr[cur].r = r, tr[cur].val = 0;
if(l == r) {
tr[cur].val = num[l];
return;
}
int mid = l + r >> 1;
build(cur << 1, l, mid);
build(cur << 1 | 1, mid + 1, r);
pushup(cur);
}
//查询
int query(int cur, int ql, int qr) {
int l = tr[cur].l, r = tr[cur].r;
if(ql <= l && qr >= r) return tr[cur].val;
int mid = l + r >> 1;
int val = 0;
if (ql <= mid) val += query(cur << 1, ql, qr);
if (qr > mid) val += query(cur << 1 | 1, ql, qr);
return val;
}
//修改
void modify(int cur, int tar, int val) {
int l = tr[cur].l, r = tr[cur].r;
if (l == r) {
tr[cur].val = tr[cur].val + val;
return;
}
int mid = l + r >> 1;
if (tar <= mid) modify (cur << 1, tar, val);
else modify (cur << 1 | 1, tar, val);
pushup(cur);
}
int main() {
int t;
cin >> t;
for(int i = 1; i <= t; i++) {
int n;
cin >> n;
for(int j = 1; j <= n; j++) cin >> num[j];
build(1, 1, n); //建树
int flag = 1;
while (true) {
string str;
cin >> str;
//Case 1:
if (flag) {
printf("Case %d:\n", i);
flag = 0;
}
//询问
if (str == "Query") {
int ql = read(), qr = read();
printf("%d\n", query(1, ql, qr));
}
//添加
if (str == "Add") {
int c = read(), m = read();
modify(1, c, m);
}
//减去
if (str == "Sub") {
int c = read(), m = read();
modify(1, c, -m);
}
//结束
if (str == "End")
break;
}
}
return 0;
}