线段树学习笔记

什么是线段树

图片来自百度百科

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 \(O(\log N)\) 。而未优化的空间复杂度为 \(2N\) ,因此有时需要离散化让空间压缩。——by 百度

怎么构造线段树

首先明确一件事,根(\(root\))的左孩子是$root \cdot 2 \(或\)root << 1\(,右孩子是\)root \cdot 2+1\(或\)root<< 1 | 1$

我们有个大小为 \(5\) 的数组 \(a={10,11,12,13,14}\) 要进行区间求和操作,现在我们要怎么把这个数组存到线段树中(也可以说是转化成线段树)呢?我们这样子做:设线段树的根节点编号为 \(1\) ,用数组 \(d\) 来保存我们的线段树, \(d[i]\) 用来保存编号为 \(i\) 的节点的值(这里节点的值就是这个节点所表示的区间总和),如图所示:

上图来自\(oi-wiki\)

void build(int root,int l,int r) {
    if(l==r) {//如果到了叶子节点就直接赋值
        tree[root]=a[l];
        return;
    }
    int mid=(l+r)/2;
    build(root*2,l,mid);//递归左子树
    build(root*2+1,mid+1,r);//递归右子树
    tree[root]=tree[root*2]+tree[root*2+1];//注意需要更新根节点
}

我们来看一下\(build\)函数的运行过程,当从\(root\)开始递归时递归了左子树,左子树又递归左子树,一直到叶节点返回了左叶节点的值,然后和上面的一样去递归右子树,一直到叶然后返回了右叶节点的值(上面描述的可能不是太清楚,可以自己结合一下上图图),然后一层一层的返回就可以了

线段树的区间查询

区间查询,比如求区间 \([l,r]\) 的总和(即 \(a[l]+a[l+1]+ \cdots +a[r]\) )、求区间最大值/最小值……还有很多很多……怎么做呢?

如上图举例
如果要查询区间 \([1,5]\) 的和,那直接获取 \(d[1]\) 的值( \(60\) )即可。那如果我就不查询区间 \([1,5]\) ,我就查区间 \([3,5]\) 呢?

傻了吧。但其实呢我们肯定还是有办法的!

你要查的不是 \([3,5]\) 吗?我把 \([3,5]\) 拆成 \([3,3]\)\([4,5]\) 不就行了吗?

int query(int root,int l,int r,int x,int y) {
    if(x<=l && r<=y) return tree[root];
    int mid=(l+r)/2;
    int ans=0;
    pushdown(root,l,r,mid);
    if(x<=mid)	ans+=query(root*2,l,mid,x,y);
    if(mid<y)   ans+=query(root*2+1,mid+1,r,x,y);
    return ans;
}

线段树的区间修改与懒惰标记

这里就是线段树的精髓了,请仔细理解

区间修改是个很有趣的东西……你想啊,如果你要修改区间 \([l,r]\) ,难道把所有包含在区间[l,r]中的节点都遍历一次、修改一次?那估计这时间复杂度估计会上天。这怎么办呢?我们这里要引用一个叫做 「懒惰标记」 的东西。

我们设一个数组 \(b\)\(b[i]\) 表示编号为 \(i\) 的节点的懒惰标记值。啥是懒惰标记、懒惰标记值呢?这里我再举个例子:

A 有两个儿子,一个是 B,一个是 C。

有一天 A 要建一个新房子,没钱。刚好过年嘛,有人要给 B 和 C 红包,两个红包的钱数相同都是 \((1000000000000001\bmod 2)\) 圆(好多啊!……不就是 \(1\) 元吗……),然而因为 A 是父亲所以红包肯定是先塞给 A 咯~

理论上来讲 A 应该把两个红包分别给 B 和 C,但是……缺钱嘛,A 就把红包偷偷收到自己口袋里了。

A 高兴地说:「我现在有 \(2\) 份红包了!我又多了 \(2\times (1000000000000001\bmod 2)=2\) 元了!哈哈哈~」

但是 A 知道,如果他不把红包给 B 和 C,那 B 和 C 肯定会不爽然后导致家庭矛盾最后崩溃,所以 A 对儿子 B 和 C 说:「我欠你们每人 \(1\)\((1000000000000001\bmod 2)\) 圆的红包,下次有新红包给过来的时候再给你们!这里我先做下记录……嗯……我钱你们各 \((1000000000000001\bmod 2)\) 圆……」

儿子 B、C 有点恼怒:「可是如果有同学问起我们我们收到了多少红包咋办?你把我们的红包都收了,我们还怎么装X?」

