线段树学习笔记

1. 线段树

1.1. 功能

对线性表做快速的区间修改、区间查询

1.2. 原理:分治

将每个区间平分为左右两个处理,建二叉树

[1,10]
[1,5]
[6,10]
[1,3]
[4,5]
[6,8]
[9,10]
[1,2]
3
4
5
[6,7]
8
9
10
1
2
6
7

1.3. 操作

1.3.1. 建树

从下往上合并
把树上所有点都便利一遍,时间复杂度 O ( n ) O(n) O(n)
合并操作merge/pushUp:
求区间和、最大、最小值等可由子区间数值计算父区间数值的,才能使用线段树维护
区间和: v 父 = v 左 + v 右 最小值: v 父 = m i n ( v 左 , v 右 ) 最大值: v 父 = m a x ( v 左 , v 右 ) 字符串哈希: v 父 = v 左 ∗ P l e n ( 右 ) + v 右 ( P 为进制,不是自然溢出法的要取模) . . . . . . 区间和:v_父=v_左+v_右\\ 最小值:v_父=min(v_左,v_右)\\ 最大值:v_父=max(v_左,v_右)\\ 字符串哈希:v_父=v_左*P^{len(右)}+v_右\\ (P为进制,不是自然溢出法的要取模)\\ ...... 区间和:v=v+v最小值:v=min(vv)最大值:v=max(vv)字符串哈希:v=vPlen()+vP为进制,不是自然溢出法的要取模)......
后文将要维护的数值统称为 v v v

1.3.2. 单点查询

i,递归,
i在该区间的左子区间还是右子区间,
进入子区间接着查,查到叶子节点直接返回
例如:查找 [ 8 , 8 ] [8,8] [8,8]

[1,10]
[1,5]
[6,10]
[1,3]
[4,5]
[6,8]
[9,10]
[1,2]
3
4
5
[6,7]
8
9
10
1
2
6
7

时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)

1.3.3. 单点修改

同上,查到叶子节点,再一层一层往回改父区间(merge)
时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)

1.3.4. 区间查询

[ l , r ] [l,r] [l,r],递归,
#1、若覆盖该区间,直接返回
#2、若在左子区间,递归左子
#3、若在右子区间,递归右子
#4、若左右都有,递归两头

时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)
分析(以上图为例):
情况#1,#2,#3不用说,
情况#4的时候左右对称同理,以左为例:
若覆盖要查询的区间覆盖左孩子的右孩子,则在左孩子的右孩子处直接返回
如,查 [ 2 , 5 ] [2,5] [2,5]
则在内侧 [ 4 , 5 ] [4,5] [4,5]直接返回,在 [ 1 , 3 ] [1,3] [1,3]继续搜;

[1,10]
[1,5]
[6,10]
[1,3]
[4,5]
[6,8]
[9,10]
[1,2]
3
4
5
[6,7]
8
9
10
1
2
6
7

若覆盖要查询的区间不覆盖左孩子的右孩子,则一定不覆盖左孩子的左孩子
如,查 [ 5 , 5 ] [5,5] [5,5]
则只往内侧 [ 4 , 5 ] [4,5] [4,5]走,不查 [ 1 , 3 ] [1,3] [1,3]

[1,10]
[1,5]
[6,10]
[1,3]
[4,5]
[6,8]
[9,10]
[1,2]
3
4
5
[6,7]
8
9
10
1
2
6
7

故要么走左边,右边走一步就停(一串紫的挂着一串红的,log)
要么只走右边,(一串紫的,log)
要么是以上两种的组合(log)
所以复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)

1.3.5. 区间修改

1.3.5.1. 错误做法

查询要改的区间,然后直接改
错因:
只改了父区间,没改子区间,查询子区间会出错
如:
整个序列值都为 1 1 1,线段树维护区间最大值
先把 [ 1 , 10 ] [1,10] [1,10]中每个元素加 1 1 1

[1,10]
v=1 +1
[1,5]
v=1
[6,10]
v=1
[1,3]
v=1
[4,5]
v=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

再查询 [ 1 , 1 ] [1,1] [1,1]

[1,10]
v=1 +1
[1,5]
v=1
[6,10]
v=1
[1,3]
v=1
[4,5]
v=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

