浅析线段树

参考资料:算法竞赛进阶指南-李煜东

        https://www.luogu.com.cn/problem/solution/P3372
 

线段树是一种基于分治思想的二叉树结构,与树状数组相比,线段树更加通用。

其基本特点:

1.线段树每个节点都代表一个区间。

2.线段树具有唯一的根节点,代表的区间是整个统计范围,比如[1,N]

3.线段树每个叶节点都代表一个长度为1的元区间,[x,x]

4.对于每个内部节点[l,r]它的左子节点是[l,mid],右子节点是[mid+1,r],(其中mid=(l+r)/2(向下取整))

 二叉树视角

(图片来源于网络)

上图展示了一棵线段树。可以发现,除去树的最后一层,整棵线段树一定是一棵完全二叉树,树的深度为O(log N)。因此,我们可以按照与二叉堆类似的“父子2倍”节点编号方法。

        1.根节点编号为1.

        2.编号为x的节点的左子节点编号为x*2,右子节点编号为x*2+1

        这样一来,我们就能简单地用一个struct数组来保存你线段树。当然,树的最后一层节点在数组中保存的位置不是连续的,直接空出数组中多余的位置即可。在理想状况下,N个叶节点的满二叉树有  N+N/2+N/4+...+2+1=2N-1 个节点。因为在上述存储方式下,最后还有一层产生了空余,所以保存线段树的数组长度要不小于4N才能保证不会越界。

线段树的建树

        线段树的基本用途是对序列进行维护,支持查询与修改指令。给定一个长度为N的序列A,我们可以在区间 [1,N] 上建立一棵线段树,每个叶节点 [i,i] 保存A[i] 的值。线段树的二叉树结构可以很方便的从下往上传递信息。以区间最大值问题为例,记dat(l,r)等于max{A[i]},显然 dat(l,r)=max(dat(l,mid),dat(mid+1,r))

代码

struct SegmentTree {
    int l,r;
    int dat;
} t[SIZE * 4];                              // struct 数组存储线段树

void build(int p, int l, int r){
    t[p].l=l, t[p].r=r;                    // 节点p代表区间[l,r]
    if (l==r) { t[p].dat = a[l]; return ;} // 叶节点
    int mid = (l+r) / 2;                   // 折半
    build(p*2, l, mid);
    build(p*2+1, mid+1, r);
    t[p].bat = max(t[p*2].dat, t[p*2+1].bat);
}

build(1, 1, n);                            //调用入口

线段树的单点修改

            在线段树中,根节点是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间 [x,x] 的叶节点,然后从下往上更新 [x,x] 以及它的所有祖先节点上保存的信息。时间复杂度为 O(log N).

代码

void change(int p, int x, int v){
	if(t[p].l == t[p].r) {                      //找到叶节点 
		t[p].dat = v; 
		return ;
	}
	int mid = (t[p].l + t[p].r) / 2;
	if(x <= mid) change(p*2,x,v);               //x属于左半区间 
	else change(p*2+1,x ,v);
	t[p].dat = max(t[p*2].dat, t[p*2+1].dat);   //x属于右半区间 
}

change(1,x ,v);                                 //调用入口 

线段树的区间修改

延迟标记

        对于区间操作,我们每次遇到被 l,r 覆盖的区间可以直接将该节点的值添加到答案中。从分块的角度中我们可以证明,被询问的区间 [l,r] 线段树上会被分为 logN 个节点,从而在 logN 的时间求出答案,不过在区间操作中,区间 [l,r] 完全覆盖,那么该节点的全部子节点都会被改变,如果逐一进行更新,那么每次区间修改的时间复杂度会增加到 O(N) 

        在这里我们引进一个延迟标记,俗称懒标记(lazy tag)

        我们在找到被完全覆盖的节点后,如果将所有子节点更新一次,但是在查询时该节点被完全覆盖,根本用不到该节点的所有子节点,那么更新子节点就是徒劳的。所以我们可以将这个被完全覆盖的字节点打上懒标记,如果在下次修改或者查询操作时还需要用到该节点的子节点,我们就将懒标记下传,修改我们所需要的子节点。

