线段树进阶笔记
update:
- 2024.10.23 整理了 Segment Tree Beat 与 历史最值操作
线段树是一个比较基础的数据结构,但其变形与拓展的难度依旧极高,仍有许多 人类の智慧 藏匿其中。
0. 线段树的双半群模型
一些群论的东西,但当维护信息 极多 时较为有用,具体可见 \(5.\) 历史最值问题。
(写的很粗糙,详细请找群论内容)
形象的,线段树上每个节点都有 数据 与 标记 两种信息,称作 \(D\) 与 \(T\)。
- 则需要存在 \(D \ast D = D'\) 的转移,即 数据合并。
- 以及 \(D \ast T = D'\),即 标记转移。
- 以及 \(T \ast T = T'\),即 标记合并。
同时还需要满足 结合律 与 分配律,这是一个 半群,再存在一个 单位元 \(\epsilon\) ,使 \(T \ast \epsilon = \epsilon \ast T = T\) 则为 幺半群。
则我们可以维护两个结构体,将三种转移加入即可。
这里举个例子。
区间加,区间乘,区间查询。则有 \(D = \{l,s\}\),\(T = \{a,b\}\) (乘法与加法标记)。
则有 \((l_1,s_1) \ast (l_2,s_2) = (l_1 + l_2,s_1 + s_2)\)。
以及 \((l,s) \ast (a,b) = (l,as + lb)\)。
以及 \((a_1,b_1) \ast (a_2,b_2) = (a_1 a_2,b_1a_2 + b_2)\)。
注意 顺序。
当然,这些数据也可以用 矩阵 维护,如果你感兴趣(常数稍大)
区间历史操作,从矩阵乘法到标记 - EnofTaiPeople
参考文章
线段树维护分治信息略解 - 铃悬
数据结构闲谈:范围分治的「双半群」模型 - Meatherm
1. 可持久化线段树
1.1 算法介绍
首先可持久化线段树是 动态开点 的,本质上是通过记录 原来的信息 实现 时间域 上的查询,每次插入需要以上一个版本的节点为基础,新建节点进行更新,复杂度是 \(\mathcal{O}(n\log{V})\),一般是解决 区间 kth 的。
1.2 扩展应用
1.3 例题
我们考虑二分 \(mid\) ,我们令 \(< mid\) 的值定为 \(-1\),\(\geq mid\) 定为 \(1\),则我们即求区间 \([a,b]\) 的后缀最大值, \([b+1,c-1]\) 的区间和即 \([c,d]\) 的 前缀最大值,这个东西我们可以按照 权值大小从小到大 构造可持久化线段树,这样我们只需找到 \(mid\) 这颗树,求答案即可,若 \(ans \geq 0\) 则答案一定 大于等于 \(mid\)。
2. 李超线段树
李超树比较好些,凸包的细节太多。
貌似ktt可以代替,但我没学。
2.1 算法介绍
李超树是解决 直线插入,查询某一 横坐标 使纵坐标最大的直线。
对于值域在 \([l,r]\) 中的直线,设 \(mid = \frac {l + r} 2\),两条直线 \(p,q\),若 \(f(p,mid) \geq f(q,mid)\),则直线 \(p\) 比直线 \(q\) 大的值域一定至少包含改区间一半值域,所以我们在每个节点存该节点 \(x = mid\) 时纵坐标最大的直线编号,新加入一个直线,我们只需要判断是否 \(f(p,mid) \geq f(q,mid)\),若是则交换,然后我们可以找出另一半不一定大的值域,递归进去即可,复杂度 \(\log{V}\)。
一般李超树都用的是标记永久化,所以我们查询的时候需要找到包含 \(x\) 点所有值域的最优答案。
对于插入一个 线段,我们可以先把线段的值域拆分,然后再加入线段即可,复杂度 \(\mathcal{O}(n\log^2{V})\)。
struct line{
db k,b = -inf;
int id;
line(db k,db b,int id):k(k),b(b),id(id){}
line(){}
db val(int x){return k * x + b;}
};
bool cmp(line a,line b,int x){return (fabs(a.val(x) - b.val(x)) < eps) ? a.id < b.id : a.val(x) > b.val(x);}
struct LiChao_tree{
line mx[N<<5];
line ask(line a,line b,int x){return cmp(a,b,x) ? a : b;}
void modify(int p,int l,int r,line x){
if(l > r)return;
int mid = l + r >> 1;
if(cmp(x,mx[p],mid))swap(mx[p],x);
if(cmp(x,mx[p],l))modify(p<<1,l,mid,x);
if(cmp(x,mx[p],r))modify(p<<1|1,mid+1,r,x);
}
void modify(int p,int l,int r,int L,int R,line x){
if(L <= l && r <= R)return modify(p,l,r,x),void();
int mid = l + r >> 1;
if(L <= mid)modify(p<<1,l,mid,L,R,x);
if(R > mid)modify(p<<1|1,mid+1,r,L,R,x);
}
line query(int p,int l,int r,int x){
if(l == r)return mx[p];
int mid = l + r >> 1;
if(x <= mid)return ask(query(p<<1,l,mid,x),mx[p],x);
else return ask(query(p<<1|1,mid+1,r,x),mx[p],x);
}
}t;
int main(){
scanf("%d",&n);
for(int i = 1;i <= n;i++){
int op,a,b,c,d;
scanf("%d%d",&op,&a);a = (a+las-1)%m1+1;
if(op == 0)printf("%d\n",las = t.query(1,1,m1,a).id);
else{
scanf("%d%d%d",&b,&c,&d);
b = (b+las-1)%m2+1,c = (c+las-1)%m1+1,d = (d+las-1)%m2+1;
if(a > c)swap(a,c),swap(b,d);
double k = 0,B = 0;
if(a == c)B = max(b,d);
else k = (double)(d - b) / (c - a),B = (double)b - a * k;
t.modify(1,1,m1,a,c,{k,B,++m});
}
}
return 0;
}
给出一个包装比较好的实现,不是很难写。
2.2 扩展应用
2.2.1 优化 DP(代替斜率优化)
斜率优化本质就是维护一些直线,通过建立凸包来寻找最大值。
而 李超树 可以直接维护直线,复杂度 \(\mathcal{O}(n\log{V})\),有时会劣于斜率优化。
但是有些性质会导致不可使用 李炒熟,注意甄别。
见例 I。
2.2.2 动态开点
当横坐标值域过大时,可以用 动态开点,空间复杂度甚至是 \(\mathcal{O}(n)\) 的。
感性证明一下:设当前直线 \(x\) 在值域 \([l,r]\) 中,若当前值域没有直线,则不会向下递归,空间复杂度增加 \(\mathcal{O}(1)\),否则该线段会向下递归知道找到没有直线的区间,复杂度也是增加 \(\mathcal{O}(1)\)。
2.2.3 李超线段树合并
与普通线段树合并类似。
将 \(y\) 合并到 \(x\) 节点上时,我们只需要将 \(y\) 节点上的直线插入到 \(x\) 上即可。
因为直线的总数是 \(\mathcal{O}(n)\) 的,所以总复杂度依旧是 \(\mathcal{O}(n\log{V})\)。
若我们用 动态开点 + 回收内存 则空间复杂度依旧是 \(\mathcal{O}(n)\) 的。
2.2.4 可持久化李超树
动态开点,每加一条直线代表一个版本即可。
2.3 例题
考虑 DP,则 \(f_i = max(\frac {f_j} 2 + a_i \times a_j - b_j)\),这个式子显然可以斜率优化,不过我们暴力一点,直接将其化为 $f_i = \underline{a_j}_k \times \underline{a_i}_x + \underline{\frac {f_j} 2 - b_j}_b $,相当于有一些直线,给定横坐标,求最大纵坐标,这不就是我们李炒熟吗,复杂度 \(\mathcal{O}(n\log{V})\),而该题斜优复杂度是 \(\mathcal{O}(n)\) 的,复杂度稍劣,常数稍大,不过 暴力,好想,好写。
II P9020 [USACO23JAN] Mana Collection P
首先我们考虑如何求每个点的贡献,可以发现只有最后一次经过某点的时间是有用的,我们可以考虑 最少失去的法力值,设其为 \(w\) ,则答案即为 \(s \times \sum m - w\),\(n\) 较小,考虑状压 DP,因为询问规定了最终点,所以一维是不行的,设 \(f_{i,j}\) 表示已经最后一次经过状态 \(i\) 中的点,且当前在 \(j\) 位置的最小答案,则有状态转移方程:
其中 \(d_{i,j}\) 表示 \(i\) 到 \(j\) 的最短路,\(s_{i}\) 表示状态 \(i\) 中所有节点的 \(m\) 和。
然后对于答案,即为 \(ans = \underline{s_{i}}_k \times \underline{s}_x + (\underline{-f_{i,j}}_b)\),显然可以 李焯书 解决。
复杂度 \(\mathcal{O}(2^nn^2 + 2^nn\log{V} + q\log{V})\),当然也可以维护凸包,但是瓶颈不在这,复杂度差不多。
III COGS 3922. 删除题目
首先我们假设两条边 \((u1,v1)\),\((u2,v2)\) 中间未选择断边,则我们可以考虑 DP,设 \(f_i\) 表示 必须选 \((fa_i,i)\) 这条边时在其子树内走可以得到的最大贡献,则任意一点 \(v\) 在其子树内,则有转移方程 \(f_i = \max\limits_{v \in son(i)} f_v + size_v \times (size_u - size_v)\),这东西可以用 李炒熟 维护,对于树形结构可以用 李超树合并 解决子树问题。
但是这显然没完,上述所求的只是在一条链 从子树到祖先 上的路径,然后我们考虑在某个点合并两条链,假设合并两点 \(u,v\),则需要再减去重复的贡献 \(size_u \times size_v\),所以我们求得答案即为 \(f_u + f_v - size_u \times size_v\) 的最大值,显然也可以用 李焯书,然后考虑如何合并,可以用 淀粉质,复杂度是 \(\mathcal{O}(n\log^2{n})\) 的,但巨大长代码。
知周所众,dsu on tree 是可以代替一些淀粉质的,所以我们考虑用 dsu on tree,考虑每个点 \(x\),保存其 重儿子 的李超树,其余节点暴力查询答案,然后进行 李焯书贺兵 即可,复杂度也是 \(\mathcal{O}(n\log^2{n})\),但较好写,常数也小。
IV CF1175G Yet Another Partiton Problem
神题
显然有 DP \(f_{i,k} = \min\limits_{j=k-1}^{i-1} f_{j,k-1} + (i - j) \times \max\limits_{l=j+1}^{i}a_l\)。
这种东西一般决策单调性,但是没有。
对于内部的 \(max\) 我们显然可以单调栈处理,变为 \(max\) 值相同的几段,然后对于每一段设当前值为 \(w\),则我们考虑求 \(f_{j,k-1} + (i - j)\times w = f_{j,k-1} - j \times w + i\times w\),先不考虑 \(i \times w\),然后显然可以建下凸包斜率优化,求出每一段的最小值。
然后对于所有段,答案即为 \(\underline{f_{j,k-1} - j \times w}_b + i \times \underline{w}_k\) 最小值,前面划线部分对于每一段是不变的,所以相当于是一些斜线求 \(min\),可以 李炒熟 维护。
每加入一个新的决策,则单调栈中可能会合并一些区间,则我们建立的凸包需要合并,需要 启发式合并 保证复杂度为 \(\mathcal{O}(n\log{n})\),所以需要 deque 维护,而李超树需要撤销,可以可持久化解决。
总复杂度 \(\mathcal{O}(nk\log{n})\)。
傻逼凸包细节真多,要你李超树有何用?。
难写的一批
2.4 参考文章
3. 线段树分治
更像是一种利用线段树结构的思想。
3.1 算法思想
主要思想就是 离线按照时间轴 划分,类似的有 CDQ分治。
但线段树分治可以完成一个独有的操作 —— 删除,一些不可以支持删除的数据结构中可以使用,如 李超树,并查集等。
我们将时间轴按照线段树的结构分层,形成了 \(n\log{n}\) 个节点,每个点的意义就是 存在时间包含该点区间 的操作数集合,则对于每一个修改,把删除变为修改在一段 时间区间 的操作,可以在线段树上插入,复杂度是 \(\mathcal{O}(\log{n})\) 的。
则离线之后我们遍历整颗线段树(按照时间顺序),假设在当前节点,则将该点内的集合进行修改,在退出该点区间时再将 所有操作撤销,撤销是好办的,只需将原节点数据存到栈内即可,但是 不可维护复杂度均摊的数据结构(没有撤销除外)。
注意,支持删除改撤销是线段树分治的 功能,不是线段树分治必须撤销。
3.2 扩展应用
3.2.1 询问时间区间
当询问在一段 时间区间 内时,我们也可以按照时间分治,将询问区间拆分,遍历一遍找出最优答案,若修改是单点的,则可以不需要撤销,清空即可。
见例 II。
3.3 例题
板子,我们可以用 扩展域 并查集实现二分图的判断,因为路径压缩是均摊的,我们需要按秩合并即可。
复杂度 \(\mathcal{O}(n\log{n}\log{k})\)。
int n,m,k;
struct node{int fa,siz;};
struct DSU{
node s[N];
void build(){for(int i = 1;i <= n + n;i++)s[i] = {i,1};}
int find(int x){return x == s[x].fa ? x : find(s[x].fa);}
void merge(int x,int y,vpi &st){
x = find(x),y = find(y);
if(x == y)return;
if(s[x].siz < s[y].siz)swap(x,y);
st.pb({x,s[x]}),st.pb({y,s[y]});
s[y].fa = x,s[x].siz += s[y].siz;
}
void del(vpi d){//撤销
for(int i = (int)d.size()-1;i >= 0;i--){
pi now = d[i];
s[now.fi] = now.se;
}
}
}U;
struct made{int x,y;};
struct segment{
vector<made>s[N<<2];
bool vi[N<<2];
void insert(int p,int l,int r,int L,int R,made x){
if(L > R)return;
if(L <= l && r <= R)return s[p].pb(x),void();
int mid = l + r >> 1;
if(L <= mid)insert(p<<1,l,mid,L,R,x);
if(R > mid)insert(p<<1|1,mid+1,r,L,R,x);
}
void ask(int p,int l,int r){
vpi de;
if(!vi[p]){
for(made now : s[p]){
int u = now.x,v = now.y;
if(U.find(u) == U.find(v)){vi[p] = 1;break;}
U.merge(u,v + n,de),U.merge(u + n,v,de);
}
}
if(l == r)return vi[p] ? printf("No\n") : printf("Yes\n"),U.del(de),void();
vi[p<<1] = vi[p<<1|1] = vi[p];
int mid = l + r >> 1;
ask(p<<1,l,mid),ask(p<<1|1,mid+1,r),U.del(de);
}
}t;
int main(){
n = read(),m = read(),k = read();
U.build();
for(int i = 1;i <= m;i++){
int x = read(),y = read(),l = read(),r = read();
t.insert(1,1,k,l+1,r,{x,y});
}
t.ask(1,1,k);
return 0;
}
题意很考验语文功底,是shit,但是题目很好。
题意:修改使一个商店在当天暂时(明天就没了)有一个价值为 \(w\) 的商品,询问 \(d\) 天内 \([l,r]\) 区间内商店异或最大值。
首先子问题可以 可持久化 Trie 做,对于询问,我们拆分时间,而且不用撤销,直接分治即可。
最后是一棵树,即求当前时刻断开 \((x,y)\) 这条边两个联通块大小的乘积,我不会 LCT,只能另寻他法。
删除,我们可以想到用线段树分治,加上可撤销并查集,对于每条边,即存在的区间即为 除去询问 的所有区间,总区间个数是 \(\mathcal{O}(n)\) 的,插入修改,然后按照时间顺序遍历线段树,撤销并查集即可,复杂度 \(\mathcal{O}(n\log^2{n})\)。
一道好题。
这是一个树形结构,我们可以先拍成 \(dfn\) 序,然后对于每个星球,本质上就是在其子树内区间再 删去一些小子树 所构成的一些区间,因为最多 \(n\) 个操作,所以区间最多 \(\mathcal{O}(n)\) 个,线段树分治,把这些区间插入,然后我们考虑如何求答案。
\(y,z\) 是没用的,最终答案即为 \((x_0 - x)^2 + c\),拆开得 \(- 2x_0x + {x_0}^2 + x^2 + c\)。
我们考虑斜率优化,在看这个式子 \(s = - 2x_0x + {x_0}^2 + x^2 + c\),移项得 \(x^2 + c = 2x_0x - {x_0}^2 + s\),即点 \((x,x^2 + c)\) 斜率为 \(2x_0\),求最小截距,维护下凸包即可,若二分则复杂度是 \(\mathcal{O}(n\log^2{n})\),只需将 \(x\) 与斜率 \(x_0\) 都从小到大排序,我们可以利用单调栈达成 \(\mathcal{O}(n\log{n})\) 的复杂度。
3.4 参考文章
4. Segment Tree Beats
又称吉司机线段树,简称 STB。
当存在一些形如 使区间 \([l,r]\) 中的数 \(a_i \rightarrow \min(a_i,k)\) 或 \(a_i \rightarrow \max(a_i,k)\)。
吉司机线段数是势能线段树,复杂度约 \(\mathcal{O}(n\log^2{n})\)。
4.1 算法简介
上述操作我们称其为 区间最值操作。
我们考虑简单的操作,有区间求和与区间最值操作,我们以区间取 \(\min(a_i,k)\) 为例。
可以发现该操作只会影响 \(> k\) 的节点,所以我们考虑在线段树节点中存储 \(mx\) 最大值,\(se\) 次大值,以及 \(cnt\) 最大值出现的次数。这样我们分类讨论:
- \(k \geq mx\),则该操作不会对该区间有任何影响。
- \(mx > k > se\),的最大值会改变,且区间和会减去 \(cnt \times (mx - k)\),记下懒标记即可。
- \(k \leq se\),我们无法简单的处理,向下递归即可。
可以证明,该算法的复杂度是 \(\mathcal{O}(m\log{n})\) 的。
- 证明: 先咕掉。
4.2 拓展操作
4.2.1 区间加法
加上区间加法操作,我们有两种方法:
-
运用多种懒标记:\((add,k)\) 表示先加上 \(add\) 在与 \(k\) 取 \(min\),即 \(a_i = \min(a_i + add,k)\)。
考虑合并两个懒标记 \((add_1,k_1) \ast (add_2,k_2) = (add_1 + add_2,\min(k_1 + add_2,k_2))\),注意顺序,可以看 \(0.\)。
-
一个核心思想:将区间最值问题转化为对值域的区间加减问题。
我们可以发现 \(4.1\) 中我们只进行的对最大值的修改,其实就相当于在该区间对最大值进行减 \(mx - k\) 的操作,所以我们可以将每个区间的数分为 最大值 与其他数,即划分数域,这样我们只需要维护两个不同的区间加懒标记即可。
两种方法都有用途,建议都学。
论文中证明了带有区间加法操作的总复杂度为 \(\mathcal{O}(m\log^2{n})\)。
4.2.2 与历史最值问题的结合
详见 \(5.2.2\)
4.3 例题
这是模板题,这里没用划分数域的方法。
struct STB{
#define ls p<<1
#define rs p<<1|1
int mx[M],se[M],cn[M],la[M];//最大 次大 最大值个数 懒标记
ll sum[M];
void pushup(int p){
sum[p] = sum[ls] + sum[rs];
if(mx[ls] > mx[rs])mx[p] = mx[ls],cn[p] = cn[ls],se[p] = max(mx[rs],se[ls]);
else if(mx[ls] < mx[rs])mx[p] = mx[rs],cn[p] = cn[rs],se[p] = max(mx[ls],se[rs]);
else mx[p] = mx[ls],cn[p] = cn[ls] + cn[rs],se[p] = max(se[ls],se[rs]);
}
void update(int p,int k){
if(k < mx[p]){
sum[p] -= (ll)(mx[p] - k) * cn[p];
mx[p] = la[p] = k;
}
}
void pushdown(int p){
if(la[p] != inf)update(ls,la[p]),update(rs,la[p]);
la[p] = inf;
}
void build(int p,int l,int r){
la[p] = inf;
if(l == r)return sum[p] = mx[p] = a[l],se[p] = -inf,cn[p] = 1,void();
int mid = l + r >> 1;
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int l,int r,int L,int R,int k){
if(k >= mx[p])return;
if(L <= l && r <= R && k < mx[p] && k > se[p])return update(p,k),void();
int mid = l + r >> 1;
pushdown(p);
if(L <= mid)modify(ls,l,mid,L,R,k);
if(R > mid)modify(rs,mid+1,r,L,R,k);
pushup(p);
}//区间最值操作
int cal1(int p,int l,int r,int L,int R){
if(L <= l && r <= R)return mx[p];
int mid = l + r >> 1;
pushdown(p);
if(R <= mid)return cal1(ls,l,mid,L,R);
else if(L > mid)return cal1(rs,mid+1,r,L,R);
else return max(cal1(ls,l,mid,L,R),cal1(rs,mid+1,r,L,R));
}
ll cal2(int p,int l,int r,int L,int R){
if(L <= l && r <= R)return sum[p];
int mid = l + r >> 1;
pushdown(p);
if(R <= mid)return cal2(ls,l,mid,L,R);
else if(L > mid)return cal2(rs,mid+1,r,L,R);
else return cal2(ls,l,mid,L,R) + cal2(rs,mid+1,r,L,R);
}
#undef ls
#undef rs
}t;
给出一个实现。
操作及其的多,我们若用一坨懒标记则会麻烦的不得了,需要用到划分数域的方法。
我们将区间中的数分为 最大值、最小值以及其他值,将区间最值操作改为值域区间加减操作,我们维护三个懒标记,每个节点要存 最大最小值,次大次小值,最大最小值次数,在区间内只存在很少数时会存在数域重叠的情况,会多算答案,需要分类讨论一下,码量很大。
比如最大值等于最小值,次大值等于最小值等。
III P10638 BZOJ4355 Play with sequence
对于操作一,我们可以视为 \(a_i \rightarrow max(a_i - inf,c)\),这样我们把前两个操纵归结成了一个,即先区间加,再区间取最值。
对于操作三,我们只需要找到 \(\sum cnt \times [mi == 0]\),即可,类似区间取最值的写法。
复杂度 \(\mathcal{O}(m\log^2{n})\)。
IV P9631 [ICPC2020 Nanjing R] Just Another Game of Stones
好题,首先操作 \(1\) 是 STB,利用异或的性质可以处理。
对于操作 \(2\),我们知道当且仅当所有石堆个数的异或和为 \(0\) 时先手必败,设 \(s = \operatorname{xor}\limits_{i=l}^r a_i \operatorname{xor} x\),即我们需要在 \([l,r]\) 中一个石堆 \(a_i\) 选出 \(k\) 个,变为了 \(a_i'\),使得 \(s \operatorname{xor} a_i \operatorname{xor} a_i' = 0\),即 \(a_i' = a_i \operatorname{xor} s\)。
即 \(a_i \rightarrow a_i \operatorname{xor} s\),需要满足 \(a_i \geq a_i \operatorname{xor} s\),可以发现若 \(s\) 最高位 \(1\) 为 \(bit\),当且仅当 \(a_i\) 在 \(bit\) 位为 \(1\) 时,符合 \(a_i \geq a_i \operatorname{xor} s\)。
- 证明:很好证,大于 \(bit\) 的位数是不变的,若 \(a_i\) 的 \(bit\) 位为 \(0\),则异或后一定为 \(1\),最后一定大于 \(a_i\)。
这样我们拆下位,记录区间异或和,以及区间内各二进制位数的数目。
复杂度 \(\mathcal{O}(m\log{n}\log{V})\)。
4.4 参考文章:
吉司机线段树(Segment Tree Beats!)复杂度分析 - Charles Wu
区间最值操作与历史最值操作 - 吉如一 2016集训队论文
5. 历史最值问题
对于一个数列,在操作的途中,我们钦定另一个数列 \(b_i\) 表示 \(a_i\) 的历史,例如历史最大/最小值,历史和。
5.1 算法介绍
- 历史最大值:每次操作后,我们都令 \(b_i = \max(b_i,a_i)\),称 \(b\) 为 \(a\) 的历史最大值数组。
- 历史最小值:每次操作后,我们都令 \(b_i = \min(b_i,a_i)\),称 \(b\) 为 \(a\) 的历史最小值数组。
- 历史和:每次操作后,我们都令 \(b_i = b_i + a_i\),称 \(b\) 为 \(a\) 的历史和数组。
我们继续从简单的懒标记方法引入。
例题:P4314 CPU 监控
- 查询区间最大值。
- 查询区间历史最大值。
- 区间加,区间赋值。
我们把通常的标记与数据都分为两类,最大值与历史最大值 \(mxh\),加法标记与历史最大加法标记 \(addh\),覆盖标记与历史最大覆盖标记 \(tagh\)。
考虑标记合并,我们定于顺序为先加后覆盖,所以对于加法标记,若该区间内已经有覆盖标记,可视为将覆盖标记增加。
对于历史最大值,有 \((mx,mxh) \ast (add,addh) = (mx + add,\max(mxh,mx + addh))\) 与 \((mx,mxh) \ast (tag,tagh) = (tag,\max(mxh,tagh))\)。
对于历史标记合并,有:
- \((add_1,addh_1) \ast (add_2,addh_2) = (add_1 + add_2,\max(addh_1,add_1 + addh_2))\)。
- \((tag_1,tagh_1) \ast (tag_2,tagh_2) = (tag_2,\max(tagh_1,tagh_2))\)。
- \((tag,tagh) \ast (add,addh) = (tag + add,\max(tagh,tag + addh))\)。
第三类转移与所定义的标记顺序相关。
总复杂度 \(\mathcal{O}(m\log{n})\)。
总结:其实本质上就是暴力的将标记与数据拆分为普通标记与另类标记,在进行合并的时候会更加麻烦。
5.2 扩展应用
5.2.1 与区间最值问题的结合
对于一些问题,不仅有历史操作,还有最值操作。
我们定义标记 \(tag = (add,k)\) 表示先加后向 \(k\) 取 \(\max\),则前三个操作可以归结为标记 \((-x,0),(x,-inf),(-inf,x)\)
然后在拆分为普通标记与历史最大标记,普通标记合并有 \((add_1,k_1) \ast (add_2,k_2) = (add_1 + add_2,\max(k1 + add_2,k_2))\)。
对于历史最大标记,我们可以把该标记作为一个分段函数,合并两个标记即合并连个函数。
如图有:\((addh_1,kh_1) \cdot (addh_2,kh_2) = (\max(addh_1,addh_2),\max(kh_1,kh_2))\)。
所以对于标记合并有 \((tag_1,tagh_1) \ast (tag_2,tagh_2) = (tag_1 \ast tag_2,tagh_1 \cdot (tag_1 \ast tagh_2))\)。
比较难写,建议用结构体维护标记,以及半群转移。
复杂度 \(\mathcal{O}(m\log{n})\)。
5.2.2 划分数域方法。
一些情况下,历史标记无法合并,这时就不能用普通的懒标记维护,例如区间最值与所查询答案相反。
例题:P6242 【模板】线段树 3(区间最值操作、区间历史最值)
该题操作存在区间取最小值,但是询问的是历史最大值。
此时我们无法合并历史最大标记,考虑数域划分,将区间最值转化为值域加减问题,我们需要先将值域分为最大值与其他值,再拆分为加法标记与历史最大加法标记,所以我们需要维护 \(4\) 个加法标记。
标记合并与上述类似,不在赘述,只需要分类下即可。
复杂度 \(\mathcal{O}(m\log^2{n})\)。
另一个模板:#169. 【UR #11】元旦老人与数列
5.2.3 无区间最值的历史和
这里只讨论无区间最值的历史和。
有的不会
5.2.3.1 历史最小值的和
即求 \(\sum\limits_{i=l}^{r} b_i\),每次操作有 \(b_i = \min(b_i,a_i)\)。
我们定义辅助数组 \(c\),每一时刻都有 \(c_i = a_i - b_i\)。
当我们将 \(a_i\) 加上 \(k\) 时,若 \(b_i\) 不变,则有 \(c_i\) 也加上 \(k\),否则 \(c_i\) 会得 \(0\),即 \(c_i = \max(c_i + k,0)\)。
我们分别维护 \(a\) 与 \(c\),做差就可得到 \(b\),复杂度 \(\mathcal{O}(m\log^2{n})\)。
5.2.3.2 历史最小值的和
即求 \(\sum\limits_{i=l}^{r} b_i\),每次操作有 \(b_i = \max(b_i,a_i)\)。
我们定义辅助数组 \(c\),每一时刻都有 \(c_i = a_i - b_i\),类似的可以作为 \(c_i = \min(c_i + k,0)\),复杂度 \(\mathcal{O}(m\log^2{n})\)。
5.2.3.3 历史和的和
即求 \(\sum\limits_{i=l}^{r} b_i\),每次操作有 \(b_i = b_i + a_i\)。
我们定义辅助数组 \(c\),令 \(t\) 表示目前总操作数,每一时刻有 \(c_i = b_i - t \times a_i\),则当给 \(a_i\) 加上 \(k\) 时,有 \(c_i\) 减去 \(k \times t\),这是简单的区间加减操作,复杂度 \(\mathcal{O}(m\log{n})\)。
5.2.4 有区间最值的历史和
咕咕咕....
5.3 例题
5.4 参考文章
区间最值操作与历史最值操作 - 吉如一 2016集训队论文
1