则错误方法返回 1 1 1(应该为 1 + 1 = 2 1+1=2 1+1=2

1.3.5.2. 懒标记(lazy)
1.3.5.2.1. 定义:

该节点的lazy表示该节点自己的 v v v已修改,该节点的所有子节点 v v v未修改
(根据需要,每个节点可以有多个lazy,如等差数列的首项和公差)

1.3.5.2.2. 下放懒标记(pushDown):

1、子区间根据父区间 l a z y lazy lazy修改 v v v
2、子区间根据父区间 l a z y lazy lazy修改 l a z y lazy lazy
3、父区间 l a z y lazy lazy清空

1.3.5.2.3. 作用:

用了lazy,先查要改的区间,遇到#1情况,则将此区间的 v v v l a z y lazy lazy都修改,则此区间的数据已经正确,只需必要时将lazy下放给子区间即可

1.3.5.2.4. 下放时机:

1、区间查询中,情况#2,#3,#4需要用到子区间的 v v v,故需要pushDown
2、区间修改需要pushDown,否则merge操作时左、右孩子的 v v v均未修改,父区间的 v v v会错误
如:
整个序列值都为 1 1 1,线段树维护区间最大值, l a z y lazy lazy表示增加量

1、把 [ 1 , 10 ] [1,10] [1,10]中每个元素加 1 1 1

[1,10]
v=1+1=2
lazy=1
[1,5]
v=1
[6,10]
v=1
[1,3]
v=1
[4,5]
v=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

2、查询 [ 6 , 10 ] [6,10] [6,10]

[1,10]
v=2
lazy=0
[1,5]
v=1+1=2
lazy=1
[6,10]
v=1+1=2
lazy=1
[1,3]
v=1
[4,5]
v=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

返回2,正确
3、把 [ 4 , 5 ] [4,5] [4,5]中每个元素加 1 1 1
(1)正确示范:
[ 1 , 5 ] [1,5] [1,5]处pushDown

[1,10]
v=2
[1,5]
v=1+1=2
lazy=0
[6,10]
v=2
lazy=1
[1,3]
v=1+1=2
lazy=1
[4,5]
v=1+1=2
lazy=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

修改 [ 4 , 5 ] [4,5] [4,5] v v v l a z y lazy lazy

[1,10]
v=2
[1,5]
v=2
[6,10]
v=2
lazy=1
[1,3]
v=2
lazy=1
[4,5]
v=2+1=3
lazy=1+1=2
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

一路往上merge

[1,10]
v=2
[1,5]
v=max(2,3)=3
[6,10]
v=2
lazy=1
[1,3]
v=2
lazy=1
[4,5]
v=3
lazy=2
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1
[1,10]
v=max(3,2)=3
[1,5]
v=3
[6,10]
v=2
lazy=1
[1,3]
v=2
lazy=1
[4,5]
v=3
lazy=2
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

(2)错误示范:区间修改没有pushDown
修改 [ 4 , 5 ] [4,5] [4,5] v v v l a z y lazy lazy

[1,10]
v=2
lazy=0
[1,5]
v=2
lazy=1
[6,10]
v=2
lazy=1
[1,3]
v=1
[4,5]
v=1+1=2
lazy=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

merge

[1,10]
v=2
lazy=0
[1,5]
v=max(1,2)=2
lazy=1
[6,10]
v=2
lazy=1
[1,3]
v=1
[4,5]
v=2
lazy=1
[6,8]
v=1
[9,10]
v=1
[1,2]
v=1
3
v=1
4
v=1
5
v=1
[6,7]
v=1
8
v=1
9
v=1
10
v=1
1
v=1
2
v=1
6
v=1
7
v=1

发现 v v v都改错了
因此pushDown操作在区间修改中也一定要做!!

1.4. 实现

维护长度为n的序列 n ∈ ( 0 , N ) n \in (0,N) n(0,N),初始值为a[i] i ∈ [ 0 , n ) i \in [0,n) i[0,n)

struct Node{
    int l;int r;
    int v;int lazy;
};
Node tr[N*4];
/*
用数组实现线段树,记得开4*N
叶子结点一共:N
父亲节点一共:N-1
叶子结点下一层:2*N (防止越界)
故总共4*N

另外,我把tr[0]当根
用tr[1]当根也可,但算孩子下标会不一样
*/
void build(int x,int l,int r){
    if(l==r){//到叶子了
        tr[x]=Node{l,r,a[l],0};
        return;
    }
    int xl=x*2+1;int xr=xl+1;
    int mid=(l+r)/2;
    build(xl,l,mid);
    build(xr,mid+1,r);
    //merge
    tr[x]=Node{
        l,r,
        max(tr[xl].v,tr[xr].v),0
    };
}
inline void pushDown(int x){
    if(tr[x].lazy){
        int xl=x*2+1;int xr=xl+1;
        tr[xl].lazy+=tr[x].lazy;
        tr[xl].v+=tr[x].lazy;
        tr[xr].lazy+=tr[x].lazy;
        tr[xr].v+=tr[x].lazy;
        tr[x].lazy=0;
    }
}
//区间查询
int query(int x,int l,int r){
    int mid=(tr[x].l+tr[x].r)/2;
    int xl=x*2+1;int xr=xl+1;
    if(l<=tr[x].l&&tr[x].r<=r){//#1
        return tr[x].v;
    }
    pushDown(x);
    if(r<=mid){//#2
        return query(xl,l,r);
    }
    else if(mid+1<=l){//#3
        return query(xr,l,r);
    }
    else{//#4
        return max(
            query(xl,l,mid),
            query(xr,mid+1,r)
        );
    }
}
//区间修改
void add(int x,int l,int r,int y){
    int mid=(tr[x].l+tr[x].r)/2;
    int xl=x*2+1;int xr=xl+1;
    if(l<=tr[x].l&&tr[x].r<=r){//#1
        tr[x].lazy+=y;
        tr[x].v+=y;
        return;
    }
    pushDown(x);
    if(r<=mid){//#2
        add(xl,l,r,y);
    }
    else if(mid+1<=l){//#3
        add(xr,l,r,y);
    }
    else{//#4
        add(xl,l,mid,y);
        add(xr,mid+1,r,y);
    }
    //merge
    tr[x].v=max(tr[xl].v,tr[xr].v);
}
  • 34
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值