线段树入门题 之 数据结构

目录

瞎回忆

本次真正想记录的题:数据结构

题目描述

想想

代码

bug记录QWQ深刻了


瞎回忆

当多次修改一组数据,最后再给出多个询问,求区间和时,用前缀和就挺好.

但是前缀和修改慢,功能也没线段树强大(树状数组也没线段树的功能多,但人家代码简单...),大多数时候前缀和都不能做到(就比如求max,mul,gcd(最大公因数)等等)。(听说)线段树细节容易写错。QWQ

哪些情况能用线段树? 必须满足“区间加法”.....就比如:

可:主区间和=左儿子(区间) + 右儿子(区间)

主区间最大值=max(左区间最大值,右区间最大值) 等

不可:求众数,最长的连续的数等

记得有学长说过“ 线段树更多的是一种工具....你可以用它维护数据,在树上加很多东西.... ”(害怕.jpg)

线段树是一种维护区间线性的数据结构,要用线段树的问题一般得转化为线性的,(雨巨讲了“时间线”的概念,将二维的问题,其中一维按某种方式排序(即时间线)后,就只剩一维要再处理,即是线性的)如“数星星”这题:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAKuWkp-elug==,size_20,color_FFFFFF,t_70,g_se,x_16

红框处的提示就很重要,在这里,在本星星出现前出现的星星才有可能符合条件,在这之后出现的星星都与本星星无关,在本星星前的星星中,x在“我”前面的才对“我” “有贡献”。

先打住,看线段树的基本结构:

主要是 建树Build()、修改UpDate()、查询Query()几个板块吧,为了方便把 更新 也弄一个函数PushUp(),要用到懒标记的话还有PushDown().

#include<iostream>
#define MAX 1000
using namespace std;
int sum[MAX<<2], a[MAX], n;

//1)建树 
//PushUp函数更新节点信息,这里是求和 
void PushUp(int rt){
	//sum[rt] = sum[rt<<1]+sum[rt<<1|1];  //rt<<1|1即2*rt+1 
	sum[rt]=max(sum[rt<<1], sum[rt<<1|1]);
}
//Build函数建立线段树 
//[l,r]表示当前节点区间,rt表示当前节点的实际存储位置
void Build(int l, int r, int rt){
	if(l==r){
		sum[rt]=a[l];
		return;
	}
	int m=(l+r)>>1;
	Build(l,m,rt<<1); //左右递归 
	Build(m+1, r,rt<<1|1);
	PushUp(rt);        //更新信息 
}

//点的修改 
void Update(int pos, int C, int l,int r,int rt){
	if(l==r){
		sum[rt]+=C;
		printf("sum[%d]=%d\n",rt,sum[rt]);
		return;
	}
	int m=(l+r)>>1;
	if(pos <= m) Update(pos,C,l,m,rt<<1);
	else Update(pos,C,m+1,r,rt<<1|1);
	PushUp(rt);//子节点的信息更新了,所以本节点也要更新信息 
}

//3)区间查询(本题为求和
int Query(int L,int R,int l,int r,int rt){
	if(L <= l&&R>=r)return sum[rt];
	int m=(l+r)>>1;
	int ans =-1e9;
	if(L <= m) /*ans+=Query(L,R,l,m,rt<<1);*/ans=max(ans,Query(L,R,l,m,rt<<1));
	if(R > m) /*ans+=Query(L,R,m+1,r,rt<<1|1);*/ans=max(ans, Query(L,R,m+1,r,rt<<1|1));
	return ans;
} 
int main(){
	int x, y, z, n, m;
	cin >>n >>m;
	for(int i=1; i<=n; i++)cin >>a[i];
	Build(1,n,1);
	while(m--){
		cin >> x>> y>>z;
		if(x==1)Update(y,z,1,n,1);
		else cout <<Query(y, z, 1,n,1)<<'\n';
	}
	return 0;
}

上述代码中,只要把PushUp()和Query()部分稍做改动就由求和变为求max.

上面代码的UpDate()是点的修改,如果要对一个区间的数都进行操作,将点的位置pos改成要修改的区间 L~R:

