线段树

线段树是一种维护区间的数据结构,与树状数组类似,其支持区间的修改与查询
树状数组优缺点:
优:时空复杂度好,码量小
缺:对于解决区间问题有局限性
线段树优缺点:
优:更加通用,可轻松解决用树状数组难以解决的问题
缺:时空复杂度较高,码量较大
线段树三大操作:
1.建树
我们的建树方式是:若p为父节点,p2为左子节点,p2+1为右子节点
定义结构体变量ST(Segment Tree)保存我们需要的当前节点,左右子节点以及当前节点所需要的信息,所需要维护的信息在建树时完成初始化
Code:以最大值为例进行建树

点击查看代码
void build(int p.int l,int r){
   t[p].l=l;//保存左节点
   t[p].r=r;//保存右节点
   if(l==r){//l==r是区间中元素唯一情况,对应线段树中叶节点
      t[p].dat=a[l];//元素唯一,直接保存
   }
   int mid=(l+r)/2;//二分建立左右节点,保存相应区间
   build(p*2,l,mid);//p*2为左子节点,对应保存靠左侧的区间
   build(p*2+1,mid+1,r)//p*2+1为右子节点,对应保存右区间,注意起点为mid+1防止重复
   t[p].dat=max(t[p*2].dat,t[p*2+1].data)//两段区间对应的最大值即为该区间最大值,向上递归返回信息
}
2.修改

单点修改,从整个区间递归查找,查找到后修改该区间的值,同时
不要忘记向上递归修改相关节点的值!
Code:仍以最大值为例

点击查看代码
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);
    }
    else{
        change(p*2+1,x,v);
    }
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}
3.查询

1.区间最值
例题:
延绵的山峰
求区间最值(不修改)
题面:有一座延绵不断、跌宕起伏的山,最低处海拔为0,最高处海拔不超过8848米,从这座山的一端走到另一端的过程中,每走1米海拔就升高或降低1米。有Q个登山队计划在这座山的不同区段登山,当他们攀到各自区段的最高峰时,就会插上队旗。请你写一个程序找出他们插旗的高度。

自打的暴力非常弱
#include <bits/stdc++.h>
using namespace std;
const int p=1e6+10;
struct node{
	int id;
	int exp;
}a[p];
int q,n,x,y;
inline int read(){
   int s=0,w=1;
   char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
   while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
   return s*w;
}
inline void in(){
	n=read();
	for(register int i=0;i<=n;i++){
		a[i].exp=read();
		a[i].id=i;
	}
	q=read();
} 
bool cmp(const node &x,const node &y){
	return x.exp>y.exp;
}
void work(){
	int ans;
	sort(a,a+n,cmp);//这里离散化找最大值
	for(register int i=1;i<=q;i++){
		x=read();
		y=read();
		for(register int j=0;j<=n;j++){//最坏情况下就是qn,当然最好直接是个q。。。
			if(x<=a[j].id&&y>=a[j].id){
				printf("%d\n",a[j].exp);
				break;
			}
		}
	}
}
int main(){
	in();
	work();
	return 0;
}
正解代码(线段树)
# include <bits/stdc++.h>
#define lson (x<<1)
#define rson (x<<1|1)
#define mid ((z+y)>>1)
#define up c[x].sum=max(c[lson].sum,c[rson].sum)
using namespace std;
inline int r(){
	int x=0,flag=1;char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')flag=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
   return x*flag;	
}

const int da=1e6+1;int a[da];int n , m;
struct C{
	int z, y, sum;
}c[da*4];
int cha (int x,int z,int y){
	 if(c[x].z>y||c[x].y<z)return 0;
	 if(z<=c[x].z&&c[x].y<=y){return c[x].sum;}
	  int ret=max(cha(lson,z,y),cha(rson,z,y));
	  return ret;
}
void build(int x,int z,int y){
	c[x].z=z,c[x].y=y;
	if(z==y){c[x].sum=a[z];return ;}
	build (lson,z,mid);build (rson,mid+1,y);
	up;
}


