当前位置: 首页 > article >正文

论线段树的调试

论线段树的调试

  • 前言
  • 写出线段树
    • 一级函数
      • ls--左儿子
      • rs--右儿子
      • update--懒标记的生效
    • 二级函数
      • push_up--向上传值
      • push_down--向下传值
    • 三级函数
      • build--树的建立
      • add--区间加操作
      • query--区间访问
  • 调试线段树
    • 线段树的规律
    • 常见的错误
  • AC代码
  • 后记

前言

线段树,一种很犇的数据结构,基本所有符合结合律的区间问题都可以使用线段树,但是,线段树对于新手来说并不好调,会直接造成RE,TLE等众多错误,本文将不再介绍线段树的基本思想,请新手大致了解后阅读本文
例题在洛谷 P3372 【模板】线段树 1

写出线段树

线段树最难的就是函数的嵌套,十分复杂,这里分别给出

一级函数

就是直接写出不需要嵌套的

ls–左儿子

根据一维数组存二叉树的特性,我们把当前下标乘 2 2 2就行
(用inline和位运算会更快)
代码

inline int ls(int x){
	return x<<1;
}

rs–右儿子

同理可得,为左儿子 + 1 +1 +1
(乘 2 2 2后必定为偶数,或 1 1 1会更快)
代码

inline int rs(int x){
	return x<<1|1;
}

图示(包括ls和rs)

update–懒标记的生效

当前区间长度,乘以父节点的懒标记,加上去就可以
主要用于使懒标记转化为值
因为不遍历到下面,还要给自己打上懒标记
代码

void update(int k,int l,int r,int ftag){
	a[k]+=(r-l+1)*ftag;
	tag[k]+=ftag;
}

二级函数

嵌套了ls,rs,update

push_up–向上传值

我们更新儿子节点,一定要回归父节点
直接让父节点等于儿子节点
代码

void push_up(int k){
	a[k] = a[ls(k)]+a[rs(k)];
}

push_down–向下传值

我们要更新或访问子节点了,懒标记不可能还在父节点上
我们把懒标记传下去,当前懒标记归零即可
代码

void push_down(int k,int l,int r){
	int mid = (l+r)>>1;
	update(ls(k),l,mid,tag[k]);
	update(rs(k),mid+1,r,tag[k]);
	tag[k] = 0;
}

三级函数

到了这里,函数加入了递归,开始作用于整棵线段树

build–树的建立

我们要先递归一遍,目的是把初始值传进去
到了最底层,肯定是直接等于输入数据了
向上回归时,就可以使用push_up
这样的话,树上的父节点等于子节点的和,懒标记就为 0 0 0
代码

void build(int k,int l,int r){
	tag[k] = 0;
	if(l==r){
		a[k] = s[l];
		return;
	}
	int mid = (l+r)>>1;
	build(ls(k),l,mid);
	build(rs(k),mid+1,r);
	push_up(k);
}

add–区间加操作

就是题中给出的区间加和
我们设想一下,区间操作在线段树上,其实就是划分区间的过程
这就分情况了,请看图
在这里插入图片描述
注意,继续递推要清空当前懒标记,回归要用子节点改父节点
具体实现就不难了
代码(有注释)

void add(int ll,int rr,int l,int r,int k,int v){
	if(ll<=l&&rr>=r){//更新区间 
		tag[k]+=v;
		a[k]+=(r-l+1)*v;
		return;
	}
	int mid = (l+r)>>1; 
	push_down(k,l,r);//清空懒标记 
	if(mid>=ll)add(ll,rr,l,mid,ls(k),v);//递推 
	if(mid+1<=rr)add(ll,rr,mid+1,r,rs(k),v);//if不成立,则和目标区间相离 
	push_up(k);//别忘了修改这个区间本身 
}

query–区间访问

跟add都没区别
本质上还是分区间问题,参照上面的图
这次要有返回值,懒标记一定还是要更新的
代码

int query(int ll,int rr,int l,int r,int k){
	if(ll<=l&&rr>=r){
		return a[k];
	}
	int mid = (l+r)>>1,ans = 0;
	push_down(k,l,r);
	if(mid>=ll)ans+=query(ll,rr,l,mid,ls(k));
	if(mid+1<=rr)ans+=query(ll,rr,mid+1,r,rs(k));
	push_up(k);
	return ans;
}

调试线段树

线段树的规律

1访问一个节点,则它的所有祖先节点的懒标记必定为 0 0 0,不然访问到的就是错的!
2更新一个节点,必然会更新他的所有祖先节点,不然祖先节点的值是错的!
3分区间问题,包含返回,相交递推,相离舍去
4tag不管当前节点,只管儿子节点

