线段树详解(AWA)

引入

  问题1 有一列长度为 n n n 的数,刚开始全是 0 0 0 。现在执行 m m m 次操作,每次可以执行一下操作之一:

  1. 将数列中的某个数加上某个数值
  2. 询问给定区间的所有数字的和

题目分析

  根据题目我们很容易想到时间复杂度为 O ( n ) O(n) O(n) 的做法,但是这样做有一个问题,如果 n , m n,m n,m 的值非常大,大概去到 1 0 8 10^8 108 左右,处理的结果往往不那么令人满意。

线段树

  线段树就是一棵二叉树,平衡二叉树。(注意:这里的每个区间都是左封闭右开放)根节点的两个子节点就是自己二分的结果,通过不断的二分,构造出这样一棵树,我们就叫他 线段树
线段树

线段树的操作

  线段树,顾名思义,就是一棵树,所以对树的操作线段树也不能少。又因为他是一棵完全二叉树,所以有两种方式储存:静态链表储存和动态指针储存

树的储存

// 动态指针储存
struct Node {
    int Left, Right;
    Node *LeftChild, *RightChild;
    int data;  // 外纪(参数awa)
};
// 静态列表储存
#define NN 1000
int c[4*NN] = { 0 };

  这里有个需要注意的地方:为什么c数组的空间需要 4 × N N 4 \times NN 4×NN 呢,因为这是一棵完全二叉树,但是在构建树的时候有可能出现上图这种情况,所以为了满足完全二叉树的要求,我们必须补全这棵树于是需要声明 4 × N N 4 \times NN 4×NN 的空间。

构建树

  构建树的过程也是相当简单QAQ,因为树是由递归定义的,所以我们只需要稍微搞个递归就好啦~

// 建立一棵树
void build(Node* cur, int l, int r) {
    cur->Left = 1;
    cur->Right = r;
    if (l + 1 < r) {
        cur->LeftChild = new Node;
        cur->RightChild = new Node;
        build(cur->LeftChild, l, (l + r) >> 1);
        build(cur->RightChild, (l + r) >> 1, r);
    } else
        cur->LeftChild = cur->RightChild = NULL;
}

修改树

  同样,我们只需要通过递归来修改这棵树就好~~

// 修改树_1
void change(Node* cur, int x, int delta) {
    if (cur->Left + 1 == cur->Right) {
        cur->data += delta;
    } else {
        if (x < (cur->Left + cur->Right) >> 1)
            change(cur->LeftChild, x, delta);
        if (x > (cur->Left + cur->Right) >> 1)
            change(cur->RightChild, x, delta);
        cur->data =
            cur->LeftChild->data + cur->RightChild->data; // 对接点的操作
    }
}

查询树

  有了修改,那么自然要有查询的操作,查询也很简单()。

// 查找区间和
int qurey(Node* cur, int l, int r) {
    if (l <= cur->Left && cur->Right <= r)
        return cur->data;
    else {
        int ans = 0;
        if (l < (cur->Left + cur->Right) >> 1)
            ans += qurey(cur->LeftChild, l, r);
        if (r > (cur->Left + cur->Right) >> 1)
            ans += qurey(cur->RightChild, l, r);
        return ans;
    }
}

例题

  有了上面这些操作,接下来精灵王国请你帮忙解决一些问题(笑~
问题描述
  精灵王的桌子上零散地放着若干个盒子,桌子的后方是一堵墙。如下图所示。现在从桌子的前方射来一束平行光,把盒子的影子投射到了墙上。问影子的总宽度是多少?在这里插入图片描述
输入输出格式
输入格式:
第1行:盒子的个数N(1≤=N≤10000)。
第2…N+1行:每个盒子的起始位置S和结束位置T(1≤S, T≤100000)。

输出格式:
第1行:包含一个整数,表示影子的总宽度。

输入输出样例
输入样例#1:

4
1 2
3 5
4 6
5 6

输出样例#1:

4

加油!聪明的勇士,我相信你可以的!所以我就不写题解啦!再见!

高阶玩法

  恭喜你,你成功解锁了线段树的高阶玩法嘿嘿~。通过上面的分析我们可以知道,线段树基础玩法对每个点的修改和查询和修改时间复杂度是 O ( l o g 2 n ) O(log_2n) O(log2n) ,确实是个好用的工具,但是!如果我们每次修改一片区间的值,时间复杂度就会去到 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) ,还不如我直接暴力模拟呢(怒)。所以 聪明的你 一定想到了,我比你帅,可以通过标记节点来延迟修改来提高效率,这种方法我们把它叫做 lazy-tag

  我们只需要修改一点代码就能实现这个功能:

  1. 首先是储存部分:
struct Node {
    int Left, Right;
    Node *LeftChild, *RightChild;
    int data;  // 外纪(参数awa)
    int delta; // 延迟修改 <---就是这个东东
};
  1. 然后是修改树和查询树的部分:
// lazy-tag
void update(Node* cur) {
    cur->LeftChild->data +=
        cur->delta * (cur->LeftChild->Right - cur->LeftChild->Left);
    cur->RightChild->data +=
        cur->delta * (cur->RightChild->Right - cur->RightChild->Left);
    cur->LeftChild->delta += cur->delta;
    cur->RightChild->delta += cur->delta;
    cur->delta = 0;
}
// 修改树
void lazy_tag_change(Node* cur, int l, int r, int delta) {
    if (l <= cur->Left && cur->Right <= r) { // 区间被覆盖
        cur->data += delta * (cur->Right - cur->Left);
        cur->delta += delta;
    } else {
        if (cur->delta != 0)
            update(cur);
        if (l < (cur->Left + cur->Right) >> 1)
            lazy_tag_change(cur->LeftChild, l, r, delta);
        if (r > (cur->Left + cur->Right) >> 1)
            lazy_tag_change(cur->RightChild, l, r, delta);
        cur->data =
            cur->LeftChild->data + cur->RightChild->data; // 对接点的操作
    }
}
// 查询树
int lazy_tag_qurey(Node* cur, int l, int r) {
    if (l <= cur->Left && cur->Right <= r)
        return cur->data;
    else {
        if (cur->delta != 0)
            update(cur);
        int ans = 0;
        if (l < (cur->Left + cur->Right) >> 1)
            ans += lazy_tag_qurey(cur->LeftChild, l, r);
        if (r > (cur->Left + cur->Right) >> 1)
            ans += lazy_tag_qurey(cur->RightChild, l, r);
        return ans;
    }
}

  这里我稍微的解释一下,这里每次扫描,只要扫描到左右区间能完全覆盖到的地方,就可以直接返回了,剩下了很多的不必要的计算,再在查询的时候直接更新,再加上就好。到这里,你的不承认,我真的比你帅你真的很聪明。

撤退!再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值