线段树单点修改(细化代码每一行的具体含义)

188 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情

概念

线段树的概念:线段树的本质是一种二叉树。

将每个区间[L,R][L, R]分解成 [L,Mid][L, Mid][Mid+1,R][ Mid + 1, R] (其中Mid = [L+R2][\frac{L+R}{2}])这里的 [x][x] 表示不大于X的整数,【即向下取整】一直到 "左儿子 == 右儿子"

假设线段树的开始区间为[1, n] 那么线段树最大高度为 :log2(n1)+2log_2(n-1)+2 即时间复杂度为:O(logn)O(logn)

线段树的作用

对于区间(或者线段)的修改、维护,从 O(n)O(n) 的时间复杂度变成 O(logn)O(logn)

线段树的储存

因为线段树是一棵二叉树,并且除了最后一层,是一颗满二叉树,且,堆也是除了最后一层是一棵满二叉树, 所以一般情况下:用堆(一维数组)来存满整棵树。 存树的时候:

用一维数组存线段树需要开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()

  1. cur代表当前节点
  2. 初始化:建树的初始值 "tr[cur].l"、"tr[cur].r"、"tr[cur].val"
  3. 判断叶节点(l==r):如果到达叶节点就把人数赋给该节点,然后return
  4. 否则求一下当前区间的中点
  5. 如果不是叶节点,就一直递归找左右孩子,直到找到叶节点进行传值
  6. 递归建立左边区间和右边区间
  7. 最后将子节点的人数进行汇总,给父节点
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()

  1. [l, r]查询区间,cur代表当前线段树里面的端点。
  2. 树中节点,已经被完全包含在[l, r]中了,返回当前值
  3. 如果不能完全包含,就去找它的左右孩子
  4. 求和(判断与左、右边有交集)
  5. 这里要划分: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)

一个人养水仙花,每盆水仙花都有一个价值,水仙花是排成一行。有三个操作:有时某盆水仙花的价值会上升,有时某盆水仙花的价值会下降。有时他想知道某段连续的水仙花的价值之和是多少,你能快速地告诉她结果吗?

输入格式

第一行一个整数 TT,表示有 TT 组测试数据。

每组测试数据的第一行为一个正整数 NN (N<=50000)(N<=50000),表示有N盆水仙花。

接下来有 NN 个正整数,第 ii 个正整数aia_i (1<=ai<=50)(1<=ai<=50) 表示第i盆水仙花的初始价值。

接下来每行有一条命令,命令有4种形式:

(1)AddAdd ii jj, iijj 为正整数,表示第 ii盆水仙花价值增加j(j<=30)j (j<=30)

(2)SubSub ii jj, iijj 为正整数,表示第 ii 盆水仙花价值减少 j(j<=30)j (j<=30)

(3)QueryQuery ii jj, iijj 为正整数,i<=ji<=j,表示询问第 ii 盆水仙花到第 jj 盆水仙花的价值之和

(4)EndEnd,表示结束,这条命令在每组数据最后出现 每组数据的命令不超过40000条

输出格式

对于第 ii 组数据,首先输出"Case i:"和回车。

对于每个"Query i j"命令,输出第 ii盆水仙花到第 jj 盆水仙花的美观值之和。

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;
}