(zhx)线段树 (结构体线段树)

(zhx)线段树 (结构体线段树)

前言

由于退役时间太久,线段树都忘了,好好复习一下,发现许多误点,特意详细的写一下,方便下次复习

使用原因

以前也是用普通的线段树,长啥样忘记了,但是因为zhx线段树特别好理解,并且需要更改的地方非常少,很方便

线段树1,区间加,区间求和

以下代码有详细标注,不xi讲

/*
	尝试第一次复建线段树
	15:41 start 
	16:05 end 
	开始该..... 
	出现的问题是 m=l+r<<1 乘以 2 而不是 除以 2 
	第一次提交 成功 RE 原因线段树数组要开到平常的四倍
	第二次提交 AC 
	16:14
*/
#include<bits/stdc++.h>
#define root 1,n,1 //方便调用 
#define lson l,m,rt<<1 //注意是 l 不是 1 lson表示左子树 
#define rson m+1,r,rt<<1|1//同理右子树 
#define int long long 
using namespace std;
int a[100009];
int read(){int x;scanf("%lld",&x);return x;}
struct node//结构体线段树 
{
	int l,r,col,sum;// 分别记录 区间左右端点,加号标记,区间总和 
	node() {l=r=col=sum=0;}//别忘记初始化 
	void init(int l_,int r_) {l=l_,r=r_,sum=a[l];}//init函数用来赋予线段树实际意义 
}z[400018];//超级注意 线段树的空间大小要比平时大四倍 
node operator +(const node &l,const node &r)//从在运算符,方便updata 
{
	node p;
	p.l=l.l,p.r=r.r,p.sum=l.sum+r.sum;//其实就是区间合并 
	p.col=0;
	return p;
}
void color(int rt,int v) {z[rt].col+=v; z[rt].sum+=(z[rt].r-z[rt].l+1)*v;}//加法标记函数,线段树中的常规操作,为防止用时太多,不必讲线段树上对应每一个节点都更新,打个标记,使用时下穿即可。 
void push(int rt)//下传标记 
{
	if (z[rt].col!=0)
	{
		color(rt<<1,z[rt].col);
		color(rt<<1|1,z[rt].col);
		z[rt].col=0;//下传后别忘记清零 
	}
	return;
}
void update(int rt) {z[rt]=z[rt<<1]+z[rt<<1|1];}//非常简洁的updata 
void build(int l,int r,int rt)//建树 
{
	if (l==r) {z[rt].init(l,r);return;}//赋予线段树实际意义 
	int m=l+r>>1;
	build(lson);//构建左子树 
	build(rson);//构建有子树 
	update(rt);//向上合并区间, 
}
void modify(int l,int r,int rt,int nowl,int nowr,int v)//加法操作 
{
	//l,r,rt,表示当前所在的子树标号 rt, 所管理的区间为 l-r 
	// nowl, nowr, v 表示现在需要再 nowl-nowr 的区间内加上v 
	if (nowl<=l && r<=nowr) {color(rt,v);return;}//如果子树区间在所需区间内部,可进行加法标记操作 
	push(rt);//可有可无 求和的时候下传就可。 
	int m=l+r>>1;
	if (nowl<=m) modify(lson,nowl,nowr,v);//在 l-m 区间内有所需区间,深入更新 
	if (nowr>m) modify(rson,nowl,nowr,v);// 在 m+1-r 区间存在所需区间,深入更新 
	update(rt);//注意这里是 m+1-r 是右区间,不是 m-r,这样规定的原因是防止 m 被 modify 两边 
}
node query(int l,int r,int rt,int nowl,int nowr)//求和操作,返回结构体形,方便调用 
{
	if (nowl<=l && r<=nowr) return z[rt];//原理同 modify 
	push(rt);
	int m=l+r>>1;
	if (nowl<=m)
	{
		if (nowr>m) return query(lson,nowl,nowr)+query(rson,nowl,nowr);
		return query(lson,nowl,nowr);
	}
	return query(rson,nowl,nowr);
}
int n,m;
signed main()//下面是常规操作 
{
	n=read(),m=read();
	for (int i=1;i<=n;i++) a[i]=read();
	build(root);
	while (m--)
	{
		int opt,l,r,v;
		opt=read();
		if (opt==1)
		{
			l=read(),r=read(),v=read();
			modify(root,l,r,v);
		}
		else 
		{
			l=read(),r=read();
			node q=query(root,l,r);
			cout<<q.sum<<endl;
		}
	}
	return 0;
}