常见的错误

1build时,一定注意a[k] = s[l],而不是s[k],不然会访问到 0 0 0
2一定注意,除了push_up和清零tag,其他都是+=,不然会WA

AC代码

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N = 114514;
int n,m,a[N<<2],s[N],tag[N<<2];
inline int ls(int x){
	return x<<1;
}
inline int rs(int x){
	return x<<1|1;
}
void update(int k,int l,int r,int ftag){
	a[k]+=(r-l+1)*ftag;
	tag[k]+=ftag;
}
void push_up(int k){
	a[k] = a[ls(k)]+a[rs(k)];
}
void push_down(int k,int l,int r){
	int mid = (l+r)>>1;
	update(ls(k),l,mid,tag[k]);
	update(rs(k),mid+1,r,tag[k]);
	tag[k] = 0;
}
void build(int k,int l,int r){
	tag[k] = 0;
	if(l==r){
		a[k] = s[l];
		return;
	}
	int mid = (l+r)>>1;
	build(ls(k),l,mid);
	build(rs(k),mid+1,r);
	push_up(k);
}
void add(int ll,int rr,int l,int r,int k,int v){
	if(ll<=l&&rr>=r){
		tag[k]+=v;
		a[k]+=(r-l+1)*v;
		return;
	}
	int mid = (l+r)>>1; 
	push_down(k,l,r);
	if(mid>=ll)add(ll,rr,l,mid,ls(k),v);
	if(mid+1<=rr)add(ll,rr,mid+1,r,rs(k),v);
	push_up(k);
}
int query(int ll,int rr,int l,int r,int k){
	if(ll<=l&&rr>=r){
		return a[k];
	}
	int mid = (l+r)>>1,ans = 0;
	push_down(k,l,r);
	if(mid>=ll)ans+=query(ll,rr,l,mid,ls(k));
	if(mid+1<=rr)ans+=query(ll,rr,mid+1,r,rs(k));
	push_up(k);
	return ans;
}
signed main(){
	cin>>n>>m;
	for(int i = 1;i<=n;i++){
		cin>>s[i];
	}
	build(1,1,n);
	for(int i = 1;i<=m;i++){
		int q;
		cin>>q;
		if(q==1){
			int x,y,k;
			cin>>x>>y>>k;
			add(x,y,1,n,1,k);
		}else if(q==2){
			int x,y;
			cin>>x>>y;
			cout<<query(x,y,1,n,1)<<endl;
		}
	}
	return 0;
}

后记

线段树其实是由函数组成的,但是单看每一个函数,都不难
那为什么不会呢,因为函数之间的关系,一般老师是不会讲的
对于初学者好像也不能这么讲
为什么写不出来,调不明白,就是因为没用真正理解
我们在考虑问题时也是有顺序的,顺序选对了,才能有最小的时间复杂度
所有人都觉得二分,前缀和简单
我们把更复杂的算法分成简单的部分,才能更好的理解
本文作者是蒟蒻,如有错误请各位神犇指点!
森林古猿出品,必属精品,请认准CSDN森林古猿1!


http://www.kler.cn/news/357188.html

相关文章:

  • 如何保护您的服务器免受Shellshock Bash漏洞的影响
  • IDEA项目提交至SVNGIT仓库
  • 【升华】人工智能python重要库scikit-learn学习
  • 【ARM】MDK-Flex服务管理软件使用说明
  • 基于SpringBoot+Vue+uniapp微信小程序的校园反诈骗微信小程序的详细设计和实现(源码+lw+部署文档+讲解等)
  • 【华为HCIP实战课程十三】OSPF网络中3类LSA及区域间负载均衡,网络工程师
  • 读人工智能全传16读后总结与感想兼导读
  • 苍穹外卖笔记
  • LeetCode 206 - 反转链表
  • YoloV10改进:Block改进|使用ContextAggregation模块改善C2f模块|即插即用
  • 探索C++的工具箱:双向链表容器类list(1)
  • 【linux】Microsoft Edge 的 Bookmarks 文件存储位置
  • 三大编程思想(POP、OOP、AOP、FOP)及oop 五大设计原则
  • 用Python构建动态折线图:实时展示爬取数据的指南
  • 【74LS48译码器】2022-1-2
  • S7-200 SMART 与 S7-1200 之间 TCP 通信— S7-200 SMART 作为服务器
  • T-SNE
  • 接口测试 —— 如何测试加密接口?
  • 【安当产品应用案例100集】022-阿里云、腾讯云、华为云等公有云上ECS服务器中数据加密保护方案
  • C++容器适配器的模拟实现-stack、queue、priority_queue