(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
*/
还有几张调试的图片,也放上吧,挺壮观的~~
还没动,明天搞。