线段树2

和线段树1不同的地方是存在一个乘法,可以简单的想到,多一个乘法标记不就完事了?但写完后发现,有加有乘,是有顺序的,不能简单的标记上,最后下穿。

现在出现第一个问题:如何实现乘法加法的顺序化

显然如果真的顺序化,那就成暴力了。

一种想法,在乘法的时候,现将加法标记下传,然后乘法标记。可是你发现 你调用的下传函数 push 中 就有你现在考虑的乘法标记函数 color2 ,这样就形成了环形调用,显然会报错

但是如果 push 中只有 color1 就不会形成环形调用,在乘法操作之前,提前将col1传下去,对子树的sum进行更新,然后再下传乘法操作,这样确实可以实现顺序化。我试试。不行

void color2(int rt,int v) 
{
	spcial_push(rt);
	z[rt].col2=(z[rt].col2*v)%P;
	z[rt].sum=z[rt].sum*v%P;
//	z[rt].col1=(z[rt].col1%P*v)%P;
}
void spcial_push(int rt)// 
{
	if (z[rt].col1!=0)
	{
		color1(rt<<1,z[rt].col1);
		color1(rt<<1|1,z[rt].col1);
		z[rt].col1=0;
	}
	return;
} 
void push(int rt)
{
	if (z[rt].col1!=0)
	{
		color1(rt<<1,z[rt].col1);
		color1(rt<<1|1,z[rt].col1);
		z[rt].col1=0;
	}
	if (z[rt].col2!=1)
	{
		color2(rt<<1,z[rt].col2);
		color2(rt<<1|1,z[rt].col2);
		z[rt].col2=1;
	}
	return;
}

样例过了,但是爆零,试试下面这组数据

7 8 107
4 10 8 5 9 3 6 
2 3 4 2
2 2 6 7
3 4 5 
1 2 7 9
2 2 7 4
3 3 3
3 2 4
3 2 3

通过逐步调试我们可以发现,对于 3 3 3 的访问操作,在线段树中

5 号子树(l-r=3-4)的 col1=4,col2=9,在 query 操作时,push函数会先传递加法,再传递乘法,这和数据中的操作恰好相反,尽管我们实现了在给出乘法操作时将加法标记下传,可以实现顺序化,但是下穿并不是彻底的,

举个简单的例子,这里用图,由于没带鼠标,回家画画,再加上。其实就是出现上面 5 号子树的情况。

针对同时下传的问题,

思考,为什么错,

是因为要么就是应该先乘后加,要么就是先加后乘

如果 col1 本身就已经被乘法操作了,换句话说,在进行乘法操作的时候,同时也对加法标记进行操作,这样就算之后再有加法操作,之前的加法标记已经实现了乘法操作,

那么此刻如果我先下传乘法操作,在下传加法操作,是不是就是答案,即强行先乘后加。

想想为什么反过来不行,为什么我不在需要考虑顺序?

反过来不对是显然的,因为co1在父亲节点col2下传时就已经实现乘法操作,如果先加后乘,就会出现col1被乘以两次。多次下传就会被操作多次。

所以实现的操作时,在col2下传的时候对 col1 进行乘法操作,在同时下传的时候,先乘后加。

代码如下

void color2(int rt,int v) 
{
	z[rt].col2=(z[rt].col2*v)%P;
	z[rt].sum=z[rt].sum*v%P;
	z[rt].col1=(z[rt].col1%P*v)%P;//对 col1 进行乘法操作
}
void push(int rt)//实现先乘后加
{
	if (z[rt].col2!=1)
	{
		color2(rt<<1,z[rt].col2);
		color2(rt<<1|1,z[rt].col2);
		z[rt].col2=1;
	}
	if (z[rt].col1!=0)
	{
		color1(rt<<1,z[rt].col1);
		color1(rt<<1|1,z[rt].col1);
		z[rt].col1=0;
	}
	
	return;
}

本题的大坑已经成功跳过,接下来就是一些小地方,自己同样也花费了不少时间才找到,不得不说,调线段树真的好痛苦

请问 col2 作为乘法标记的初始化应该是多少?

是不是很容易忽略。很容易照着线段树一的思路将其初始化成 0。

请问 多次乘法操作时,col2 的更新是相加还是相乘?

显然相乘,但是我第一遍就写的相加,原因还是照着加法标记写的。

后言