int main (){
	 n=r();for(int i=0;i<=n;i++){a[i]=r();}build (1,0,n);
	 m=r();
	 for(int i=1;i<=m;i++){
	 	int ja=r(),jb=r();
	 	printf("%d\n",cha(1,ja,jb));
	 }
	
	return 0;
}

2.区间求和
如果直接修改和会很麻烦,而且可能查询不到,于是,我们有了一个新的操作,叫
标记延迟下传
修改时先打上标记,然后在查询将大区间的标记打到小区间(只打一次),最后传到单点上,由于不是立即下传增大的值(即不是立即修改区间内元素的值),而是打成标记,
也叫标记延迟下传
还有个标记永久化蒟蒻不会,咕了
打个模板

加和
#include <bits/stdc++.h>//线段树维护区间求和
using namespace std;//参考了蓝书自打一遍
#define ll long long //不开long long见祖宗
const int p=1e5+10;//数据规模
int a[p+10];//存原始数据
struct st{//线段树结构体
	int l;//记录左端点
	int r;//记录右端点
	ll p;//记录和
	ll add;//记录下传标记,延迟下传
}t[4*p];//数据规模:线段树开4倍大小
void build(int n,int l,int r){//建树:n:节点编号,l:左端点,r:右端点
	t[n].l=l;//存左端点
	t[n].r=r;//存右端点
	if(l==r){//存到单点,此时该编号节点对应叶节点
		t[n].p=a[l];//保存相应信息
		return ;//向上返回
	}
	int mid=(l+r)/2;//二分区间
	build(n*2,l,mid);//向下:左区间,该节点编号乘2为左区间节点编号
	build(n*2+1,mid+1,r);//同理,该编号*2+1为右编号
	t[n].p=t[n*2].p+t[n*2+1].p;//左右区间信息加和为该区间和,保存该区间信息并向上返回
}
void sp(int n){//延迟标记
	if(t[n].add){//有标记
		t[n*2].p+=t[n].add*(t[n*2].r-t[n*2].l+1);//左区间和更新
		t[n*2+1].p+=t[n].add*(t[n*2+1].r-t[n*2+1].l+1);//右区间和更新
		t[n*2].add+=t[n].add;//左区间标记下传
		t[n*2+1].add+=t[n].add;//右区间标记下传
		t[n].add=0;//下传标记完成,清空标记即可
	}
}
void change(int n,int x,int y,int z){//修改,n编号,x左端点,y右端点,z增幅
	if(x<=t[n].l&&y>=t[n].r){//区间规模小于修改规模
		t[n].p+=(ll)z*(t[n].r-t[n].l+1);//先把和加上
		t[n].add+=z;//打上延迟标记
		return ;//向上返回即可
	}
	sp(n);//及时下传标记,小区间更新要用
	int mid=(t[n].l+t[n].r)/2;//二分当前节点区间
	if(x<=mid){//左侧需要更新
		change(n*2,x,y,z);//在左区间(即n*2节点代表区间)执行更新操作,该次更新左右端点和增幅
	}
	if(y>mid){//右侧需要更新
		change(n*2+1,x,y,z);//同理,右侧区间更新
	}
	t[n].p=t[n*2].p+t[n*2+1].p;//维护新的区间和
}
ll ques(int n,int x,int y){//查询,n编号,x左端点,y右端点
	if(x<=t[n].l&&y>=t[n].r){//区间被查询范围覆盖
		return t[n].p;//直接返回区间信息即可
	}
	sp(n);//下传标记
	int mid=(t[n].l+t[n].r)/2;//二分区间
	ll ans=0;//定义非单点区间对应的返回值
	if(x<=mid){//左侧被覆盖
		ans+=ques(n*2,x,y);//查询左侧值并将左侧返回值加入返回值中
	}
	if(y>mid){//右侧被覆盖
		ans+=ques(n*2+1,x,y);//同理,加入右侧值
	}
	return ans;//向上返回值
}
int main(){//主函数
	int n,m;//定义题目变量
	scanf("%d%d",&n,&m);//输入
	for(int i=1;i<=n;i++){//依据区间规模进行循环,执行输入操作
		scanf("%d",&a[i]);//输入区间信息
	}
	build(1,1,n);//从1到n执行建树操作
	for(int i=1;i<=m;i++){//执行m条指令
		int cmd,x,y,z;//定义指令信息
		scanf("%d",&cmd);//输入指令类型
		if(cmd==1){//修改
			scanf("%d%d%d",&x,&y,&z);//输入修改信息
			change(1,x,y,z);//修改
		}
		else{//查询
			scanf("%d%d",&x,&y);//输入查询信息
			ll ans=ques(1,x,y);//定义答案
			printf("%lld\n",ans);//输出答案
		}
	}
	return 0;//结束,你会了
}

