数据结构-浅谈线段树,树状数组[例题讲解学习]

🏆今日学习目标:
🍀学习算法-数据结构-线段树
✅创作者:贤鱼
⏰预计时间:30分钟
🎉个人主页:贤鱼的个人主页
🔥专栏系列:算法
🍁贤鱼的个人社区,欢迎你的加入 贤鱼摆烂团

在这里插入图片描述

🍁线段树

🍀线段树的用途

线段树可以实现单点修改,区间修改,区间查询等操作

为什么使用线段树

  • 可以在 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)的时间复杂度内实现

🍀线段树结构

首先,线段树一定是一个二叉树

举个例子

a[6]={0,11,22,33,44,55};

这是一个数组,那么这个数组构建的线段树是什么样呢(黑色编号(下文tr介绍),红色数值)
在这里插入图片描述

设置a[1]为根节点,a[2]和a[3]分别是左右儿子
那么是不是可以理解为
a[n]的孩子是a[n*2]和a[n*2+1]
我们设一个数组tr[i]储存编号为i所包含的数值
我们将数字编号1-n取一个中间数mid
1~midmid+1~n分别分为左儿子右儿子(注意mid不要重复)

在这里插入图片描述

🍀建树

上面介绍了线段树的基本构成,下面详细介绍如何建树

创建build函数

	void build(int o,int l,int r){//o当前节点,l,r就是上文分的mid, l~mid 和 mid+1~r
		if(l==r){
			cin>>tr[o].a;
			return;
		}
		build(o<<1,l,M);//o<<1代表o*2,但是左移速度快很多
		build(o<<1|1,M+1,r);//|1代表+1,速度比+1快
		pushup(o);//下文介绍
	}

唠唠pushup

	void pushup(int o){
		tr[o].a=tr[o<<1].a+tr[o<<1|1].a;//很简单啦,tr[i]=tr[i/2]+tr[i/2+1]递归啦
	}

🍀区间修改,单点修改

🍎懒惰标记

什么是懒惰标记(lazy)呢?

  • 用来储存当前节点的状态(只有修改数值的时候会用到)

举个栗子(将2-3的每一个值增加3),我们就会将tr[2]和tr[3]的父亲的lazy记为3,这样子,如果tr[i].lazy有值,我们就往下推一位,将他的两个儿子lazy和值分别+3,然后清空当前lazy

注意,如果是往下推的lazy,必须累加,避免顶替之前的状态

为什么只往下推一位?
反正我记录lazy了,用到它了再推,可以节省时间麻~,反正多次推到同一个位置也是累加,不需要每次推到底(乘法另讲)


上文创建的tr,需要用结构体

	struct node{
		int a;
		int laz;//懒惰标记
	}tr[400040];

🍎加减

	void update(int o,int l,int r,int ql,int qr,int k){//o当前节点,lr是当前范围. ql,qr是修改范围,k是修改值
		if(ql<=l&&qr>=r){//包含就修改当前值并且记录lazy
			tr[o].a+=(r-l+1)*k;
			tr[o].laz+=k;
			return;
		}
		if(tr[o].laz)down(o,l,r);//这里down就是往下推,下文会有
		if(ql<=M) update(o<<1,l,M,ql,qr,k);
		if(qr>M) update(o<<1|1,M+1,r,ql,qr,k);//寻找合适范围,像不像二分~,原理如下图(千万不要if else!!!!!!!!!,可能存在两种都有的情况)
		pushup(o);

	}

假设修改2-4的值
在这里插入图片描述
符合ql<=mid,不符合qr>mid

在这里插入图片描述
两个都符合

在这里插入图片描述
灰色lr区间找到

在这里插入图片描述
全部找到

	void down(int o,int l,int r){//按照父亲节点的lazy修改当前值
		tr[o<<1].a+=(M-l+1)*tr[o].laz;
		tr[o<<1|1].a+=(r-M)*tr[o].laz;
		tr[o<<1].laz+=tr[o].laz;
		tr[o<<1|1].laz+=tr[o].laz;
		tr[o].laz=0;//记得清零
	}

🍎乘

和+有亿点点区别,需要记录lazx,
tr[i]的lazx往下推的时候,需要用儿子的lazx*父亲的lazx
我乘一个数字,然后上面右往下乘了一个数字,是不是要互相乘
tr[i]的laz往下推的时候,需要用儿子的laz*父亲的lazx+父亲的laz
我加一个数字,是不是要先乘上面的数字(没有乘的时候lazx=1)再加上面推的数字
比较绕,但是读几遍应该可以理解