父亲 A 赶忙说:「有同学问起来我就会给你们的!我欠条都写好了不会不算话的!」

这样 B、C 才放了心。

在这个故事中我们不难看出,A 就是父亲节点,B 和 C 是 A 的儿子节点,而且 B 和 C 是叶子节点,分别对应一个数组中的值(就是之前讲的数组 \(a\) ),我们假设节点 A 表示区间 \([1,2]\) (即 \(a[1]+a[2]\) ),节点 B 表示区间 \([1,1]\) (即 \(a[1]\) ),节点 C 表示区间 \([2,2]\) (即 \(a[2]\) ),它们的初始值都为 \(0\) (现在才刚开始呢,还没拿到红包,所以都没钱~)。

如图:


注:这里 D 表示当前节点的值(即所表示区间的区间和)。
为什么节点 A 的 D 是 \(2\times (1000000000000001\bmod 2)\) 呢?原因很简单:节点 A 表示的区间是 \([1,2]\) ,一共包含 \(2\) 个元素。我们是让 \([1,2]\) 这个区间的每个元素都加上 \(1000000000000001\bmod 2\) ,所以节点 A 的值就加上了 \(2\times (1000000000000001\bmod 2)\) 咯。

如果这时候我们要查询区间 \([1,1]\) (即节点 B 的值)怎么办呢?不是说了吗?如果 B 要用到的时候,A 就把它欠的还给 B!

具体是这样操作(如图):

注:为什么是加上 \(1\times (1000000000000001\bmod 2)\) 呢?

原因和上面一样——B 和 C 表示的区间中只有 \(1\) 个元素啊!

由此我们可以得到,区间 \([1,1]\) 的区间和就是 \(1\) 啦!O(∩_∩)O 哈哈~!

PS:上述解释来自\(Oi-wiki\),我觉得解释的很好可以看看,附上上面解释的原版代码

void update(int l, int r, int c, int s, int t,int p){
  // [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p 为当前节点的编号
  if (l <= s && t <= r) {
    d[p] += (t - s + 1) * c, b[p] += c;
    return;
  }// 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
  int m = (s + t) / 2; 
  if (b[p] && s!=t){
    // 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
    d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
    b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
    b[p] = 0; // 清空当前节点的标记
  }
  if (l <= m) update(l, r, c, s, m, p * 2);
  if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
  d[p] = d[p * 2] + d[p * 2 + 1];
}

下面是我的代码:

区间修改

void update(int root,int l,int r,int x,int y,int v) {
    if(x<=l && r<=y) return add(root,l,r,v);
    int mid=(l+r)/2;
    pushdown(root,l,r,mid);
    if(x<=mid)	update(root*2,l,mid,x,y,v);
    if(y>mid)   update(root*2+1,mid+1,r,x,y,v);
    tree[root]=tree[root*2]+tree[root*2+1];
}

\(add\)函数

void add(int root,int l,int r,int v) {
    tree[root]+=v*(r-l+1);
    lazy[root]+=v;
}

下放懒标记

void pushdown(int root,int l,int r,int mid) {
    if(lazy[root]==0) return ;
    add(root*2,l,mid,lazy[root]);
    add(root*2+1,mid+1,r,lazy[root]);
    lazy[root]=0;
}

单点修改

单节点更新是指只更新线段树的某个叶子节点的值,但是更新叶子节点会对其父节点的值产生影响,因此更新子节点后,要回溯更新其父节点的值

/*
功能:更新线段树中某个叶子节点的值
root:当前线段树的根节点下标
[nstart, nend]: 当前节点所表示的区间
index: 待更新节点在原始数组arr中的下标
addVal: 更新的值(原来的值加上addVal)
*/
void updateOne(int root, int nstart, int nend, int index, int addVal)
{
    if(nstart == nend)
    {
        if(index == nstart)//找到了相应的节点,更新之
            segTree[root].val += addVal;
        return;
    }
    int mid = (nstart + nend) / 2;
    if(index <= mid)//在左子树中更新
        updateOne(root*2+1, nstart, mid, index, addVal);
    else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子树中更新
    //根据左右子树的值回溯更新当前节点的值
    segTree[root].val = segTree[root*2+1].val+segTree[root*2+2].val;
}

参考资料

1.oi-wiki

2.一步一步理解线段树

3.信息学奥赛一本通

posted @ 2019-06-29 09:02  pyyyyyy  阅读(252)  评论(0编辑  收藏  举报