附赠一份乘积与加和的维护

乘积与加和
#include <bits/stdc++.h>//线段树维护乘积与加和
using namespace std;
#define ll long long 
const int maxn=1e5+10;
ll a[maxn],n,m,d;
struct st{
	ll s;
	ll m;
	ll a;
	ll l;
	ll r;
}t[maxn*4+10];
ll read(){
	ll x=0,y=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
			y=-1;
		}
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*y;
}
void build(int p,int l,int r){//建树
	t[p].l=l;
	t[p].r=r;
	t[p].m=1;
	if(l==r){
		t[p].s=a[l]%d;
		return ;
	}
	ll mid=(l+r)/2;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	t[p].s=(t[p*2].s+t[p*2+1].s)%d;
}
/*重点说一下标记操作:
1.乘积与加和的标记分开维护,否则人为增加思考量
2.由四则运算法则我们知道,先维护乘积下传再维护加和下传,加和时不用考虑维护乘积
3.乘积初始化为1,加和初始化为0
4.模数时原有数据最后进行取模,加和标记要先取模
5.变更标记要取模
6.开long long
*/
void spread(ll p){
	t[p*2].s=(ll)(t[p].m*t[p*2].s+((t[p*2].r-t[p*2].l+1)*t[p].a)%d)%d;
	t[p*2+1].s=(ll)(t[p].m*t[p*2+1].s+((t[p*2+1].r-t[p*2+1].l+1)*t[p].a)%d)%d;
	t[p*2].m=(ll)(t[p*2].m*t[p].m)%d;
	t[p*2+1].m=(ll)(t[p*2+1].m*t[p].m)%d;
	t[p*2].a=(ll)(t[p*2].a*t[p].m+t[p].a)%d;
	t[p*2+1].a=(ll)(t[p*2+1].a*t[p].m+t[p].a)%d;
	t[p].m=1;
	t[p].a=0;
}
void add(ll p,ll l,ll r,ll k){
	if(t[p].l>=l&&t[p].r<=r){
		t[p].a=(t[p].a+k)%d;
		t[p].s=(ll)(t[p].s+k*(t[p].r-t[p].l+1))%d;
		return ;
	}
	spread(p);
	t[p].s=(t[p*2].s+t[p*2+1].s)%d;
	ll mid=(t[p].l+t[p].r)/2;
	if(l<=mid){
		add(p*2,l,r,k);
	}
	if(r>mid){
		add(p*2+1,l,r,k);
	}
	t[p].s=(t[p*2].s+t[p*2+1].s)%d;
}
void mul(ll p,ll l,ll r,ll k){
	if(t[p].l>=l&&t[p].r<=r){
		t[p].a=(t[p].a*k)%d;
		t[p].m=(t[p].m*k)%d;
		t[p].s=(t[p].s*k)%d;
		return ;
	}
	spread(p);
	t[p].s=(t[p*2].s+t[p*2+1].s)%d;
	ll mid=(t[p].l+t[p].r)/2;
	if(l<=mid){
		mul(p*2,l,r,k);
	}
	if(r>mid){
		mul(p*2+1,l,r,k);
	}
	t[p].s=(t[p*2].s+t[p*2+1].s)%d;
}
ll ask(ll p,ll l,ll r){
	if(t[p].l>=l&&t[p].r<=r){
		return t[p].s;
	}
	spread(p);
	ll ans=0,mid=(t[p].l+t[p].r)/2;
	if(l<=mid){
		ans=(ans+ask(p*2,l,r))%d;
	}
	if(r>mid){
		ans=(ans+ask(p*2+1,l,r))%d;
	}
	return ans;
}
int main(){
	cin>>n>>m>>d;
	for(int i=1;i<=n;i++){
		a[i]=read();
	}
	build(1,1,n);
	for(int i=1;i<=m;i++){
		int ty=read();
		if(ty==1){
			ll cn=read(),cm=read(),cw=read();
			mul(1,cn,cm,cw);
		}else if(ty==2){
			ll cn=read(),cm=read(),cw=read();
			add(1,cn,cm,cw);
		}else {
			ll cn=read(),cm=read();
			cout<<ask(1,cn,cm)<<endl;
		}
	}
	return 0;
}