修改也是同理
当前的laz要乘修改的k
当前的lazx也要乘修改的k
乘法,那么未往下推的laz是不是也要相对应的乘

	void down(int x,int l,int r){
		if(tr[x].lazx==1&&tr[x].laz==0) return;
		tr[x<<1].lazx=tr[x].lazx*tr[x<<1].lazx;
		tr[x<<1|1].lazx=tr[x].lazx*tr[x<<1|1].lazx;
		tr[x<<1].laz=tr[x<<1].laz*tr[x].lazx+tr[x].laz;
		tr[x<<1|1].laz=tr[x<<1|1].laz*tr[x].lazx+tr[x].laz;
		tr[x<<1].a=tr[x<<1].a*tr[x].lazx+(M-l+1)*tr[x].laz;
		tr[x<<1|1].a=tr[x<<1|1].a*tr[x].lazx+(r-M)*tr[x].laz;
		tr[x].laz=0;
		tr[x].lazx=1;
	}
		void update1(int o,int l,int r,int ql,int qr,int k){
		if(ql<=l&&qr>=r){
			tr[o].a=tr[o].a*k;
			tr[o].laz=tr[o].laz*k;
			tr[o].lazx=tr[o].lazx*k;
			return;
		}
		down(o,l,r);
		if(ql<=M) update1(o<<1,l,M,ql,qr,k);
		if(qr>M) update1(o<<1|1,M+1,r,ql,qr,k);
		up(o);
	}

🍀区间查询

看懂了上面的,这个其实和他差不多.符合就返回值,不然继续寻找区间

	int query(int o,int l,int r,int ql,int qr){
		int ans=0;
		if(ql<=l&&qr>=r){
			return tr[o].a;
		}
		if(tr[o].laz)down(o,l,r);
		if(ql<=M) ans+=query(o<<1,l,M,ql,qr);
		if(qr>M) ans+=query(o<<1|1,M+1,r,ql,qr);
		return ans;
	}

🍀例题

🍌【模板】线段树 2

🍌题目描述

如题,已知一个数列,你需要进行下面三种操作:

  • 将某区间每一个数乘上 x x x
  • 将某区间每一个数加上 x x x
  • 求出某区间每一个数的和。
🍌输入格式

第一行包含三个整数 n , q , m n,q,m n,q,m,分别表示该数列数字的个数、操作的总个数和模数。

第二行包含 n n n 个用空格分隔的整数,其中第 i i i 个数字表示数列第 i i i 项的初始值。

接下来 q q q 行每行包含若干个整数,表示一个操作,具体如下:

操作 1 1 1: 格式:1 x y k 含义:将区间 [ x , y ] [x,y] [x,y] 内每个数乘上 k k k

操作 2 2 2: 格式:2 x y k 含义:将区间 [ x , y ] [x,y] [x,y] 内每个数加上 k k k

操作 3 3 3: 格式:3 x y 含义:输出区间 [ x , y ] [x,y] [x,y] 内每个数的和对 m m m 取模所得的结果

🍌输出格式

输出包含若干行整数,即为所有操作 3 3 3 的结果。

🍌 样例 #1
🍌样例输入 #1
5 5 38
1 5 4 2 3
2 1 4 1
3 2 5
1 2 4 2
2 3 5 5
3 1 4
🍌样例输出 #1
17
2
🍌提示

【数据范围】

对于 30 % 30\% 30% 的数据: n ≤ 8 n \le 8 n8 q ≤ 10 q \le 10 q10
对于 70 % 70\% 70% 的数据:$n \le 10^3 , , q \le 10^4$。
对于 100 % 100\% 100% 的数据: 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ q ≤ 1 0 5 1 \le q \le 10^5 1q105

除样例外, m = 571373 m = 571373 m=571373

(数据已经过加强 _

样例说明:

故输出应为 17 17 17 2 2 2 40   m o d   38 = 2 40 \bmod 38 = 2 40mod38=2)。

🍌AC代码
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
using namespace std;
#define int long long
#define M ((l+r)/2)%m
int n,m,q;
namespace tr{
	struct node{
		int a;
		int laz;
		int lazx;
	}tr[4004000];
	void up(int x){
		tr[x].a=(tr[x<<1].a%m+tr[x<<1|1].a%m)%m;
	}
	
	void build(int o,int l,int r){
		tr[o].lazx=1;
		if(l==r){
			cin>>tr[o].a;
			return;
		}
		build(o<<1,l,M);
		build(o<<1|1,M+1,r);
		up(o);
	}
	
	void down(int x,int l,int r){//就是增加了一~大~堆~mod防止爆int
		if(tr[x].lazx==1&&tr[x].laz==0) return;
		tr[x<<1].lazx=1ll*(tr[x].lazx%m*tr[x<<1].lazx%m)%m;
		tr[x<<1|1].lazx=1ll*(tr[x].lazx%m*tr[x<<1|1].lazx%m)%m;
		tr[x<<1].laz=(1ll*(tr[x<<1].laz%m*tr[x].lazx%m)%m+tr[x].laz%m)%m;
		tr[x<<1|1].laz=(1ll*(tr[x<<1|1].laz%m*tr[x].lazx%m)%m+tr[x].laz%m)%m;
		tr[x<<1].a=(1ll*(tr[x<<1].a%m*tr[x].lazx%m)%m+(M-l+1)*tr[x].laz%m)%m;
		tr[x<<1|1].a=(1ll*(tr[x<<1|1].a%m*tr[x].lazx%m)%m+(r-M)*tr[x].laz%m)%m;
		tr[x].laz=0;
		tr[x].lazx=1;
	}
	
