线段树

常用技巧

  • 动态开点
  • 标记永久化
  • 值域线段树
  • 树套树
  • 线段树优化建图
  • 可持久化
  • \(\text{zkw}\)线段树

应该思考的

1、每个区间需要记录哪些值?
2、需要哪些标记?
3、如何叠加标记(在原有标记的区间增加新的标记?)
4、如何对区间进行整体修改?
5、如何合并区间?

Day1

例题

窗口的星星

给定一个平面上的 \(n\) 个点,求一个 \(w × h\) 的矩形可以覆盖到的点数最多多少?

\(n\le 10^4,x,y<2^{31},w,h\le 10^6\)

考虑对于一颗星星来说,矩形的一个角在哪个范围内有贡献,转化成矩阵加全局最大值,扫描线即可

方差

给定一个序列,要求支持:区间加,区间平均数,区间方差。其中方差定义为:
\(σ^2=\frac{1}{n}∑_{i=1}^n(x_i − \bar{x})^2\)

$ n, m ≤ 10^5$

同时维护区间和,区间平方和

色版游戏

给定一个区间,要求维护下列操作:把一个区间染上一个颜色;询问一个区间
的颜色个数,\(n, m ≤ 10^5\),颜色数不多于 \(30\)

将颜色状压进一个\(int\),线段树维护

WallBreaker5th 的毒瘤题

要求维护一个长度为 \(n\) 的序列,支持区间加,区间查询积木大赛答案。

积木大赛:给定一个长度为 \(n\) 的序列,你现在需要清除它,每次可以选择一个区间全部减少 \(1\),但不能减少已经是 \(0\) 的。求最小次数

众所周知积木大赛有一个神必递归解法:我们先递归求左半部分的答案和右半部分的答案,然后考虑怎样减小答案:就是中间相接部分可以同时进行,形式化地说:
\(ans_{i,j} = ans_{i,mid} + ans_{mid+1,j} − \min(h_{mid}, h_{mid+1})\)

[NOI2016] 区间

给定区间上 \(n\) 条线段,和常数 \(m\)。现在求至少 \(m\) 条线段使得他们有公共点,
且最长线段减去最短线段最小,求这个最小值。不存在则输出 \(-1\)

双指针+线段树即可

yyy loves OI IV

给定 \(n\) 个数要求连续分组,每个数有颜色,共两种。要求每一块要么颜色相同,要么两种颜色差的绝对值不超过 \(m\),求最少块数。

\(n ≤ 5 × 10^5, m ≤ 2000\)

记一种颜色为\(1\),一种为\(-1\)\(s\)为前缀和有两种情况

  • \(dp_i=dp_{last[!a[i]]}+1\)
  • \(dp_i=\min_{|s[i]-s[j]|\le m}dp_j+1\)

第二种拆开绝对值,线段树维护即可

CF1295E

给定一个长度为 \(n\) 的排列,每个数 \(p_i\) 有一个权值 \(a_i\)。要求先将排列划分成两个非空序列,然后将一个序列中的部分元素花费对应权值放到另一边边,使得最后左边所有数小于右边所有数,两个序列任意一个为空都看作满足要求,求最小花费。

\(n ≤ 2 × 10^5\)

考虑\(DP\),令\(f_{i,j}\)为按照第\(i\)个元素划分,左边最大是\(j\)的花费

我们考虑用\(f_{i,j-1}\)计算出\(f_{i,j}\),如果\(j\)本来就在左边,减去\(j\)的花费,否则增加\(j\)的花费,这样可以做到\(O(n^2)\)

我们发现对于每一个\(j\),\(f_i\)的变化有区间性,所以我们从小到大枚举\(j\),把所有\(f_i\)用线段树维护,问题转化为区间修改,全局最小值

Day2

基础练习题

楼房重建

  • 单点修改
  • 询问区间不退栈单调栈大小

\(n,m\le 10^5\)

维护对应区间单调栈的 \(sz\)。现在考虑如何合并两个区间:
首先,左儿子的元素一定是单调栈的一部分。现在只需要考虑右儿子:
• 如果右儿子的左儿子的最大元素大于左儿子的最大元素,则向右儿子的左
儿子递归。
• 否则,向右儿子的右儿子递归。

时间复杂度\(\mathcal{O(n\log^2n)}\)

CF gym101955 E

长为 \(n\) 的点列,每个点有颜色,\(m\) 次操作。要求支持:
• 单点修改
• 询问区间不同颜色点的最大曼哈顿距离

