数据结构乱扯
才刚刚开始/youl
线段树
最基础的线段树
首先是板子,维护最简单的信息以及懒标记的使用和下推(当然还有下推顺序)
对于最一般的线段树题(常规的维护序列信息)只要会 pushup
和 pushdown
(后者仅仅在有懒标记的时候需要)就能使用线段树
pushup
即将节点 \(i\) 的左右儿子信息合并成同等规模的 \(i\) 的信息,例如区间最大值的pushup
即为 \(maxval(i)=\max(maxval(lson),maxval(rson))\)
上面的板子的 pushup
和 pushdown
是最简单的一类
做线段树题需要三个要素
- 线段树节点上维护什么信息
- 如何将两个区间的信息合并成一个区间的信息
- 如果有懒标记,如何下推懒标记
用一道比较简单的例题来体会上面的流程
例1-洛谷P6492-STEP
维护全局最长的形如 LRLRLR
的子段长(即,不包含 LL
或者 RR
的最长子段)
考虑如果我们知道了 \([L,mid]\) 和 \([mid+1,R]\) 的信息,如何推出区间 \([L,R]\) 的信息,也就是 \([L,R]\) 的答案(最长子段)是怎么产生的
显然,\([L,R]\) 的最长子段只有 \(3\) 种情况
- 完全来源于 \([L,mid]\)
- 完全来源于 \([mid+1,R]\)
- 跨过 \(mid\) 的一段,也就是由 \([L,mid]\) 的一段后缀和 \([mid+1,R]\) 的一段前缀拼成
所以,为了得到 \([L,R]\) 的答案
我们需要以下信息:
- \([L,mid]\) 的答案和 \([mid+1,R]\) 的答案(\(ans_{[L,mid]}\) 和 \(ans_{[mid+1,R]}\))
- \([L,mid]\) 的最长合法后缀和 \([mid+1,R]\) 的最长合法前缀(\(suf_{[L,mid]}\) 和 \(pre_{[mid+1,R]}\))
那么是不是 \(ans_{[L,R]}=\max(ans_{[L,mid]},ans_{[mid+1,R]},suf_{[L,mid]}+pre_{[mid+1,R]})\) 呢
不完全是,因为 \(suf_{[L,mid]}+pre_{[mid+1,R]}\) 可能是 RLRL
\(+\) LRLR
这样的,虽然本身都是合法的,但是不能拼在一起
所以还要额外记录 \([L,mid]\) 的最右边字符以及 \([mid+1,R]\) 的最左边字符,二者相异才能后缀拼前缀
三要素因为本题是单点修改变成了二要素
维护信息
线段树节点上维护该区间的最长合法子段长(\(ans_i\))、最长合法前缀(\(pre_i\))、最长合法后缀(\(suf_i\))、最左边字符(\(lt_i\))、最右边字符(\(rt_i\))、区间长度(\(len_i\))(区间长度是为了合并 \(pre_i\) 和 \(suf_i\))
合并信息
void pushup(int i)
{
ans[i]=max(ans[ls],ans[rs]);
lt[i]=lt[ls],rt[i]=rt[rs];
pre[i]=pre[ls];
if(rt[ls]!=lt[rs])
{
if(pre[ls]==len[ls]) pre[i]+=pre[rs];//前缀可能越过mid
if(suf[rs]==len[rs]) suf[i]+=suf[ls];//后缀可能越过mid
ans[i]=max(ans[i],suf[ls]+pre[rs]);
}
return ;
}
练习题1
解决前缀最大值类问题-“楼房重建”型线段树
参考了小粉兔的博客
楼房重建
单点修改,每次询问 \(\sum\limits_{i=1}^n[\max\limits_{j<i}\{a_j\}<a_i]\) (多少个 \(i\) 是前缀 \(i\) 的严格最大值)
这种线段树的 pushup
很不一样,复杂度不再是常见的 \(O(1)\) 而是 \(O(\log n)\)
考虑区间 \([L,R]\) 的答案 \(res_{[L,R]}\),显然不能再 \(res_{[L,R]}=res_{[L,mid]}+res_{[mid+1,R]}\),因为 \(res_{[mid+1,R]}\) 并没有考虑 \([L,mid]\) 的影响,所以合并的时候最多只能继承左半区间的答案,右半区间需要在考虑 \([L,mid]\) 的最大值对其的影响下重新计算
于是定义函数 calc(i,pre)
,作用为计算节点 \(i\) 代表的区间在值 \(pre\) 的影响下的答案
记 \(cnt_i\) 表示节点 \(i\) 的右子树的答案 (即区间 \([L,R]\) 在考虑 \([L,mid]\) 对右半区间的影响时右半区间的答案)
int calc(int L,int R,int i,int pre)//这里的L,R仅仅起到标识叶节点的作用
{
if(L==R) return mx[i]>pre;
int mid=L+R>>1;
if(mx[ls]<=pre) return 0+calc(mid+1,R,rs,pre);//pre比左半区间的最大值还要大,那么左半区间不可能有任何贡献
else return calc(L,mid,ls,pre)+cnt[i];//否则pre对右半区间的影响不如左半区间对右半区间的影响,右半区间答案不变,重新计算左半区间
}
0+calc(mid+1,R,rs,pre)
因为 \(pre\) 大于左半区间最大值,左半区间连最大值都无法做出贡献,所以左半区间答案为 \(0\)
calc(L,mid,ls,re)+cnt[i]
对于右半区间,左半区间的限制严格强于 \(pre\) 的限制,所以右半区间直接取 cnt[i]
,重新计算左半区间
所以 pushup
就自然的写出来
void pushup(int L,int R,int i)
{
mx[i]=max(mx[ls],mx[rs]);
int mid=L+R>>1;
cnt[i]=calc(mid+1,R,rs,mx[ls]);
return ;
}
回答询问就直接 return calc(1,N,1,0)
由于 pushup
复杂度是 \(O(\log n)\) 的,所以 "楼房重建" 型线段树时间复杂度为 \(O(Q\log^2 n)\)
练习题2
为了防止Vijos突然暴毙把题面留这里(
给定一棵 \(n\) 个节点的树,每个节点有 \(a_i,b_i\) 两个权值,你需要支持两种操作
- 询问,给出起点终点 \(s,t\),你将从 \(s\) 走到 \(t\),每到一个节点 \(x\) 你会把手上大于 \(a_x\) 的数字都扔掉,然后获得 \(b_x\) ,问走完 \(s-t\) 之后你手上有多少个数
- 修改节点 \(x\) 的 \(a_x\)
\(1\le n\le 4\times 10^4,1\le Q\le 6\times 10^4\)
李超线段树
支持插入直线/查询最大值的线段树
每个节点保存该区间的“优势直线”,以及一个标记,标记该节点有没有“优势直线”
超哥树就每个节点标记一下是不是有直线,插入的时候遇到空节点直接塞进来,否则考虑和已有直线的关系
-
完全碾压
因为直线都是单调的,所以如果发现在当前区间左右端点都满足待插入直线都比已有直线更优的话,已有直线就没什么用了 -
完全被碾压
同上,如果在当前区间左右端点都满足待插入直线比已有直线更劣的话待插入直线就没什么用了 -
相交于区间内某一点
排除掉1 2两种情况之后已有直线和待插入直线就一定会相交于区间内的一点,以中点为划分点来判断
-- 交点在左边
这时候右半区间一定是碾压的局面,有争议的只有左半边,所以判断一下右半边是谁碾压谁,如果是待插入直线碾压已有直线,那么该区间优势直线就是待插入直线,然后把已有直线插入左半区间
-- 交点在右边
同上,这时候左半区间一定是碾压局面,判断一下是否替换优势直线,然后继续插入到有争议的区间内即可
查询的时候取递归过程访问到的每一条直线在查询点处的纵坐标的最值作为答案
(写的时候想到区间查询这种东西,但是直线都是单调的所以区间查询就是在区间端点处单点查询取最值即可)
但是还有一种情况是区间插入,也就是插入的是线段不是直线,这种情况下区间查询就不能再像上面那样转成两个单点查询,即使直线是单调的,但是可能区间内部有比较强的线段,但是在端点处查不到的情况
此时最好使用标记永久化,插入线段的时候如果当前区间完全被线段所在区间包含,就计算插入线段在端点处的值进行更新
时间复杂度,全局插入 \(O(n\log n)\),区间插入 \(O(n\log^2n)\)