复习个线段树用了两天多一点,从重新解读 zhx 的结构体线段树,到自己应用结构体线段树,再到调试代码的复习,再到暴力代码,造数据代码,对拍代码,统一复习,自己不仅仅对结构体线段树有更深的理解,而且还复习了对拍知识。还是不戳的!

礼物

线段树2代码+做题经历

/*
	线段树2
Day1
	16:14 开始敲代码 
	16:52 开始调代码
	17:21 还在调代码,图书馆开始赶人。。。 
Day 2	
	9:03 继续想,如何才能使得乘法和加法标记有顺序 
	9:13 过样例,成功爆零 
	10:00 写完对拍
	10:22 对拍拍出错误,继续更改无语。。。。,不想调线段树,疯了!!!!!! 
	10:35 疯了!!!!
	11:06 30pts 其他都WA 疯了!挑不出来了,出错的问题在于因为已经对加号标记进行乘法的贡献操作,所以push时,应先下穿乘法标记,让乘法标记仅对未添加加法标记的sum进行操作,
	如果反向,那么之前的加法标记会乘以两边。 
	16:17 超级困,没办法,开始调试,准备发疯..... 
	16:41 终于对拍过了 错因:
	如果有两次以上的是乘法,那么乘法标记是相乘,而不是相加
	因为我用的是相加,在改过以后,发现乘法标记失效,一直是0;
	原因是初始化的时候乘法标记应该是 1 而不是 0;
	因为开始时用的相加,导致这里的 0 的错误也没有发现,
Day3
	15:15 更改代码,有新思路,也就是老思路,在乘法之前先下传加法 
	15:54 发现不足之处。 
	16:13 完成笔记大部分 
*/ 
#include<bits/stdc++.h>
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define int long long 
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=200009;
int a[B];
int n,m,P;
struct node
{
	int l,r,sum,col1,col2;
	node() {l=r=sum=col1=0;col2=1;}
	void init(int l_,int r_) {l=l_,r=r_,sum=a[l];col1=0;col2=1;}
}z[B<<2];
node operator +(const node &l,const node &r)
{
	node p;
	p.l=l.l,p.r=r.r,p.sum=(l.sum%P+r.sum%P)%P,p.col1=0,p.col2=1;
	return p;
}
void color1(int rt,int v) {z[rt].col1=(z[rt].col1%P+v)%P;z[rt].sum=(z[rt].sum%P+(z[rt].r-z[rt].l+1)*v%P)%P;}
void color2(int rt,int v) 
{
	z[rt].col2=(z[rt].col2*v)%P;
	z[rt].sum=z[rt].sum*v%P;
	z[rt].col1=(z[rt].col1%P*v)%P;
}
void push(int rt)
{
	if (z[rt].col2!=1)
	{
		color2(rt<<1,z[rt].col2);
		color2(rt<<1|1,z[rt].col2);
		z[rt].col2=1;
	}
	if (z[rt].col1!=0)
	{
		color1(rt<<1,z[rt].col1);
		color1(rt<<1|1,z[rt].col1);
		z[rt].col1=0;
	}
	
	return;
}
void update(int rt) {z[rt]=z[rt<<1]+z[rt<<1|1];}
void modify1(int l,int r,int rt,int nowl,int nowr,int v)
{
	if (nowl<=l && nowr>=r) {color1(rt,v);return;}
	push(rt);
	int m=l+r>>1;
	if (nowl<=m) modify1(lson,nowl,nowr,v);
	if (nowr>m) modify1(rson,nowl,nowr,v);
	update(rt);
}
void modify2(int l,int r,int rt,int nowl,int nowr,int v)
{
	if (nowl<=l && nowr>=r) {color2(rt,v); return;}
	push(rt);
	int m=l+r>>1;
	if (nowl<=m) modify2(lson,nowl,nowr,v);
	if (nowr>m) modify2(rson,nowl,nowr,v);
	update(rt);
} 
void build(int l,int r,int rt)
{
	if (l==r) {z[rt].init(l,r);return;}
	int m=l+r>>1;
	build(lson);
	build(rson);
	update(rt);
}
node query(int l,int r,int rt,int nowl,int nowr)
{
	if (nowl<=l && nowr>=r) return z[rt];
	push(rt);
	int m=l+r>>1;
	if (nowl<=m)
	{
		if (nowr>m) 
		{
			node q1=query(lson,nowl,nowr);
			node q2=query(rson,nowl,nowr);
			return q1+q2;
		}
		return query(lson,nowl,nowr);
	}
	return query(rson,nowl,nowr);
}
signed main()
{
//	freopen("data.in","r",stdin);
//	freopen("zj.out","w",stdout);
	n=read(),m=read(),P=read();
	for (int i=1;i<=n;i++) a[i]=read();
	build(root);
	while (m--)
	{
		int opt,l,r,k;
		cin>>opt;
		if (opt==1)
		{
			cin>>l>>r>>k;
			modify2(root,l,r,k); 
		}
		else if (opt==2)
		{
			cin>>l>>r>>k;
			modify1(root,l,r,k);
		}
		else
		{
			cin>>l>>r;
			cout<<query(root,l,r).sum%P<<endl; 
		}
//		printf("1=%lld 2=%lld 3=%lld 4=%lld 5=%lld 6=%lld\n",z[8].sum,z[9].sum,z[5].sum,z[12].sum,z[13].sum,z[7].sum);
	}
	return 0;
}
/*

6 5 1007
5 2 5 4 9 6 
1 5 6 1
1 4 5 4
1 6 6 4
1 1 5 2
3 3 6 

5 5 100000007
1 5 4 2 3
3 2 4
2 2 3 2
3 3 4
2 1 5 1
3 1 4

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
*/