拆成四份,然后维护区间最大次大,最小次小,要求最和次的颜色不同。最后讨论一下即可。

简单题1

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 区间加正整数
• 区间大于等于 \(k\) 的数的和
\(k\) 初始给定。\(n, m ≤ 10^5\)

考虑开两颗线段树,对\(\ge k\) 的维护一颗区间加

\(\le k\)的记录\(\max\),一旦\(\ge k\)就将其提到另一颗线段树中

简单题2

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 询问区间 \(mex\)
\(n, m ≤ 10^5\)

1.用主席树维护每个权值上一次出现的位置,然后线段树上二分即可。

2.扫描线,\(f_i\)表示区间\([l,i]\)\(mex\),每次对\([l,ne[a[l]]-1]\)\(\min\),也可线段树上二分后转化成区间赋值

简单题3

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 单点修改
• 询问区间不同元素个数
强制在线。\(n, m ≤ 10^5\)

\(i\)点坐标记为\((i,lst_i)\),于是转化成单点修改,矩阵查询,树套树维护

简单题4

一个长为\(n\)的序列\(A\)

右端点\(R\)向右移动,动态维护\(\forall L\in[1,R),\max[L,R]\)

单调栈+线段树维护

CF407E

长为 \(n\) 的数列。求一个最长的区间,使得最多加入 \(k\) 个数之后,排序得到的是
一个公差为 \(d\) 的等差数列。\(n ≤ 2 × 10^5\)

首先这个区间必须\(\bmod d\)同余

归一,转化为加入\(k\)个数,使\(sort\)后公差为\(1\),\([L,R]\)合法当且仅当

  • 没有重复元素
  • \(\max[L,R]-\min[L,R]-(R-L)\le k\)

枚举\(R\),即找到在\([T,R]\)中最小的\(L\),满足

\(w_L=\max[L,R]-\min[L,R]+L\le k+R\)

\(w\)可以单调栈维护,然后在线段树上二分即可

P2824 排序

给一个\(1\)\(n\)的排列,\(m\)次操作,要求支持:
• 升序排序一个区间
• 降序排序一个区间

最后输出这个排列

考虑\(ODT\)区间推平,对每个颜色段维护一颗线段树,记录每个颜色段是升序还是降序,然后分裂时线段树分裂,合并时线段树合并

ARC085F

长为 \(n\) 的数列,\(m\) 个区间。选出任意多个,最后求所有区间并的数的和的最大值。

\(n, m ≤ 2 × 10^5\)

将区间按\(r\)排序,考虑\(DP\)

  • \(r_j<l_i,f_i=f_j+S_{r_i}-S_{l_i-1}\)
  • \(l_j<l_i\le r_j<r_i,f_i=f_j+S_{r_i}-S_{r_j}\)
  • \(l_i\le l_j\le r_j\le r_i\),直接忽略即可

我们把\((l_i,r_i)\)看做一个点,两种转移都是对矩阵查询,二维线段树维护即可

BZOJ3252 加强版

一棵大小为$ n$ 的有根树,顶点带权,可以选择 \(k\) 条到根的路径,求最后所有经
过的顶点的权值和最大值。
\(n, k ≤ 10^6\)

对每个节点维护到根的贡献,然后查最大值位置,往上调消除其祖宗对其子树的贡献,如果已经消除过就结束。

用线段树维护\(DFS\)序即可

李超线段树

Segment

李超树用了标记永久化的思想,插入直线是单\(\log\)的,插入线段因为要找到对应区间,所以两个\(\log\),且常数很小

Segment Tree Beats

板子1

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 区间对 \(k\)\(\min\)
• 区间求和
• 区间求 \(\min\)
\(n, m ≤ 10^5\)

考虑对线段树每个节点维护区间最大值 \(mx\),严格次大值 \(se\),区间最大值个数
\(cnt\),然后考虑每次操作:
• 如果 \(mx ≤ k\),那么没有影响
• 如果 \(se < k < mx\),那么最大值个数不变,在当前点对最大值打 tag,对区间和做修改即可
• 如果 \(se ≥ k\),递归到左右儿子。
复杂度可以直接考虑每次递归操作一定会让最大值和次大值相等,于是区间的不同数个数至少减 \(1\)。于是总复杂度为 \(\mathcal{O((n+m)\log n)}\)

板子2

长为 n 的数列,m 次操作。要求支持:

