线段树题单记录
线段树题单记录
线段树的题都很板的,模板敲上去再改改就行
P3372 【模板】线段树 1
题目
为什么模板是绿题,还有下面那道
思路
首先我们要明白它为什么叫线段树:
OI Wiki 上的这张图很好理解:
从这张图也可以看出来,线段树的每个节点管辖的一个又一个的线段(区间),所以我们通俗地叫它线段树。
废话
这里只讲最简单的线段树,关于什么 \(ZKW\) 线段树、动态开点线段树 请自行了解。
普通线段树学完了好像那些奇奇怪怪的线段树优化更好理解
现在给出你一个数组的值,然后让你区间修改,区间求和 (当然树状数组也能做,就是麻烦了亿点),这就是线段树最基本的功能。当然,它也可以维护区间最值什么的,看下面的题目就知道了。
因为线段树的时空复杂度大约是树状数组和 \(st\) 表的 \(4\) 倍,所以能用树状数组或 \(st\) 表写的题目尽量不要用线段树去写。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
#define int long long
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct node{
int l,r;
int s;
int lt;
}t[N*4];
int n,m;
int a[N];
void pushup(int p){t[p].s=t[p*2].s+t[p*2+1].s;}
void pushdown(int p){
if(t[p].lt==0)
return;
t[p*2].s+=t[p].lt*(t[p*2].r-t[p*2].l+1);
t[p*2+1].s+=t[p].lt*(t[p*2+1].r-t[p*2+1].l+1);
t[p*2].lt+=t[p].lt;
t[p*2+1].lt+=t[p].lt;
t[p].lt=0;
}
void build(int p,int l,int r){
t[p].l=l;
t[p].r=r;
if(l==r){t[p].s=a[l];return;}
int mid=(l+r)>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
pushup(p);
}
void update(int p,int l,int r,int c){
if(t[p].l>=l&&t[p].r<=r){
t[p].lt+=c;
t[p].s+=c*(t[p].r-t[p].l+1);
return;
}
pushdown(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
update(p*2,l,r,c);
if(r>mid)
update(p*2+1,l,r,c);
pushup(p);
}
int query(int p,int l,int r){
if(t[p].l>=l&&t[p].r<=r)
return t[p].s;
int reu=0;
int mid=(t[p].l+t[p].r)>>1;
pushdown(p);
if(l<=mid)
reu+=query(p*2,l,r);
if(r>mid)
reu+=query(p*2+1,l,r);
return reu;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
int ty;
int x,y,k;
for(int i=1;i<=m;i++){
cin>>ty;
if(ty==1){
cin>>x>>y>>k;
update(1,x,y,k);
}
else{
cin>>x>>y;
cout<<query(1,x,y)<<endl;
}
}
return 0;
}
建树复杂度:\(O (NlogN)\)
查询复杂度:\(O (logN)\)
修改复杂度:\(O (logN)\)
代码解释
先说它优化时间复杂度的原理。
优化原理 && \(lt\)
重在 \(lt\)。
它是 \(LazyTag\) 的缩写,中文即懒标记。
它的作用很简单。
当我们修改到某一个节点,而这个节点被修改区间包含时,我们就可以直接修改该区间的值并 return
。这可以大大节省时间。
最极限的情况下一次也只需要遍历 \(logN\) 个点
但是由于它的儿子没有被修改,所以我们需要记录已经修改的值再 return
。
如果下一次经过这个节点,就先将 \(lt\) 给传递下去,以保证儿子的和是最新的。否则,可能你的某次修改就会被吞掉。
变量声明
结构体 \(node\):树的结点,包含左右端点和保存的信息什么的。
数组 \(t\):树的数组
数组 \(a\):原始数组
\(int\) 整型 \(n\):原始数组长度
\(int\) 整型 \(m\):操作个数
build
定义:void build (int p,int l,int r)
传入三个参数:节点编号 \(p\),该节点的左端点 \(l\) 和右端点 \(r\)。
然后对其进行赋值 t [p].l=l;t [p].r=r;
(有些题目可能要赋的值比较多)。
然后递归建树:
if(l==r){t[p].s=a[l];return;}
int mid=(l+r)>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
pushup(p);
如果说当前节点的左右端点编号相等,就证明它已经建到叶子节点了,就可以赋值 return
了。
否则,就将区间不严格对半拆开,然后递归建树,然后 pushup
。
然后你就看到了突然出现的 pushup
。
pushup
声明:void pushup (int p)
pushup
是用来在子节点更新完后更新父节点的。
其中 \(p\) 是节点编号,这道题中该节点的区间和等于它左右儿子的和的和。
有些题目可能要维护的东西比较多,比如下面的 P1471 方差 那道题。
它的作用很简单,就是在儿子节点修改后将修改值给向上推给父亲:{t [p].s=t [p*2].s+t[p*2+1].s;}
现在树建完了,然后该维护和查询了。
update
声明:void update (int p,int l,int r,int c)
传入 \(4\) 个参数:
当前节点 \(p\)、当前修改的左端点 \(l\)、当前修改的右端点 \(r\)、当前修改值 \(c\)。
如果说当前节点的左右端点已经被完全包含了,那么就在当前节点修改并 return
:
if(t[p].l>=l&&t[p].r<=r){
t[p].lt+=c;
t[p].s+=c*(t[p].r-t[p].l+1);
return;
}
否则,继续往下递归修改:
pushdown(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
update(p*2,l,r,c);
if(r>mid)
update(p*2+1,l,r,c);
pushup(p);
于是我们就看到了 pushdown
函数。
pushdown
声明:void pushdown (int p)
这个函数是用来进行儿子节点更新的,传入要进行 pushdown
操作的节点编号 \(p\),然后进行修改,就像这样:
{
if(t[p].lt==0)
return;
t[p*2].s+=t[p].lt*(t[p*2].r-t[p*2].l+1);
t[p*2+1].s+=t[p].lt*(t[p*2+1].r-t[p*2+1].l+1);
t[p*2].lt+=t[p].lt;
t[p*2+1].lt+=t[p].lt;
t[p].lt=0;
}
那个 if (t [p].lt==0) return;
是用来进行判断的:如果当前节点没有需要进行 pushdown
操作的懒标记就 return
。没有它也行,它主要是用来加快程序运行的。
这里也是题目设难点的一个重灾区,某些题目要考虑的情况很多,一不注意就 \(WA\) 了
现在我们来看查询 query。
query
声明:int query (int p,int l,int r)
传入三个参数:
当前节点编号 \(p\)、当前修改的左端点 \(l\)、当前修改的右端点 \(r\)。
和 update
差不多,query
的判断逻辑也是如果说当前节点的左右端点已经被查询区间完全包含了,那么就 return
当前节点的查询值:
if(t[p].l>=l&&t[p].r<=r)
return t[p].s;
否则就接着往下查询:
int reu=0;
int mid=(t[p].l+t[p].r)>>1;
pushdown(p);
if(l<=mid)
reu+=query(p*2,l,r);
if(r>mid)
reu+=query(p*2+1,l,r);
return reu;
至此,整个线段树的核心代码就讲完了。
可能有的人会比较好奇,pushdown
和 pushup
是个什么用的,为什么要在那些位置放他们 是的没错就是我朋友的问题。
关于 pushup
和 pushdown
为什么会有它俩:
用 OI Wiki 上的图画的
假如说我们某次修改了区间 [1,3](图中红色区间)。
现在我们要查询区间 [2,2] 其实就一个点(图中蓝色的点)。
然后你会发现它并没有被修改,所以在 query
中出现了 pushdown
,用来确保它的儿子节点是最新的。
那么它为什么没有 pushup
呢?
有也可以,但是由于它的儿子节点没有 被修改,所以我们用不到 pushup
。
那么这个 “被修改” 是什么意思呢?
显然,这里在它的儿子节点修改之前它已经修改过了,所以它不用修改。
那么 update
中为什么会同时有 pushup
和 pushdown
呢?
还是这张图。
假如说我们现在蓝色标记不是查询而是修改,即
我们某次修改了区间 [1,3](图中红色区间)。
现在我们要修改区间 [2,2](图中蓝色的点)。
然后我们会发现,它的值不是最新的,我们要把 [1,3] 区间的懒标记进行 pushdown
操作已保证修改前它的值是最新的。
那为什么还要有 pushup
操作呢?
和刚刚的 update
相对,这里它的子节点一定被修改了,而且这次修改没有在它自己身上体现,所以我们需要用 pushup
进行修改以确保它是最新的。
P3373 【模板】线段树 2
题目思路
这个不就是多个 \(lt\),原有 \(lt\) 功能不变,新的 \(lt\) 用来保存乘积吗,pushdown
的时候先 pushdown
乘积再 pushdown
和就行了。
这题还行,某 \(AT\) \(ABC\) 的题 MD 少 \(mod\) 了两下连 \(unsigned long long\) 都给我爆了(
关于为什么先 pushdown
乘积再 pushdown
和 显然 让我们来证一下:
首先,设其中三次修改分别为 乘、加、乘,三次修改值分别为 \(x\)、\(y\)、\(z\)。原数列为 \(A\),其中元素为 \(a\),长度为 \(N\)(简写 \(n\))。
那么修改后的和 \(NewSum\) 为:
我懒
\(NewSum\)
\(\xlongequal {} (a_1 \times x+y) \times z + \dots + (a_n \times x+y) \times z\)
\(\xlongequal {} a_1 \times x \times z + y \times z \dots + a_n \times x \times z+ y \times z\)
\(\xlongequal {} OldSum \times x \times z + n \times y \times z\)
所以我们要先更新乘法的懒标记再更新加法的懒标记。
代码
// Problem: P3373 【模板】线段树 2
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3373
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// By:CLydq
// AC Time:2024-06-04 15:51:02
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
#define int unsigned long long
struct node{
int l,r;
int s;
int clt,jlt;
}t[N*4];
int a[N];
int n,q,m;
void pushup(int p){t[p].s=t[p*2].s+t[p*2+1].s;}
void mod(int p){t[p].jlt%=m;t[p].clt%=m;t[p].s%=m;}
void pushdown(int p){
if(t[p].clt==1&&t[p].jlt==0)
return;
t[p*2].jlt*=t[p].clt,mod(p*2);
t[p*2].clt*=t[p].clt,mod(p*2);
t[p*2].s*=t[p].clt,mod(p*2);
t[p*2+1].jlt*=t[p].clt,mod(p*2+1);
t[p*2+1].clt*=t[p].clt,mod(p*2+1);
t[p*2+1].s*=t[p].clt,mod(p*2+1);
t[p*2].jlt+=t[p].jlt,mod(p*2);
t[p*2].s+=t[p].jlt*(t[p*2].r-t[p*2].l+1),mod(p*2);
t[p*2+1].jlt+=t[p].jlt,mod(p*2+1);
t[p*2+1].s+=t[p].jlt*(t[p*2+1].r-t[p*2+1].l+1),mod(p*2+1);
mod(p*2),mod(p*2+1),mod(p);
t[p].jlt=0,t[p].clt=1;
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r,t[p].clt=1,t[p].jlt=0;
if(l==r){t[p].s=a[l];return;}
int mid=(l+r)>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
pushup(p);
}
void update(int p,int l,int r,int k,int ty){
if(t[p].l>=l&&t[p].r<=r){
if(ty==1){
t[p].clt*=k,t[p].clt%=m;
t[p].jlt*=k,t[p].jlt%=m;
t[p].s*=k,t[p].s%=m;
}
else{
t[p].jlt+=k,t[p].jlt%=m;
t[p].s+=k*(t[p].r-t[p].l+1),t[p].s%=m;
}
return;
}
pushdown(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
update(p*2,l,r,k,ty);
if(r>mid)
update(p*2+1,l,r,k,ty);
pushup(p);
}
int query(int p,int l,int r){
if(t[p].l>=l&&t[p].r<=r)
return t[p].s%m;
int reu=0;
int mid=(t[p].l+t[p].r)>>1;
pushdown(p);
if(l<=mid)
reu+=query(p*2,l,r),reu%=m;
if(r>mid)
reu+=query(p*2+1,l,r),reu%=m;
return reu%m;
}
signed main(){
cin>>n>>q>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
int x,y,k,ty;
for(int i=1;i<=q;i++){
cin>>ty>>x>>y;
if(ty==3)
cout<<query(1,x,y)%m<<endl;
else
cin>>k,update(1,x,y,k,ty);
}
return 0;
}
P1198 [JSOI2008] 最大数
思路
这里应该用线段树动态开点做的。
but,它并没有强制要求在线。
也就是说,我们可以将所有操作记录下来,然后将 \(A\) 操作的数量进行计数,从而得到线段树长度 \(n\) 进行离线操作。
当时写的动态开点线段树出了亿点小 bug。
这里也体现了线段树的另外一个作用:维护区间最值,修改 pushup
、 query
和 update
就行。
代码
// Problem: P1198 [JSOI2008] 最大数
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1198
// Memory Limit: 128 MB
// Time Limit: 1000 ms
//
// By:CLydq
// AC Time:2024-06-04 17:31:05
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
struct node{
int l,r;
int s;
}t[N*4];
int m,d,n;
pair <char,int> ps[N];
void pushup(int p){ t[p].s = max( t[p*2].s , t[p*2+1].s ) ; }
void build(int p,int l,int r){
t[p].l=l,t[p].r=r,t[p].s=INT_MIN;
if(l==r)
return;
int mid = ( l + r ) >> 1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
}
void update(int p,int l,int r,int c){
if(t[p].l>=l&&t[p].r<=r){
t[p].s=max(t[p].s,c);
return;
}
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
update(p*2,l,r,c);
if(r>mid)
update(p*2+1,l,r,c);
pushup(p);
}
int query(int p,int l,int r){
if(t[p].l>=l&&t[p].r<=r)
return t[p].s;
int reu=INT_MIN;
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
reu=max(query(p*2,l,r),reu);
if(r>mid)
reu=max(query(p*2+1,l,r),reu);
return reu;
}
signed main(){
cin>>m>>d;
for(int i=1;i<=m;i++)
cin >> ps[i].first >> ps[i].second ,
n += ps[i].first == 'A' ? 1 : 0 ;
build(1,1,n);
int len = 0;
int tm=0;
for(int i=1;i<=m;i++)
if(ps[i].first == 'A'){
len ++ ;
update(1,len,len,(ps[i].second+tm)%d);
}
else{
tm=query(1,len-ps[i].second+1,len);
cout<<tm<<endl;
}
return 0;
}
[ABC357F] Two Sequence Queries
思路
关于为什么要把这个题给中间插过来:
因为它真的仅仅只是多维护了一点东西,就是个模板的难度,没什么思路。
但是它的数据是真的 unsigned long long 都给我爆了
单独维护三个和:
\(A\) 数组和
\(B\) 数组和
\(A\) 数组中每个元素 \(\times\) \(B\) 数组中每个对应位置的和
和两个 \(lt\):
对 \(A\) 数组进行更新的 \(lt\)
对 \(B\) 数组进行更新的 \(lt\)
然后就行了。
这个题告诉我们一个道理:
又时候 \(mod\) 一下不行,你得多 \(mod\) 一下,不然 \(unsigned\) \(long\) \(long\) 都能给你干爆。
代码
// LUOGU_RID: 161842966
// Problem: F - Two Sequence Queries
// Contest: AtCoder - SuntoryProgrammingContest2024(AtCoder Beginner Contest 357)
// URL: https://atcoder.jp/contests/abc357/tasks/abc357_f
// Memory Limit: 1024 MB
// Time Limit: 5000 ms
//
// By:CLydq
// AC Time:2024-06-08 20:24:02
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
const int mod=998244353;
#define int unsigned long long
struct node{
int l,r;
int s,sa,sb;
int lta,ltb;
}t[N*4];
int n,q;
int a[N],b[N];
inline void Gomod(int p){
t[p].s%=mod;
t[p].sa%=mod;
t[p].sb%=mod;
t[p].lta%=mod;
t[p].ltb%=mod;
}
inline void pushup(int p){
t[p].s=t[p*2].s+t[p*2+1].s;
t[p].sa=t[p*2].sa+t[p*2+1].sa;
t[p].sb=t[p*2].sb+t[p*2+1].sb;
Gomod(p);
}
inline void upa(int p,int lt){
Gomod(p);
t[p].lta+=lt;
t[p].sa+=lt*(t[p].r-t[p].l+1);
t[p].s+=t[p].sb*lt;
Gomod(p);
}
inline void upb(int p,int lt){
Gomod(p);
t[p].ltb+=lt;
t[p].sb+=lt*(t[p].r-t[p].l+1);
t[p].s+=t[p].sa*lt;
Gomod(p);
}
void pushdown(int p){
if(!t[p].lta&&!t[p].ltb)
return;
Gomod(p);
upa(p*2,t[p].lta);
upb(p*2,t[p].ltb);
upa(p*2+1,t[p].lta);
upb(p*2+1,t[p].ltb);
t[p].lta=0;
t[p].ltb=0;
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r){
t[p].sa=a[l];
t[p].sb=b[l];
t[p].s=a[l]*b[r];
Gomod(p);
return;
}
int mid=(l+r)>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
pushup(p);
Gomod(p);
}
void update(int p,int l,int r,int x,int ty){
if(t[p].l>=l&&t[p].r<=r){
if(ty==1)
upa(p,x);
else
upb(p,x);
return;
}
int mid=(t[p].r+t[p].l)>>1;
pushdown(p);
if(l<=mid)
update(p*2,l,r,x,ty);
if(r>mid)
update(p*2+1,l,r,x,ty);
pushup(p);
}
int query(int p,int l,int r){
if(t[p].l>=l&&t[p].r<=r)
return t[p].s;
pushdown(p);
int reu=0,mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
reu+=query(p*2,l,r),reu%=mod;
if(r>mid)
reu+=query(p*2+1,l,r),reu%=mod;
return reu%mod;
}
inline void solve(){
int ty,l,r,x;
cin>>ty>>l>>r;
if(ty!=3)
cin>>x,update(1,l,r,x,ty);
else
cout<<query(1,l,r)%mod<<endl;
}
signed main(){
cin>>n>>q;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
cin>>b[i];
build(1,1,n);
for(int i=1;i<=q;i++)
solve();
return 0;
}
P1471 方差
思路
这题蓝就蓝在那个方差公式的推导。
谁说这个公式是初中推了的,我们都没推
现在我们来推一下:
\([(X_1 - \overline{X})^2 + \dots + (X_n - \overline{X})^2] / n\)
\(\xlongequal {} [X_1^2 + \dots + X_n^2 - 2 \times (X_1 + \dots X_n) \times \overline{X} + n \times \overline {X}^2] / n\)
\(\xlongequal {} \overline{X} - 2 \times \overline{X} + (X_1^2 + \dots X_n^2) / n\)
\(\xlongequal {} (X_1^2 + \dots X_n^2) / n - \overline{X}\)
然后我们就可以看出来,这里如果要进行方差的查询就需要额外维护一个平方和,公式推导参考上面模板 \(2\)。
oh 对了别忘记数据是实数
Code
// Problem: P1471 方差
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1471
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// By:CLydq
// AC Time:2024-06-19 20:28:06
#include <bits/stdc++.h>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
const int N=1e5+5;
struct node{
int l,r;
int len;
long double s;
long double pfh;
long double pjs;
long double lt;
}t[N<<2];
int n,m;
long double a[N];
void pushup(int p){
t[p].s=t[p*2].s+t[p*2+1].s;
t[p].pfh=t[p*2].pfh+t[p*2+1].pfh;
t[p].pjs=t[p].s/t[p].len;
}
void pushdown(int p){
if(!t[p].lt)
return;
t[p*2].lt+=t[p].lt;
t[p*2].pjs+=t[p].lt;
t[p*2].pfh+=2*t[p].lt*t[p*2].s+t[p].lt*t[p].lt*t[p*2].len;
t[p*2].s+=t[p].lt*t[p*2].len;
t[p*2+1].lt+=t[p].lt;
t[p*2+1].pjs+=t[p].lt;
t[p*2+1].pfh+=2*t[p].lt*t[p*2+1].s+t[p].lt*t[p].lt*t[p*2+1].len;
t[p*2+1].s+=t[p].lt*t[p*2+1].len;
t[p].lt=0;
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r,t[p].len=t[p].r-t[p].l+1;
if(l==r){
t[p].s=a[l];
t[p].pfh=a[l]*a[r];
t[p].pjs=a[r];
return;
}
int mid=(l+r)>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
pushup(p);
}
void update(int p,int l,int r,long double k){
if(t[p].l>=l&&t[p].r<=r){
t[p].pfh+=2*k*t[p].s+k*k*t[p].len;
t[p].s+=t[p].len*k;
t[p].pjs+=k;
t[p].lt+=k;
return;
}
pushdown(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid)
update(p*2,l,r,k);
if(r>mid)
update(p*2+1,l,r,k);
pushup(p);
}
long double query(int p,int l,int r,int ty){
if(t[p].l>=l&&t[p].r<=r)
return ty==2 ? t[p].s : t[p].pfh;
int mid=(t[p].l+t[p].r)>>1;
long double reu=0;
pushdown(p);
if(l<=mid)
reu+=query(p*2,l,r,ty);
if(r>mid)
reu+=query(p*2+1,l,r,ty);
return reu;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
int ty,l,r;
long double k;
for(int i=1;i<=m;i++){
cin>>ty>>l>>r;
if(ty==1){
cin>>k;
update(1,l,r,k);
}
else if(ty==2)
printf("%.4lf\n",(double)query(1,l,r,ty)/(r-l+1));
else{
int len=r-l+1;
long double r1=query(1,l,r,2);
long double r2=query(1,l,r,3);
long double ans=(r2-r1*r1/len)/len;
printf("%.4lf\n",(double)ans);
}
}
return 0;
}