void Update(int L, int R, int d, int l,int r, int kt){
	//printf(">>l=%d r=%d kt=%d\n",l,r,kt);
	if(L<=l&&r<=R){
		tr[kt]+=d*(r-l+1);
		lazy[kt]+=d;
		return;
	}
	PushDown(kt ,l,r);
	int m=(l+r)>>1;
	if(L<=m) Update(L,R,d,l,m,kt<<1);
	if(R>m) Update(L,R,d,m+1,r,kt<<1|1);
	PushUp(kt); 
}

本次真正想记录的题:数据结构

【这么基础的题,窝个菜鸡对着代码改了半天,不行后又比较人家ac了的代码...还是找不到bug,最后最后,感谢学姐感谢学长 ~qwq】

题目描述

    qn姐姐给你了一个长度为n的序列还有m次操作让你玩,

    1 l r 询问区间[l,r]内的元素和

    2 l r 询问区间[l,r]内的元素的平方 

    3 l r x 将区间[l,r]内的每一个元素都乘上x

    4 l r x 将区间[l,r]内的每一个元素都加上x

输入描述:

第一行两个数n,m接下来一行n个数表示初始序列就下来m行每行第一个数为操作方法opt,若opt=1或者opt=2,则之后跟着两个数为l,r若opt=3或者opt=4,则之后跟着三个数为l,r,x操作意思为题目描述里说的

输出描述:

对于每一个操作1,2,输出一行表示答案

示例

输入:

5 6
1 2 3 4 5
1 1 5
2 1 5
3 1 2 1
4 1 3 2
1 1 4
2 2 3

输出:

15
55
16
41 

备注:
对于100%的数据 n=10000,m=200000 (注意是等于号)
保证所有询问的答案在long long 范围内

想想

1.这题要用lazy标记.

2.我 先加上一个数再乘上一个数,和 先乘上一个数再加一个数,有什么区别,该怎么表示。

(上一次的值)*3+5 自然没有问题 ,  但 [(上一次的值)+5]*3 呢?我们可以把它们统一成一种形式,也就是去掉表示优先的[ ] ,即 [(上一次的值)+5]*3 = (上一次的值)*3+5*3 ,就是()*k+b的形式,也就是我们的lazy标记要装k和b.

3.求 区间和 容易,那,区间内 元素的平方  的和 呢?加上x和乘上x后该怎么求?我们用线段树,就是要做到由两个子区间的状态直接求得总区间的状态而不是一个一个的访问区间最小级别的元素(不然用时更多,比暴力还差..)所以我们要判断题目符不符合这个性质,如果这题改成,对每个数都开方(向下)取整后求区间元素和,这就做不到“直接得到”了,因为结果与每个元素本身都“息息相关”(你可以有多种区间和相同的,元素不一样,开方再求和的结果也可能不一样)

展开,把式子展开:

gif.latex?x_{1}^{2}&plus;x_{2}^{2}&plus;...x_{n}^{2}      gif.latex?(x_{i}&plus;b)^{2}=x_{i}^{2}&plus;2*b*x&plus;b^{2}   gif.latex?(x_{i}*b)^{2}=x_{i}^{2}*b^{2}

也就是    sum2[kt<<1] = sum2[kt<<1]*k*k + 2*b*sum[kt<<1]+b*b*(m-l+1);

(这里用sum表示和,sum2表示平方和)

代码