• 区间加
• 区间对 \(k\)\(\min\)
• 区间求和
• 区间求 \(\min\)
\(n, m ≤ 10^5\)
可以将每个区间内的点分为最大值和非最大值,我们上一个板子里的操作本质是将区间取 \(\min\),转化对若干个区间中对最大值的加减,本题的区间加减就可以看成同时对最大值与非最大值的加减,因此可以对两种加减分别维护 \(tag\)。下传时讨论一下子区间最大值是否是原区间最大值。

此时没法用上一题的证明了,将势能改为子树内与父亲最大值不同的点个数,那么每次递归势能减 \(1\),修改某一个节点会导致势能 \(+\log(n)\),因此复杂度 $ \log^2$ ,更具体的证明可以参见 2016 年 IOI 集训队论文《区间最值操作与历史最值问题》。

板子3

长为 \(n\) 的数列,\(m\) 次操作。要求支持:

  • 区间加
  • 查询区间最大值
  • 查询区间历史最大值

以下内容来自cmd的博客

我们用标记来解决这类历史值问题。

  • 在线段树标记的下推机制中,某个点存有标记,表示整一棵子树在标记存在的时间内都未曾更新。

于是,问题的核心就在于分析单个节点上停留的标记的影响。

在非历史值问题中,我们只关注该节点“当下”的标记是什么,所以我们会直接将标记合并起来。

但在历史值问题中,我们不仅要考虑该节点现在的标记合并结果,还要考虑历史上推来的(按时间为序的)每个标记的依次作用。

为了便于理解,我们不合并标记,假设每个节点上有一个队列(按照时间先进先出),放着所有曾经推过来的标记。

下推标记时,将该节点上所有的标记推到两个儿子处,并清空队列。

一个标记队列对区间最大值的影响是容易计算的

对区间历史最大值的影响是队列最大前缀和+此时区间最大值,即标记历史最大值+此时区间最大值

而标记历史最大值就是\(\max\)(标记历史最大值,此时标记+下传的标记历史最大值)

\[u下传至v\\ hadd_v=\max(hadd_v,add_v+hadd_u)\\ hv_v=\max(hv_v,v_v+hadd_u)\\ add_v+=add_u,v_v+=add_u \]

然后这题就很容易了

线段树3

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 区间加
• 区间对\(k\)\(\min\)
• 区间和
• 区间求最大值
• 区间求历史最大值

\(n, m ≤ 10^5\)

除了操作二,我们可以记录历史最大值,历史标记最大值维护

加上操作二以后,分为最大值和非最大值维护,维护四个标记

板子4

给定一个长度为 \(n\) 的序列 \(a_i\),有 \(m\) 个操作,操作有两种:

  1. 给定区间 \([l,r]\) 和整数 \(v\),将区间 \([l,r]\) 都加上 \(v\)
  2. 给定区间 \([l,r]\),求区间 \([l,r]\) 的历史和

这个就看\(\text{cmd}\)的博客罢,我讲不清楚

这里只贴个代码

namespace T{
	#define ll long long
	struct tree{
		int l,r,len;
		ll s,hs,upd,t,ht;
	}t[N<<2];
	inline void up(int k){t[k].s=t[k1].s+t[k2].s,t[k].hs=t[k1].hs+t[k2].hs;}
	inline void add(int k,ll upd,ll tag,ll ht){
		t[k].hs+=upd*t[k].s+ht*t[k].len;
		t[k].ht+=t[k].t*upd+ht,t[k].upd+=upd;
		t[k].s+=tag*t[k].len,t[k].t+=tag;
	}
	inline void push(int k){
		if(t[k].upd||t[k].t||t[k].ht)
			add(k1,t[k].upd,t[k].t,t[k].ht),
			add(k2,t[k].upd,t[k].t,t[k].ht),
			t[k].t=t[k].upd=t[k].ht=0;
	}
	void build(int k,int l,int r){
		t[k].l=l,t[k].r=r,t[k].len=r-l+1;
		if(l==r) return t[k].s=t[k].hs=a[l],void();
		int mid=(l+r)>>1;
		build(k1,l,mid),build(k2,mid+1,r);
		up(k);
	}
	void change(int k,int l,int r,int v){
		if(l<=t[k].l&&r>=t[k].r) return add(k,0,v,0);
		push(k);
		if(l<=t[k1].r) change(k1,l,r,v);
		if(r>=t[k2].l) change(k2,l,r,v);
		up(k);
	}
	ll ask(int k,int l,int r){
		if(l<=t[k].l&&r>=t[k].r) return t[k].hs;
		push(k);
		if(r<=t[k1].r) return ask(k1,l,r);
		if(l>=t[k2].l) return ask(k2,l,r);
		return ask(k1,l,r)+ask(k2,l,r);
	}
	inline void add(){add(1,1,0,0);}
}