扫描线

扫描线求面积并
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int o=2222222;
struct myline{
    ll xa;
    ll xb;
    ll y;
    bool b;
    bool operator<(myline a)const{
        return y<a.y;
    }
}l[o];
struct node{
    int l;
    int r;
    ll len;
    ll cnt;
}t[o];
int lc,n,cnt;
ll a[o],xa,ya,xb,yb,ans;
void build(int p,int l,int r){//建树
	t[p].l=l;
	t[p].r=r;
	if(l==r){
		return ;
	}
	int mid=(l+r)/2;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
}
void pushup(int p){
    if(t[p].cnt){
        t[p].len=a[t[p].r+1]-a[t[p].l];
    }
    else{
        t[p].len=t[p*2].len+t[p*2+1].len;
	}
}
void change(int p,int l,int r,int v){
    if(t[p].l>=l&&t[p].r<=r){
        t[p].cnt+=v;
        pushup(p);
        return ;
    }
    int mid=(t[p].l+t[p].r)/2;
    if(l<=mid){
        change(p*2,l,r,v);
    }
    if(r>mid){
        change(p*2+1,l,r,v);
    }
    pushup(p);
}
void in(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%lld%lld%lld%lld",&xa,&ya,&xb,&yb);
        l[++lc].xa=xa;
        l[lc].xb=xb;
        l[lc].y=ya;
        l[lc].b=0;
        l[++lc].xa=xa;
        l[lc].xb=xb;
        l[lc].y=yb;
        l[lc].b=1;
        a[i]=xa;
        a[i+n]=xb;
    }
}
void pre(){
    sort(a+1,a+2*n+1);
    cnt=unique(a+1,a+2*n+1)-a-1;
    sort(l+1,l+2*n+1);
    build(1,1,cnt-1);
}
void work(){
    for(int i=1;i<=2*n;i++){
        myline nowline=l[i];
        if(i>1){
            ans+=t[1].len*(l[i].y-l[i-1].y);
        }
        if(nowline.b){
            change(1,lower_bound(a+1,a+cnt+1,nowline.xa)-a,lower_bound(a+1,a+cnt+1,nowline.xb)-a-1,-1);
        }
        else{
            change(1,lower_bound(a+1,a+cnt+1,nowline.xa)-a,lower_bound(a+1,a+cnt+1,nowline.xb)-a-1,1);
        }
    }
}
void out(){
    cout<<ans;
}
int main(){
    in();
    pre();
    work();
    out();
    return 0;
}
扫描线求周长并
#include <iostream>
#include <stdio.h>
#include <algorithm>
#define lson (x << 1)
#define rson (x << 1 | 1)
using namespace std;
const int MAXN = 2e4;
int n, X[MAXN << 1];
int x1, y1, x2, y2, pre = 0; /* 先初始化为 0 */