	void update(int o,int l,int r,int ql,int qr,int k){
		if(ql<=l&&qr>=r){
			tr[o].a+=(1ll*(r-l+1)*k%m)%m;
			tr[o].laz+=k%m;
			return ;
		}
		down(o,l,r);
		if(ql<=M) update(o<<1,l,M,ql,qr,k);
		if(qr>M) update(o<<1|1,M+1,r,ql,qr,k);
		up(o);
	}
	
	void update1(int o,int l,int r,int ql,int qr,int k){
		if(ql<=l&&qr>=r){
			tr[o].a=(tr[o].a%m*k%m)%m;
			tr[o].laz=(tr[o].laz%m*k%m)%m;
			tr[o].lazx=(tr[o].lazx%m*k%m)%m;
			return;
		}
		down(o,l,r);
		if(ql<=M) update1(o<<1,l,M,ql,qr,k);
		if(qr>M) update1(o<<1|1,M+1,r,ql,qr,k);
		up(o);
	}
	int query(int o,int l,int r,int ql,int qr){
		int ans=0;
		if(ql<=l&&qr>=r){
			return tr[o].a;
		}
		down(o,l,r);
		if(ql<=M) ans=(ans%m+query(o<<1,l,M,ql,qr)%m)%m;
		if(qr>M) ans=(ans%m+query(o<<1|1,M+1,r,ql,qr)%m)%m;
		return ans%m;
	}
}
using namespace tr;
signed main(){
	cin>>n>>q>>m;
	build(1,1,n);
	while(q!=0){
		q--;
		int w;
		cin>>w;
		int x,y,kk;
		if(w==1){
			cin>>x>>y>>kk;
			update1(1,1,n,x,y,kk);
		}else if(w==2){
			cin>>x>>y>>kk;
			update(1,1,n,x,y,kk);
		}else{
			cin>>x>>y;
			cout<<query(1,1,n,x,y)%m<<endl;
		}
	}
}

🍁树状数组

🍀树状数组和线段树关系

树状数组可以做的,线段树一定可以做,反之则不一定

🍀用途

树状数组可以支持单点修改和区间查询

🍀原理

如图是一个树状数组,a[i]代表当前包含内容的和
在这里插入图片描述
如何得到这个的呢? 看二进制

十进制二进制
11
210
311
4100

所以,从右往左,第一个1在哪里,当前就包含多少个内容(只算一层,不是算到底(例如4包含2,3,4,不算2,3包含的内容))

单点修改

首先我们需要知道一个操作lowbit,很简单,寻找二进制下右往左第一个1的

int lowbit(int x){
	return x&-x;
}

单点修改,同时输入也是这个

void add(int x, int k){
  while(x<=n){  
    a[x]=a[x]+k;
    x=x+lowbit(x);//一次性修改所有,从下一直修改到顶
  }
}

如上图如果1修改了,那么2,4,8的值都会改变

区间查询

int getsum(int x) { //和上面差不多,只不过这里求出来的和是1-n的,如果求l-r,需要getsum(r)-getsum(l-1)
  int ans=0;
  while(x>0){
    ans=ans+a[x];
    x=x-lowbit(x);
  }
  return ans;
}

例如求a[5]-a[7]的和
在这里插入图片描述
红色减去蓝色剩下的就是求的内容了

🍀例题

在这里插入图片描述

🍀AC代码

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#define int long long
using namespace std;
int n,m;
int a[10000005];
int l,r,k;
int lowbit(int x){
	return x&-x;
}
int getsum(int x) { 
  int ans=0;
  while(x>0){
    ans=ans+a[x];
    x=x-lowbit(x);
  }
  return ans;
}

void add(int x, int kk){
  while(x<=n){  
    a[x]=a[x]+kk;
    x=x+lowbit(x);
  }
}

signed main(){
	cin>>n>>m;
	int w;
	for(int i=1;i<=n;i++)
		cin>>w,add(i,w);
	while(m!=0){
		m--;
		int x;
		cin>>x;
		if(x==1){
			cin>>l>>k;
			add(l,k);
		}else{
			cin>>l>>r;
			cout<<getsum(r)-getsum(l-1)<<endl;
		}
	}
}

🍁结束语

如果对您有帮助的话,点个赞支持一下贤鱼吧🏆

请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贤鱼不闲

一分钱也是爱!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值