线段树2暴力代码(bl)

/*
	线段树2 暴力 
*/ 
#include<bits/stdc++.h>
#define int long long 
using namespace std;
int read() {int x;scanf("%lld",&x);return x;}
int n,m,p;
int a[200009]; 
signed main()
{
	freopen("data.in","r",stdin);
	freopen("bl.out","w",stdout);
	n=read(),m=read(),p=read();
	for (int i=1;i<=n;i++) a[i]=read();
	while (m--)
	{
		int opt,l,r,k;
		opt=read();
		if (opt==1)
		{
			cin>>l>>r>>k;
			for (int i=l;i<=r;i++)
			{
				a[i]=(a[i]*k)%p;
			}
		}
		else if (opt==2)
		{
			cin>>l>>r>>k;
			for (int i=l;i<=r;i++)
			{
				a[i]=(a[i]+k)%p;
			} 
		}
		else 
		{
			cin>>l>>r;
			int sum=0;
			for (int i=l;i<=r;i++) 
			{
				sum=(sum+a[i])%p;
			}
			cout<<sum<<endl;
		}
	}
}
/*
8 3 107
5 8 5 6 5 1 2 2 
2 1 6 3
1 3 7 8
3 3 5 
*/

线段树2造数据代码(data)

#include<bits/stdc++.h>
using namespace std;
int n,m,p;
int main()
{
//	freopen("data.in","r",stdin);
	freopen("data.in","w",stdout);
	srand(time(0));
	n=rand()%10+1;
	m=rand()%5+1;
	p=107;
	printf("%d %d %d\n",n,m+3,p);
	for (int i=1;i<=n;i++) 
	{
		int x=rand()%10+1;
		printf("%d ",x);
	}
	printf("\n");
	int s=3;
	while (m--)
	{
		int opt,l,r,k;
		opt=rand()%3+1;
		printf("%d ",opt);
		l=rand()%n+1;
		while (1)
		{
			r=rand()%n+1;
			if (r>=l) break;
		}
		printf("%d %d ",l,r);
		if (opt!=3)
		{
			k=rand()%10+1;
			printf("%d\n",k);
		}
		else printf("\n");
	}
	while (s--)
	{
		int l,r;
		printf("%d ",3);
		l=rand()%n+1;
		while (1)
		{
			r=rand()%n+1;
			if (r>=l) break;
		}
		printf("%d %d\n",l,r); 
	}
}
/*
9 2 8
5 7 6 9 5 2 2 3 3
2 1 6 2
2 6 7 1
*/ 

线段树2比较代码(bj)

#include<bits/stdc++.h>
using namespace std;
int main()
{
	while (1)
	{
		system("data.exe");
		system("zj.exe");
		system("bl.exe");
		if (system("fc zj.out bl.out")) break;
	}
} 
/*
6 7 1007
5 2 5 4 9 6 
1 5 6 1
2 3 5 9
1 4 5 4
1 6 6 4
1 1 5 2
2 5 5 2
3 3 6 
*/ 

还有几张调试的图片,也放上吧,挺壮观的~~

还没动,明天搞。

posted @ 2023-06-30 17:20  zxsoul  阅读(37)  评论(1编辑  收藏  举报