线段树空间
假设有
n
n
n 个点,那么会分配
⌈
l
o
g
2
n
⌉
\lceil log_2n \rceil
⌈log2n⌉ 层空间的满二叉树来作为线段树的空间。
第
x
x
x 层最大的位置为
2
x
+
1
−
1
2^{x+1}-1
2x+1−1。第
x
+
1
x+1
x+1 层,最大的位置为
2
x
+
2
−
1
2^{x+2}-1
2x+2−1。
现在假设
2
x
<
n
≤
2
x
+
1
2^x < n \le 2^{x+1}
2x<n≤2x+1,那么 n 个节点的线段树会分配
x
+
1
x + 1
x+1 层。它的最大位置 p 应该满足:
2
x
+
1
−
1
<
p
≤
2
x
+
2
−
1
2^{x+1}-1 <p \le 2^{x+2}-1
2x+1−1<p≤2x+2−1。因此在极端情况下,p 应该取到 n 的 4 倍大小,才能够保证满足最大空间
线段树要点
- build 主要清空信息、最后回溯维护性质(max/min/sum)
- update 区间更新使用lazy,往下走需pushDown,往回走pushUp
- query 查询整棵树的信息就走到每一个叶节点。往下走需pushDown,没修改不需要回溯
动态开点
普通线段树都是直接开 4 倍空间,然后 l s ls ls 等于 r t × 2 rt \times 2 rt×2, r s rs rs 等于 r t × 2 + 1 rt\times 2 +1 rt×2+1。动态开点是用 l s ls ls 和 r s rs rs 两个数组来记录 r t rt rt 的左右孩子。每次需要更新的时候才动态地分配空间。
- 因此在查询的时候,会遇到 r t rt rt 为 0 的情况,也就是查询到某个点或者区间时不存在。此时就根据题意返回想要的数据即可
- 对于主席树,也是动态开点的一种,不过我写主席树,一般会建一棵空树,然后再动态开点,更新每一个版本。这样就不会访问到空点的情况。
用途:对于值域为 inf ,但是操作很少的情况,可以使用动态开点,每次操作最多会访问 l o g 2 ( i n f ) log_2 (inf) log2(inf) 个节点,一共 q 次操作,时空复杂度为 q l o g 2 ( i n f ) q log_2 (inf) qlog2(inf)
注意点:
- update 回溯的时候会访问到空点需判断
- query 的时候会访问到空点需判断
P3372 【模板】线段树 1
动态开点写法,只开了3倍空间
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=3e5+10,maxm=1e6+5;
int n,m,a[maxn];
ll st[maxn];
int lazy[maxn],ls[maxn],rs[maxn],no,root;
void pushUp(int rt)
{
st[rt]=st[ls[rt]]+st[rs[rt]];
}
void pushDown(int rt,int L,int R)
{
if(lazy[rt])
{
int mid=(L+R)>>1;
st[ls[rt]]+=(mid-L+1)*lazy[rt];
st[rs[rt]]+=(R-mid)*lazy[rt];
lazy[ls[rt]]+=lazy[rt];
lazy[rs[rt]]+=lazy[rt];
lazy[rt]=0;
}
}
void build(int &rt,int L,int R)
{
if(!rt) rt=++no;
if(L==R)
{
st[rt]=a[L];
return;
}
int mid=(L+R)>>1;
build(ls[rt],L,mid);
build(rs[rt],mid+1,R);
pushUp(rt);
}
void update(int &rt,int l,int r,int L,int R,int val)
{
if(!rt) rt=++no;
if(l<=L&&R<=r)
{
st[rt]+=(R-L+1)*val;
lazy[rt]+=val;
return;
}
pushDown(rt,L,R);
int mid=(L+R)>>1;
if(l<=mid) update(ls[rt],l,r,L,mid,val);
if(r>mid) update(rs[rt],l,r,mid+1,R,val);
pushUp(rt);
}
ll query(int rt,int l,int r,int L,int R)
{
if(!rt) return 0;
if(l<=L&&R<=r) return st[rt];
pushDown(rt,L,R);
ll ans=0;
int mid=(L+R)>>1;
if(l<=mid) ans+=query(ls[rt],l,r,L,mid);
if(r>mid) ans+=query(rs[rt],l,r,mid+1,R);
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
build(root,1,n);
int op,x,y,k;
while(m--)
{
scanf("%d",&op);
if(op==1)
{
scanf("%d%d%d",&x,&y,&k);
update(root,x,y,1,n,k);
}
else if(op==2)
{
scanf("%d%d",&x,&y);
printf("%lld\n",query(root,x,y,1,n));
}
}
return 0;
}
lazy
- 使用 l a z y lazy lazy 的情况:维护区间异或、维护区间和、染色
update
- 单点更新围绕一个点 p p p ,区间更新围绕 [ l , r ] [l,r] [l,r],并且可以加一个lazy
//单点更新
void update(int rt,int p,int L,int R,int val)
{
if(L==R)
{
st[rt]=val;
return;
}
int mid=(L+R)>>1;
if(p<=mid) update(ls,p,L,mid,p);
if(p>mid) update(rs,p,mid+1,R,p);
pushUp(rt);
}
//区间更新:维护翻转操作
void update(int rt,int l,int r,int L,int R,int val)
{
if(l<=L&&R<=r)
{
st[rt]=R-L+1-st[rt];
lazy[rt]^=val;
return;
}
pushDown(rt,L,R);
int mid=(L+R)>>1;
if(l<=mid) update(ls,l,r,L,mid,val);
if(r>mid) update(rs,l,r,mid+1,R,val);
pushUp(rt);
}
query
//单点查询:查询单点值
int query(int rt,int p,int L,int R)
{
if(L==R) return st[rt];
int mid=(L+R)>>1;
if(p<=mid) return query(ls,p,L,mid);
if(p>mid) return query(rs,p,mid+1,R);
}
//区间查询:查询区间和
int query(int rt,int l,int r,int L,int R)
{
if(l<=L&&R<=r) return st[rt];
pushDown(rt,L,R);
int mid=(L+R)>>1;
int ans=0;
if(l<=mid) ans+=query(ls,l,r,L,mid);
if(r>mid) ans+=query(rs,l,r,mid+1,R);
return ans;
}
离散化
- 线段树中维护的是段,一个叶节点表示一段的信息。需要注意是找准一段的左右坐标 [ l , r ]
- 从维护一个变成维护一块,然后把给定的区间变成坐标即可:将块的左右两端 [ l , r ] [l,r] [l,r] ,然后记录为 l − 1 l-1 l−1 和 r r r
- 明确当前维护块的左右坐标: l i l_i li 和 r i r_i ri 就没什么问题了
- 自己习惯的设法:假设当前块为 L ,那么它的区间为 x [ L − 1 ] , x [ L ] x[L-1],x[L] x[L−1],x[L]
总之:维护段,明确段的两个坐标
离散化操作
sort(allx.begin(),allx.end());
allx.resize(unique(allx.begin(),allx.end())-allx.begin());
对于主席树的理解
- 首先按时间序,将序列中的每个数按权值更新到主席树上特定位置。然后主席树维护区间和。
- 主席树是 n 颗权值线段树做前缀和的结果,自己在分析的时候,可以看成是独立的,具体在做题的时候,得看成是前缀和。
- 可以查询到小于等于(大于等于) k 的个数、第 k 大、大于(小于) k 的第一个数
- 主席树上查询:抓住两个关键点: 1、p 与 mid 的大小 2、左右子树是否存在值,存在值再往下搜
- 通常遇见区间和可以直接累加
题型
1、染色:
- 做法:可以使用 l a z y lazy lazy 标记(这里 st 就表示lazy)。把区间有多种颜色或者没有颜色标记为 -1,从左往右扫的时候,当遇到 st 不为 -1 ,说明这段颜色统一,可以直接统计后返回,而不用往下扫。
- 统计区间上出现过哪些颜色?标记上次出现的颜色,然后用 v i s i t visit visit 标记一下出现过的颜色就好了。
- 统计区间上每种颜色出现的段数?标记上次出现的颜色,当前和上次的颜色不同,直接让 m p [ x ] mp[x] mp[x] ++ 即可
int last,ans;
void query(int rt,int L,int R)
{
if(st[rt]!=-1&&last!=st[rt])
{
if(!visit.count(st[rt]))
ans++,visit[st[rt]]=1;
last=st[rt];
return;
}
if(L==R)
{
last=st[rt];
return;
}
pushDown(rt);
int mid=(L+R)>>1;
query(ls,L,mid);
query(rs,mid+1,R);
}
2、给定一个序列 n ,然后在给定区间内,查找第一个小于 i 的数
这里有三种方法:线段树可以做边更新边查询的操作有点在线的意思,而主席树就可以直接全部更新完之后查询。
- 滑动窗口:这里比较特殊, a i a_i ai 是序列上的一个数,可以用set 维护一个长度为 k 的区间,然后搜出左边 k 个中的小于 a i a_i ai 最大值,右边 k 个中的小于 a i a_i ai 最大值,最后两边取最大值即可
- 线段树:先将小于自己的数先更新到线段树上,然后就查询这个区间上的最大值,就找到了。边更新边寻找答案。对自己答案有贡献的做更新,有影响的先不更新。
- 主席树:在主席树上区间 [ l , r ] [l,r] [l,r] 表现为时间序,小于 i i i 表现为 i 在主席树上的位置 p 左边第一个有数的位置,即从位置 p -1 找到 1 。
主席树上查找 p p p 左边第一个数,即小于 p 的第一个数
int query(int pre,int now,int p,int L,int R)
{
if(st[now]-st[pre]==0) return 0;
if(L==R) return L<=p-1?L:0;
int mid=(L+R)>>1;
if(p-1<=mid||st[rs[now]]-st[rs[pre]]==0)
return query(ls[pre],ls[now],p,L,mid);
int res=query(rs[pre],rs[now],p,mid+1,R);
if(res!=0) return res;
return query(ls[pre],ls[now],p,L,mid);
}
3、在区间上找一个最小的
≥
k
\ge k
≥k的数,与上面类似
抓住两个关键点: p 与 mid 的大小,左右子树是否存在值,存在值再往下搜
int query(int pre,int now,int p,int L,int R)
{
if(st[now]-st[pre]==0) return inf;
if(L==R) return L>=p?L:inf;
int mid=(L+R)>>1;
if(p>=mid+1||st[ls[now]]-st[ls[pre]]==0)
return query(rs[pre],rs[now],p,mid+1,R);
int res=query(ls[pre],ls[now],p,L,mid);
if(res!=inf) return res;
return query(rs[pre],rs[now],p,mid+1,R);
}
int Query(int pre,int now,int p,int L,int R)
{
if(L==R)
return L;
int mid=(L+R)>>1;
int cnt1=ST[ls[now]]-ST[ls[pre]],cnt2=ST[rs[now]]-ST[rs[pre]];
int ans=INF;
if(p<=mid&&cnt1)
ans=Query(ls[pre],ls[now],p,L,mid);
if(ans!=INF)
return ans;
if(cnt2)
ans=Query(rs[pre],rs[now],p,mid+1,R);
return ans;
}
4、在区间上查找所有
≥
k
\ge k
≥k 的和,k 的权值为 p
所有
≥
p
\ge p
≥p 的都可以直接累加
//cnt是区间个数,sum是区间和
pll query(int pre,int now,int p,int L,int R)
{
if(L==R) return {st[now].cnt-st[pre].cnt,st[now].sum-st[pre].sum};
int mid=(L+R)>>1;
if(p<=mid)
{
pll ans=query(ls[pre],ls[now],p,L,mid);
ans.fi+=st[rs[now]].cnt-st[rs[pre]].cnt;
ans.se+=st[rs[now]].sum-st[rs[pre]].sum;
return ans;
}
else return query(rs[pre],rs[now],p,mid+1,R);
}
其实还有更简单的写法:直接用区间查询,查询区间 [ p , t o t ] [p,tot] [p,tot] 的和即可
pll query2(int pre,int now,int l,int r,int L,int R)
{
if(l<=L&&R<=r)
return {st[now].cnt-st[pre].cnt,st[now].sum-st[pre].sum};
int mid=(L+R)>>1;
pll ans1={0,0},ans2={0,0};
if(l<=mid)
ans1=query2(ls[pre],ls[now],l,r,L,mid);
if(r>mid)
ans2=query2(rs[pre],rs[now],l,r,mid+1,R);
return {ans1.fi+ans2.fi,ans1.se+ans2.se};
}