struct ScanLine {
	int l, r, h, mark;
	if(h == rhs.h)
		return mark > rhs.mark;
    return h < rhs.h;
//		注意!这里是后来被 hack 掉以后加上去的
//		在此感谢 @leprechaun_kdl 指出问题
//		如果出现了两条高度相同的扫描线,也就是两矩形相邻
//		那么需要先扫底边再扫顶边,否则就会多算这条边
//		这个对面积并无影响但对周长并有影响
//		hack 数据:2 0 0 4 4 0 4 4 8 输出应为:24
} line[MAXN];

struct SegTree {
	int l, r, sum, len, c;
//  c表示区间线段条数
    bool lc, rc;
//  lc, rc分别表示左、右端点是否被覆盖
//  统计线段条数(tree[x].c)会用到
} tree[MAXN << 2];

void build_tree(int x, int l, int r) {
	tree[x].l = l, tree[x].r = r;
	tree[x].lc = tree[x].rc = false;
	tree[x].sum = tree[x].len = 0;
	tree[x].c = 0;
	if(l == r)
		return;
	int mid = (l + r) >> 1;
	build_tree(lson, l, mid);
	build_tree(rson, mid + 1, r);
}

void pushup(int x) {
	int l = tree[x].l, r = tree[x].r;
	if(tree[x].sum) {
		tree[x].len = X[r + 1] - X[l];
		tree[x].lc = tree[x].rc = true;
		tree[x].c = 1;
//      做好相应的标记
	}
	else {
		tree[x].len = tree[lson].len + tree[rson].len;
		tree[x].lc = tree[lson].lc, tree[x].rc = tree[rson].rc;
		tree[x].c = tree[lson].c + tree[rson].c;
//      如果左儿子左端点被覆盖,那么自己的左端点也肯定被覆盖;右儿子同理
		if(tree[lson].rc && tree[rson].lc)
			tree[x].c -= 1;
//      如果做儿子右端点和右儿子左端点都被覆盖,
//      那么中间就是连续的一段,所以要 -= 1
	}
}

void edit_tree(int x, int L, int R, int c) {
	int l = tree[x].l, r = tree[x].r;
	if(X[l] >= R || X[r + 1] <= L)
		return;
	if(L <= X[l] && X[r + 1] <= R) {
		tree[x].sum += c;
		pushup(x);
		return;
	}
	edit_tree(lson, L, R, c);
	edit_tree(rson, L, R, c);
	pushup(x);
}

ScanLine make_line(int l, int r, int h, int mark) {
	ScanLine res;
	res.l = l, res.r = r,
	res.h = h, res.mark = mark;
	return res;
}
//  POJ 不这样做就会CE,很难受

int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
		line[i * 2 - 1] = make_line(x1, x2, y1, 1);
		line[i * 2] = make_line(x1, x2, y2, -1);
		X[i * 2 - 1] = x1, X[i * 2] = x2;
	}
	n <<= 1;
	sort(line + 1, line + n + 1);
	sort(X + 1, X + n + 1);
	int tot = unique(X + 1, X + n + 1) - X - 1;
	build_tree(1, 1, tot - 1);
	int res = 0;
	for(int i = 1; i < n; i++) {
		edit_tree(1, line[i].l, line[i].r, line[i].mark);
		res += abs(pre - tree[1].len);
		pre = tree[1].len;
//      统计横边
		res += 2 * tree[1].c * (line[i + 1].h - line[i].h);
//      统计纵边
	}
	res += line[n].r - line[n].l;
//  特判一下枚举不到的最后一条扫描线
	printf("%d", res);
	return 0;
}
啊这,刚打完一维???
posted @   2K22  阅读(74)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示