CF815D

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 区间对 \(x\)\(\max\)
• 区间求 \(\max(a_i − x, 0)\) 的和
\(n, m ≤ 5 × 10^5\)。保证询问操作的 \(x\) 递减。

发现数列单调不减,而询问单调不增,就可以用类似于简单题1的做法

简单题5

长为 \(n\) 的数列,值域 \([1, k]\)\(m\) 次操作。要求支持:
• 单点删除
• 询问有多少个区间满足出现了 \([1, k]\) 中的所有数。
\(n, m ≤ 5 × 10^5\)

删除其实相当于把一个数赋成$ 0$。对每个 \(i\) 维护最大的 \(L(i)\) 使得$ [L(i), i]$ 满足条
件。现在来考虑删除操作。
考虑它右边下一个 \(a_i\)\(r\),左边上一个 \(a_i\)\(l\),于是操作相当于把 \([i, r − 1]\)
所有 \(L_i\)\(l\)\(\min\)。维护一下区间和就行了。
事实上由于 \(L\) 单调,所以可以线段树上二分找出赋值区间,再区间赋值

简单题6

长为 \(n\)的点列,\(m\) 次操作。要求支持:
• 向区间中的每两个点都连一条边
• 询问有多少个点对直接有边相连
\(n, m ≤ 3 × 10^5\)

把双向关系改为从小的向大的连边。对于每个$ i$ 维护最大的 \(f_i\) 表示$ [i + 1, f_i]$ 中
的所有点都与它相连。于是每次相当于取 \(\max\)

SP1557 GSS2

长为 \(n\) 的数列,\(m\) 次操作。要求支持:
• 询问区间中相同数只算一次的最大子段和
\(n, m,| a_i| ≤ 10^5\)

离线,对\(r\)扫描线,每次对\([lst_{a[r]}+1,i]\)加上\(a[r]\),区间\([l,r]\)的答案就是\(l\)的历史最大值,维护即可

线段树合并

贺一下课件

把两棵线段树按对应位置叠加起来。动态开点,从$ 1$ 号点开始递归,递归到某个节点时若某个树该节点为空(左右儿子没有开,只有当前点上的 \(tag\)),就把 \(tag\)加到另一棵树对应节点上,返回该节点。
分析一下线段树合并的复杂度:合并时每递归一个点,意味着合并后的线段树点数减少 \(1\),总共增加了 \(m \log n\) 个点那么复杂度就是 \(O(m \log n)\)

「Vani 有约会」雨天的尾巴

\(n\) 个点的树,每个点上有一个空的可重集合。$m $次操作,每次往一条链上的集合里加一个元素。求最后每个点的集合中最多的元素。

\(n, m ≤ 10^5\)

树上差分之后直接合并值域线段树即可。

BJOI2014大融合

\(n\) 个点,要求支持两个操作:
• 加入一条边,保证加入后还是森林
• 给定一条边 \((u, v)\),问有多少条路径 \((x, y)\) 经过了 \((u, v)\)

我是蒟蒻,只想到了线段树分治或者\(LCT\)

我是废物,没看懂题解,把题解贴上来:

离线所有操作,遍历最后的树然后得到 \(DFS\) 序,于是问题转化为了在一段 \(DFS\)序区间中有多少已经出现的点,线段树合并维护即可。

BZOJ4771

大小为 \(n\) 的树,点有颜色,要求支持:
• 询问点 \(u\) 子树中距 \(u\) 不超过 \(d\) 的所有点的不同颜色个数。
\(n ≤ 10^6\)

\(f_{x,d}\)表示\(x\)子树内最小深度为\(d\)的颜色种数,考虑用线段树合并求出

考虑合并两个\(f\):如果一个颜色出现了两次,我们需要在深度深的删掉它,维护两颗线段树,一颗\(f\),一颗是值域线段树,维护最小深度

「PKUWC2018」Minimax

大小为 \(n\) 的二叉树,叶子有权值,非叶节点有 \(p_u\) 的概率取两个儿子的 \(\max\),有
\(1 − p_u\) 的概率取两个儿子的 \(\min\)。对于每个可能的 \(w\),输出根节点是 \(w\) 的概率。
\(n ≤ 3 × 10^5\)
令$ P_{u,v}$ 为 \(u\) 的权值为 \(v\) 的概率,\(L\) 为左儿子,\(R\) 为右儿子。于是有:

