线段树进阶笔记

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 例题

I P2839 [国家集训队] middle

我们考虑二分 \(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 例题

I P8726 [蓝桥杯 2020 省 AB3] 旅行家

考虑 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\) 位置的最小答案,则有状态转移方程:

\[f_{i,j} = \min {f_{la,k} + d_{k,j} \times s_{la}} \]

其中 \(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 参考文章

线段树的高级用法 - Alex_wei

3. 线段树分治

更像是一种利用线段树结构的思想。

3.1 算法思想

主要思想就是 离线按照时间轴 划分,类似的有 CDQ分治
但线段树分治可以完成一个独有的操作 —— 删除,一些不可以支持删除的数据结构中可以使用,如 李超树,并查集等

我们将时间轴按照线段树的结构分层,形成了 \(n\log{n}\) 个节点,每个点的意义就是 存在时间包含该点区间 的操作数集合,则对于每一个修改,把删除变为修改在一段 时间区间 的操作,可以在线段树上插入,复杂度是 \(\mathcal{O}(\log{n})\) 的。

则离线之后我们遍历整颗线段树(按照时间顺序),假设在当前节点,则将该点内的集合进行修改,在退出该点区间时再将 所有操作撤销,撤销是好办的,只需将原节点数据存到栈内即可,但是 不可维护复杂度均摊的数据结构(没有撤销除外)。

注意,支持删除改撤销是线段树分治的 功能,不是线段树分治必须撤销。

3.2 扩展应用

3.2.1 询问时间区间

当询问在一段 时间区间 内时,我们也可以按照时间分治,将询问区间拆分,遍历一遍找出最优答案,若修改是单点的,则可以不需要撤销,清空即可

见例 II

3.3 例题

I P5787 二分图 /【模板】线段树分治

板子,我们可以用 扩展域 并查集实现二分图的判断,因为路径压缩是均摊的,我们需要按秩合并即可。

复杂度 \(\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;
}

II P4585 [FJOI2015] 火星商店问题

题意很考验语文功底,是shit,但是题目很好。

题意:修改使一个商店在当天暂时(明天就没了)有一个价值为 \(w\) 的商品,询问 \(d\) 天内 \([l,r]\) 区间内商店异或最大值。

首先子问题可以 可持久化 Trie 做,对于询问,我们拆分时间,而且不用撤销,直接分治即可。

代码

III P4219 [BJOI2014] 大融合

最后是一棵树,即求当前时刻断开 \((x,y)\) 这条边两个联通块大小的乘积,我不会 LCT,只能另寻他法。

删除,我们可以想到用线段树分治,加上可撤销并查集,对于每条边,即存在的区间即为 除去询问 的所有区间,总区间个数是 \(\mathcal{O}(n)\) 的,插入修改,然后按照时间顺序遍历线段树,撤销并查集即可,复杂度 \(\mathcal{O}(n\log^2{n})\)

代码

IV P5416 [CTSC2016] 时空旅行

一道好题。

这是一个树形结构,我们可以先拍成 \(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 参考文章

线段树分治总结 - foreverlasting

线段树分治 - Autoint's

线段树与离线询问 - OI-wiki

一些常用的数据结构维护手法 - command_block

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 例题

I hdu 5306. Gorgeous Sequence

这是模板题,这里没用划分数域的方法。

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; 

给出一个实现。

II P10639 BZOJ4695 最佳女选手

操作及其的多,我们若用一坨懒标记则会麻烦的不得了,需要用到划分数域的方法。

我们将区间中的数分为 最大值、最小值以及其他值,将区间最值操作改为值域区间加减操作,我们维护三个懒标记,每个节点要存 最大最小值,次大次小值,最大最小值次数,在区间内只存在很少数时会存在数域重叠的情况,会多算答案,需要分类讨论一下,码量很大。

比如最大值等于最小值,次大值等于最小值等。

代码

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})\)

代码1(普通懒标记做法)

代码2(划分数域做法)

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 与区间最值问题的结合

对于一些问题,不仅有历史操作,还有最值操作。

例题:#164. 【清华集训2015】V

我们定义标记 \(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))\)

对于历史最大标记,我们可以把该标记作为一个分段函数,合并两个标记即合并连个函数。

image

如图有:\((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

posted @ 2024-09-01 21:43  oXUo  阅读(66)  评论(0编辑  收藏  举报
网站统计