在线段树里,无论是元素还是区间都是线段树上的一个节点,所以我们不需要区分区间还是元素,加个判断就好。

        原本区间修改需要通过先改变叶子节点的值,然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达 O(nlogn) 的级别。但当我们引入了懒标记之后,区间更新的期望复杂度就降到了 O(logn) 的级别且甚至会更低.

懒标记的正确打开方式

首先,懒标记的作用是记录每次、每个节点要更新的值。但线段树的优点不在于全记录(全记录依然很慢 qwq),而在于传递式记录:

整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果只修改了自己的话,那就只改变自己。

如果我们需要用到以上优化方式的话,就需要在每次查询的时候pushdown一次,以免重复或者爆炸。

对于pushdown而言,只是pushup的逆向思维(但不是逆向操作),因为标记在父节点上,要由父节点下传 lazy tag

那么问题来了,怎么传导lazy tag呢?每次回溯时pushup是向上传递信息,那么要向下传递信息就改变顺序,向下递归是就pushdown好了!

代码

void pushdown(int p){
	if(t[p].lan){                                //节点p有标记
		t[p*2].dat += (t[p*2].r- a[p*2].l + 1) * t[p].lan;//更新左子节点信息
            //其中 *t[p].lan 因为p点可能被多次标记,所以每个标记都需要向下更新节点
		t[p*2+1].dat += (t[p*2+1].r- t[p*2+1].l + 1) * t[p].lan;//更新右子节点
		t[p*2].lan += t[p].lan;            //给左子节点打标记
		t[p*2+1].lan +=  t[p].lan;        //给右子节点打标记
		t[p].lan=0;                       //清除p的标记
	} 
}
void change(int p,int ls,int rs){    //区间修改,ls rs 为需要修改的区间 ,p为当前节点
	if(t[p].l >= ls && t[p].r <= rs){//如果这个区间被完全覆盖
		t[p].dat += (t[p].r - t[p].l + 1);//修改此节点的数据
		t[p].lan++;              //并打上懒标记,暂时不处理子节点,节省时间
		return ;
	}
	pushdown(p);      //回溯之前下传标记
           //如果发现没有被覆盖,就需要向下寻找子节点,
           //但考虑到子节点可能因为懒标记而没有被更新,所以需要下传标记
	int mid = (t[p].l + t[p].r) / 2; 
	if(ls<=mid) aad(p*2,ls,rs);//如果修改的范围与左子节点有交集,就查找左子节点
	if(rs>mid) aad(p*2+1,ls,rs);//右子节点同理
	t[p].dat = t[p*2].dat + t[p*2+1].dat;//回溯,将子节点的信息向上传递
}

区间查询

        单点查询只是区间查询的一个子问题,可以参考单点修改

从父节点开始查询区间,如果查询的区间被完全覆盖就直接返回维护的值,否则下传懒标记,将左右子节点的值累加起来。

具体看代码

int ask(int p, int ls, int rs){//ls,rs为查找区间 
	if(ls <= t[p].l && rs >= t[p].r) return t[p].dat;//区间被完全覆盖,返回维护值 
	pushdown(p);               //如果没有被完全覆盖就下传懒标记 
	int mid = (t[p].l + t[p].r) / 2;
	long long val = -10000000;
	if(ls <= mid) val += ask(p*2, ls, rs); 
       //与区间修改同理,如果查找区间与左子节点有交集则查找左子节点 
	if(rs > mid) val += aks(p*2+1, ls, rs);// 累加左右子节点的值
	return val; 
}

        懒标记的含义为“该节点曾被修改过,但其子节点未被更新”,因此,一个节点被打上懒标记后,应该及时将该节点的数值修改,所以在编写代码时,应注意“更新信息”与“打标记”之间的关系,避免出现错误。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值