\[P_{u,v} = P_{L,v} ·(p_u\sum_{k<v}P_{R,k}+(1-p_u)\sum_{k>v}P_{R,k})+P_{R,v} ·(p_u\sum_{k<v}P_{L,k}+(1-p_u)\sum_{k>v}P_{L,k}) \]

线段树合并天生地可以支持这个操作,具体来说,如果合并到某个节点然后停止了,说明另一棵线段树在这个节点所对应的区间内的所有值都是 0,于是直接打上乘法标记即可

线段树分治

继续贺

适用于修改操作有时间界限,询问单个时间的问题。
把所有修改操作离线下来,放到一棵线段树上。用标记永久化的思想,一个修改被放到一个区间当且仅当没有一个祖先完全被它包含并且它可以包含当前区间。
然后把所有的询问操作放到上面跑

二分图

板子,用可撤销并查集

猫树

\(\text{lxl}\)称其为二区间合并

贺一段\(\text{lxlPPT}\)里的话

多区间合并
• 给定一个序列,多次查询区间幺半群信息
• 幺半群可以粗略认为是可以\(O(1)\)合并,不能差分的信息
• 预处理复杂度:\(O( n * f(n,k) )\),查询的时候合并\(k\)次,\(k+1\)个区间
\(k=0:f(n,0)=n\)
\(k=1:f(n,1)=\log n\),所谓的“猫树”
\(k=2:f(n,2)=\log\log n\),所谓的“sqrt-tree”
\(k=3,4:f(n,3)=f(n,4)=\log^*n\)
• …
\(k=\alpha (n):f(n,\alpha (n))=\alpha (n)\)

回到猫树的问题来,猫树能够解决在区间查询问题中合并两个区间复杂度非常高的情况

猫树不支持修改,但更线段树相比,可以将每次查询的\(\log\)次合并降至\(1\)

如区间线性基,用线段树的复杂度为\(O(n\log n\log^2 V)\),但用猫树则是\(O(n\log n\log V)\)

具体来说,现将序列补成\(2\)的次幂,这样可以\(O(1)\)求出\(LCA\)

在建树的时候,对区间\([l,r]\),预处理出\([l,mid]\)的后缀和与\([mid+1,r]\)的前缀和

在支持离线的情况下,就变成了经典的分治算法:对于区间 \([l, r]\) 包含的询问,每次同时处理所有跨过 \(mid\) 的询问:先算出 \([l, mid]\) 的后缀和与$ [mid + 1, r]$ 的前缀和,然后每次询问只需要进行一次合并。
这个分治算法还是挺常用的

P6109 [Ynoi2009] rprmq1

维护 \(n×n\) 的二维数组 \(A\)
先执行 \(m\) 次矩阵加法,再询问 \(q\) 次矩形 \(\max\)

\(n, m ≤ 5 × 10^4, q ≤ 5 × 10^5\)

  • 对操作线段树分治,对询问猫树分治

\(x\)维扫描线,将矩阵加转化为区间加,矩阵求\(\max\)转化成一段时间的区间\(\max\)

\(x\)维分治,然后每个询问可以分成两段时间\([L,mid],[mid+1,R]\),这是可以\(O(1)\)合并的

对于分治区间\([l,r]\),考虑修改的贡献

  • 矩阵\(x\)坐标包含区间,可以直接加,最后分治退出时撤销
  • \(x\)坐标与区间相交,这样的区间只有\(O(\log n)\)个,猫树时暴力修改,最后还原。

考虑询问:左区间:暴力做\([l,mid]\)的操作,重置历史最大值,再撤销,右区间同理

时间复杂度\(O(n\log^2 n+q\log n)\)

完结撒花个鬼

不会的题放在这儿(咕了的就不管了):

  • 炸脖龙
  • 魔幻棋盘
  • 游戏
  • 大融合
  • BZOJ4311
  • [CTSC2016]时空旅行
  • [JOISC 2020 Day1] 扫除

总结

  • 考虑用线段树代替\(DP\)的一维
  • 善于发现操作的区间性
  • 维护最值考虑单调栈
  • 学会转化条件
  • 对于方便添加,撤销,不方便删除的考虑线段树分治
  • 对于合并复杂度高的考虑猫树分治
  • 标记下传时考虑父亲的标记队列对子节点的影响
posted @ 2023-06-27 20:56  Nityacke  阅读(91)  评论(0)    收藏  举报