#include<iostream>
#define ll long long
#define MAX 10005
using namespace std;
int N, M;
ll sum2[MAX<<2], sum[MAX<<2], lazy[2][MAX<<2], a[MAX+5];  //"()*k+b",lazy[0][x]是k,lazy[1][x]是b 
void PushUp(int kt){
	sum[kt]=sum[kt<<1]+sum[kt<<1|1]; 
	sum2[kt]=sum2[kt<<1]+sum2[kt<<1|1];
}
void Build(int l, int r, int kt){
	lazy[0][kt]=1;
	if(l==r){
		sum[kt]=a[l];
		sum2[kt]=a[l]*a[l];
		return;
	}
	ll m=(l+r)>>1;
	Build(l,m,kt<<1);
	Build(m+1,r,kt<<1|1);
	PushUp(kt);
}
void PushDown(int l, int r, int kt){
	ll k=lazy[0][kt], b=lazy[1][kt];
	if(k==1&&b==0)return;
	ll m=(l+r)>>1;
	//乘法标记 
	lazy[0][kt<<1]*=lazy[0][kt];
	lazy[0][kt<<1|1]*=lazy[0][kt];
	//加法标记 
	lazy[1][kt<<1]=lazy[1][kt<<1]*lazy[0][kt]+lazy[1][kt];
	lazy[1][kt<<1|1]=lazy[1][kt<<1|1]*lazy[0][kt]+lazy[1][kt];
	
	sum2[kt<<1] = sum2[kt<<1]*k*k + 2*b*sum[kt<<1]+b*b*(m-l+1);
	sum2[kt<<1|1] = sum2[kt<<1|1]*k*k + 2*b*sum[kt<<1|1]+b*b*(r-m);
	sum[kt<<1] = sum[kt<<1]*k + b*(m-l+1);
	sum[kt<<1|1] = sum[kt<<1|1]*k + b*(r-m);
	
	lazy[0][kt]=1,lazy[1][kt]=0;
}
void UpDate(int L,int R, ll k, ll b, int l,int r,int kt){
	if(L<=l&&r<=R){
		lazy[0][kt]*=k;
		lazy[1][kt]=lazy[1][kt]*k+b;
		if(k!=1){
			sum[kt]*=k;
			sum2[kt]*=k*k;
		} 
		else if(b){
			sum2[kt]+=2*b*sum[kt]+b*b*(r-l+1);
			sum[kt]+=b*(r-l+1);
		}
		return;
	}
	PushDown(l,r,kt); 
	ll m=(l+r)>>1;
	if(L<=m)UpDate(L,R,k,b,l,m,kt<<1);
	if(R>m)UpDate(L,R,k,b,m+1,r,kt<<1|1);
	PushUp(kt);
}
ll Query(int L,int R,int l,int r,int op,int kt){
	if(L<=l&&r<=R){
		if(op==1)return sum[kt];
		else if(op==2)return sum2[kt];
	}
	if(lazy[0][kt]!=1||lazy[1][kt])PushDown(l,r,kt);
	ll m=(l+r)>>1,ans=0;
	if(L<=m)
		ans+=Query(L,R,l,m,op,kt<<1);
	if(m<R)
		ans+=Query(L,R,m+1,r,op,kt<<1|1);
	return ans;
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int op,l,r,x;
	cin>>N>>M;
	for(int i=1;i<=N;i++)cin >>a[i];
	Build(1,N,1);
	while(M--){
		cin >>op;
		if(op==1||op==2){
			cin >>l>>r;
			cout <<Query(l,r,1,N,op,1)<<'\n';
		}
		else {
			cin >>l>>r>>x;
			if(op==3)
			UpDate(l,r,x,0,1,N,1);
			else UpDate(l,r,1,x,1,N,1);
		}
	}
	return 0;
}

bug记录QWQ深刻了

(0.求sum2的那几个公式要写对,比如sum2*gif.latex?k^{2}后就不用再乘区间长度了,gif.latex?b^{2}要乘区间长度...(qwq我是fw)

1.sum2要用原来的sum更新!所以先更新sum2再更新sum!

2.注意ans开long long

3.Query里的ans+=Query(...和ans2+=Query...合作一个ans,否则一个Query里要调用两次Query,所需的时间成指数级增长,(也就是窝 t 了的原因QWQ)问题来了,为什么两种不同的操作可以合用同一个ans?不会混吗?明明sum和sum2是俩分开算的啊,引用学长的话就是:

“因为query里面把两种操作的懒标记都加起来了” “不管有几种操作最后都是一个和”

“线段树本来就是能够处理区间可叠加性质的一种数据结构”   

!!!再次拜谢学长学姐!

4.plus)加上这句话来缩短时间:ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);  顺带把很慢的 endl 也用 ' \n ' 替代 世界瞬间变快乐(了)超多.

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值