【学习笔记】线段树
1.扫描线
- 面积并(P5490【模板】扫描线)
不想贴图。
把这些矩形贴在一起,然后我们发现这些矩形的侧边把整个图割成了 \(2n\) 块(假装没有边合一起,有也没问题),每块都是矩形。
所以我们考虑线段树维护每一个侧边被覆盖的长度,移到下一个侧边就是这个覆盖长度乘上移动长度。
线段树维护这个线段的贡献,以及被完全覆盖的次数。碰见左侧边就让对应区间加一,碰见右侧边就让对应区间减一。
维护这个线段的贡献和这个线段被完全覆盖的次数。
在 pushup 的时候我们如果被完全覆盖过就直接不管儿子了。因为查询只有整体,而且增加和删除是对应的,根本不需要 pushdown。
- 周长并(P1856 矩形周长)
和面积不一样的在于两个线段贴到一起中间那个是被消掉的。
所以多维护几个东西代表左右端点是否被覆盖以及这个线段有多少个不相交的线段。
2.线段树合并
首先我们可以把链上操作转成树上差分。然后我们对每个节点开一棵权值线段树。朴素的树上差分都是开一个桶然后加和,但是这里开的是线段树。
所以就有了线段树合并。
在把 \(y\) 并到 \(x\) 的过程中,如果 \(x\) 本身没有一个 \(y\) 有的节点就可以直接把 \(y\) 的那个节点接上去节省时间。
就没了。
inline void merge(int x,int y){
if(!sg[x].ls&&!sg[x].rs&&!sg[y].ls&&!sg[y].rs){//并到叶子节点了直接两个合起来
sg[x].sm+=sg[y].sm;
return;
}
if(sg[y].ls){//如果要并过来的没有左儿子就不用往左边合并了
if(sg[x].ls)merge(sg[x].ls,sg[y].ls);
else sg[x].ls=sg[y].ls;
}
if(sg[y].rs){
if(sg[x].rs)merge(sg[x].rs,sg[y].rs);
else sg[x].rs=sg[y].rs;
}
sg[x].sm=sg[sg[x].ls].sm+sg[sg[x].rs].sm;//更新
return;
}
首先很容易看得出来单次合并的复杂度是 \(O(重合大小)\) 的。
那么对于 \(n\) 棵最开始只有一个元素的线段树,合并起来的复杂度是 \(O(n\log n)\)。这个很显然,因为就算所有元素都相等每次都跑满也只有 \(\log n\) 次。
注意线段树合并只能用于动态开点,因为普通线段树本来就是满的。
关于另一种带返回值的线段树合并,本质上就是返回更新后的节点编号,需要代码直接看 这份提交。
空间如果卡得紧可以试着用 回收节点。
至于 带转移的线段树 实际上也是一样的,把转移的东西放在合并里面,在某个节点为空的时候就代表转移转完了,打上一个 tag 之类的。注意合并的时候要 pushdown。
发现 \(x\) 和 \(y\) 合并之后线段树的结构被破坏,比如如果我把 \(y\) 接到 \(x\) 的左儿子,再一次更新 \(x\) 的时候它更新左儿子那么 \(y\) 就会接受一个并不属于自己的更新。要求更新完之后 \(x\) 包含了 \(y\) 的信息,但是 \(y\) 仍然能够独立使用。当然我们可以把询问离线下来,这样 \(y\) 被破坏结构也跟我们没有关系(上面的板子题的做法)。如果强制在线我们可以考虑合并两个节点的时候新开一个节点储存这两个节点的信息。
Il int merge(int x,int y,int l,int r){
if(!x||!y)return x|y;
int p=++si,mid=(l+r)>>1;
if(l==r){sm[p]=sm[x]+sm[y];return p;}
ls[p]=merge(ls[x],ls[y],l,mid);
rs[p]=merge(rs[x],rs[y],mid+1,r);
pup(p);
return p;
}
3.线段树分裂
在分裂函数里面传当前节点,就是一边分裂一边建树。复杂度和区间查询一样。
inline void split(int ql,int qr,int &x,int &y,int l,int r){
if(!x||qr<l||r<ql)return;
if(ql<=l&&r<=qr){//就是要割的
y=x;
x=0;
return;
}
if(!y)y=build();
int mid=(l+r)>>1;
if(ql<=mid)split(ql,qr,sg[x].ls,sg[y].ls,l,mid);
if(qr>mid)split(ql,qr,sg[x].rs,sg[y].rs,mid+1,r);
sg[x].sm=sg[sg[x].ls].sm+sg[sg[x].rs].sm;
sg[y].sm=sg[sg[y].ls].sm+sg[sg[y].rs].sm;
return;
}
4.线段树优化建图
注意这个东西在最基本的情况下也有 \(8n\) 个点,边数可能达到 \(8n+q\log n\),注意复杂度。
我们考虑开线段树维护这个东西。令当前节点为 \(p\),它的左右儿子为 \(ls,rs\),它的父节点为 \(f\)。
首先对于一个维护 \([l,r]\) 的节点,我们发现我们什么都不用维护。那么肯定它能够通往它的子节点。所以给 \(p\rightarrow ls,rs\)。
然后因为子节点能够通往父节点,所以还要 \(p\leftarrow ls,rs\)。
因为是包含关系并不是实际边所以边权设为 \(0\)。
很明显这样会出问题。
这样子我们任何一个叶子节点到根节点的权值都是 \(0\),而且路径是双向的,所以任何一个节点到另一个节点的权值都是 \(0\)?
问题出在路径是双向的地方。我们尝试开两个线段树解决这个问题。
第一棵线段树 \(a\) 维护点到区间,在这棵线段树上我们的子节点连向父节点;第二棵线段树 \(b\) 维护区间到点,父节点连向子节点。
因为它们的叶子节点是同样的点,所以可以直接当同一个。我们尝试使用动态开点线段树,这样叶子节点的编号就是对应在数组的编号,比较好操作。
那么对于普通连边我们直接连就好。对于连区间的,我们搞到对应区间的编号然后和对应点连一连就好。
把每个点当成普通点跑就好了。
但是注意点向区间 连边 是 \(a\) 线段树的 父节点连向子节点。原因不清楚,但是如果你连反了在原本的线段树上路径就是 \(0\) 你再加这一条边就没有用了吗不是。
一开始连线段树的边数就有大致 \(8n\) 条边,然后每次连边最多可以达到 \(\log n\) 边,所以总边数达到了惊人的 \(8n+q\log n\)!
当然我们还能再加强。
- 区间向区间连边。
直接连边复杂度是 \(log^2\) 的。
但是我们可以开一个虚点然后这两个对应区间都向虚点连边。
点数最多加 \(q\) 个。边数最多加 \(q\log n\) 条。大概空间开十倍就好了。
5.线段树分治
注意它和线段树其实关系并不大,本质其实是分治思想,长得非常像线段树而且给了 \(\log\) 的贡献而已。
一个图是二分图代表每条边两边端点染色不同。
显然除了 dfs 我们还有一种扩展域并查集的做法,对于每个点开正反两个点代表不同的颜色,合并边的时候直接把正 \(x\) 和反 \(y\) 合并,正 \(y\) 和反 \(x\) 合并就好。
为什么用并查集?因为它支持插入,在使用可撤销并查集(带秩合并)可以做到 \(log\) 删除。
我们对于一个时间轴开线段树,每个点维护一个 vector 表示覆盖这个点的边集,那么每条边能够做到 \(\log k\) 插入。
我们考虑统一处理答案。
对于区间 \([l,r]\) 的询问,我们如果在加入当前这条边时发现已经破坏二分图结构就可以直接跳出。
注意我们开栈维护加入的边然后在分治退出的时候把这些边一一删掉就可以更好的维护(?)。
其实线段树分治是在分治的时候维护一个其它的数据结构并且能够通过 \(\log\) 来处理所有的询问,为什么叫线段树分治(?)。
6.李超树
用于维护直线,在斜率之类的题目里面算是超级有用的存在。
我们考虑维护斜率,对于线段树节点 \(p:[l,r]\),其维护当 \(x=mid\) 时使 \(y\) 最大的一次函数。
更新时因为不好覆盖,tag 需要一直传下去。假设更新 tag 时更新的值比原节点不优(如果更优可以把两个一次函数交换,因为原节点也没有下传)。然后我们根据其能否在左右端点造成更优的贡献决定是否继续下传。
因为数学不好所以给个原因。对于左端点而言,因为中点已经不优,如果左端点也不优,代表这个一次函数在 \([l,mid]\) 范围内处于原最优一次函数的下面,不会被当成答案。右端点同理。
这个东西的复杂度是 \(O(\log n)\) 的,因为在你中点被原最优盖住的情况下,最多只有一边能更优,也就是说你的 pushdown
是单点查询的复杂度的。
如果插入线段是 \(O(\log^2 n)\),插入直线不需要拆区间就是 \(O(\log n)\)。
运用了标记永久化的思想,查询时记得边走边查。
7.套矩阵
只是一种 trick。由于矩阵需要满足结合律,线段树刚好也可以满足结合律,所以我们可以把线段树和矩阵套起来维护。
转移时注意矩阵没有交换律是左儿子乘右儿子还是右儿子乘左儿子。
递归里面传矩阵作为变量常数过大,可以把矩阵在线段树外面更新然后更新的时候只需要传位置。