线段树

学习笔记四

线段树

数据结构在noi比赛中的重要性不言而喻,我们已经学了两个十分简洁有神奇的数据结构,现在我来看一个又长又好用的线段树。
首先我们要知道线段树可以干啥,区间之间的操作,单点查询,区间查询,区间修改什么的,记住有了线段树以后就不要在用O(n)的暴力搜索了。
建树。。。。
线段树的本质是一个二叉树,我们用递归的方法来建树,用回溯的方法来确定定值,只要到叶子结点赋值并且返回。代码如下:

void build(int l,int r,int o){
	if(l==r) {
		tree[o]=a[l];
		return;
	}
	int mid=(l+r)/2;
	build(l,mid,2*o);
	build(mid+1,r,2*o+1);
	tree[o]=tree[2*o]+tree[2*o+1];
}

单点查询。。。。
这个也很简单,如果该点在mid左就照左孩子,在右边就找右孩子,找到了就返回,这个比较无聊我们可以直接跳过它的代码。

单点修改。。。。
这个和单点查询差不多,不断的找左右孩子,就OK了,代码如下:

int dandian(int x,int k,int l,int r,int o){
	if(l==r&&r==x){
		tree[o]+=k;
		return;
	}
	int mid=(l+r)/2;
	if(mid>=x) dandian(x,k,l,mid,2*o);
	if(mid<x) dandian(x,k,mid+1,r,2*o+1); 
	tree[o]=tree[2*o]+tree[2*o+1];
}

区间修改。。。。
这里面我们就要用到线段树的精华,懒标记。假设我们找到的区间完全包含于要修改的区间里,那么我们就修改这个区间的值,并且我们要记得给他打上懒标记,并且返回,这里就体现了线段树的精髓,标记而不下推,在查询是再去更改。假如不完全包含,那么我们就看看左边有交集就找左边,右边有交集就找右边,等到完全包含再去修改并且打上懒标记。下推懒标记的pushdown也在下面。代码如下:

int lazy[40000]={0};
void pushdown(int l,int r,int o){
	if(lazy[o]!=0){
		lazy[2*o]+=lazy[o];
		lazy[2*o+1]+=lazy[o];
		tree[2*o]+=l*lazy[o];
		tree[2*o+1]+=r*lazy[o];
		lazy[o]=0;
	}
}

void change(int l,int r,int c,int d,int u,int o){//给c到d上的每一个数字加u 
	if(l>=c&&r<=d){
		tree[o]+=u*(l-r+1);
		lazy[o]+=u;
		return;
	}
	int mid=(l+r)/2;
	pushdown(mid-l+1,r-mid,o);
	if(mid>=c) change(l,mid,c,d,u,2*o);
	if(mid<d) change(mid+1,r,c,d,u,2*o+1);
	tree[o]=tree[2*o]+tree[2*o+1]; 
}

区间查询。。。。
还是和刚才一样如果完全包含就加上。如果完全不包含就返回0(这是一个保障和优化),之后我们一定要pushdown,给之前没干的事情一个补偿,之后如果左边包含就找左边,右边包含就找右边。代码如下:

void search(int l,int r,int c,int d,int o){
	if(l>=c&&r<=d){
		return tree[o];
	}
	int mid=(r+l)/2;
	pushdown(mid-l+1,r-mid,o);
	int ans=0;
	if(mid>=c) ans+=search(l,mid,c,d,2*o);
	if(mid<d) ans+=search(mid+1,r,c,d,2*o+1);
	return ans;	
}

这就是最基础的线段树了,像是什么主席树啦,什么根号线段树以后可能会讲一下下吧,那就下次见啦,下一次应该是哈夫曼树,用那个荷马史诗来直接进阶。

例题1

P4588
这是一道看起来和线段树毫无关系的题,第一眼看上去就想打高精度,但是发现取模高精度不会,最后看了一下标签,我去线段树什么鬼,想了一下觉得应该这样写:
我们用时间来建树,我们发现每一次操作无非就是把一个节点变为pos / 1 那么不就是单点修改线段树维护区间乘法吗,这样一想就简单多了,这样也就不会爆int了,于是我就放下了我打开python的手

例题二

P2574
这一道题的线段树就很明显简单了,我们一个节点维护l,r
的一的数目,那么0自然就是r-l+1-tree[o],每一次修改我们就像线段树一样,但是要注意假如说这个点之前被标记了,那么我们这一次是一定要把标记给移除的,要不然在查找操作时,我们查到这个点的儿子会出问题,还会再一次修改儿子的值,但实际上异或2次就不用再修改了,这个我们只要用lazy[o]^=1就欧克了

例题三 文文的摄影任务

这是一道难题,主要是分类讨论的思想,而且比较好像到用线段树去维护

首先就是对于这个柿子的理解,我们有几种可能呢
在这里插入图片描述
这里这几个维护都是比较简单的,但是要注意一下对于查询时区间的合并是有讲究的,我们最好用结构体去存这几个数据,之后在合并时直接写一个merge函数,这样就可以将多个子儿子进行和并了

node ask(int p,int l,int r,int ql,int qr){
	if(l==ql&&r==qr) return t[p];
	if(qr<=mid) return ask(p<<1,l,mid,ql,qr);
	else if(ql>mid) return ask(p<<1|1,mid+1,r,ql,qr);
	else return merge(ask(p<<1,l,mid,ql,mid),ask(p<<1|1,mid+1,r,mid+1,qr)); 
	//也就是这种写法,这样就可以方便合并了
	//这也是线段树的另外一种写法
}

小知识

1.对于均摊复杂度的题型:像是开根号,取模,之类的不可合并的操作,并且是由衰减性的,也就是我们可以维护区间最大值,之后只要满足这个要求的区间就去往下找,之后变成当代你修改,但是有衰减所以最后的复杂度时均摊的。

比如区间取模,就可以维护一个区间最大值,之后只要这个最大值大于等于p,我们就往下去找,一直找知道到达那个需要取模的叶子节点,最后进行取模。 这个为什么复杂度是对的,因为每一个数最多取模log次就不会在取模了,所以复杂度是均摊的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值