浅谈双半群模型

· · 算法·理论

1. 前置

我们在学习群论时,会遇到以下概念:

这些概念很重要,请读者完全理解后再往下看。

2. 双半群模型

2.1. 定义

下面这什么玩意儿以后一定要重写

给定:

  1. D+D\rightarrow D
  2. D \times T \rightarrow D
  3. T\times T\rightarrow T
  4. 结合律:\forall a\in Db,c\in T(a\times b)\times c=a\times(b\times c)
  5. 分配律:\forall a,b \in Dc\in T(a+b)\times c=a\times c+b\times c

我们用 n=\lvert I \rvert 表示初始权值的个数,用 m 表示范围修改查询问题的操作数。
按顺序执行 m 个操作,每个操作是一个修改或者查询操作。
t 个操作定义为:

  1. 范围修改:给出 q_t \in Qf_t \in T,定义 \mathrm{val}_t(x)=\begin{cases} \mathrm{val}_{t-1}(x)\times f_t &\text{if } x\in q_t \\ \mathrm{val}_{t-1}(x) &\text{if } x\notin q_t \end{cases}
  2. 范围查询:给出 q_t \in Q,定义 \mathrm{val}_t(x)=\mathrm{val}_{t-1},答案为 \sum_{x\in q_t} \mathrm{val}_{t}(x)

如果两个群满足开头的条件,我们称其构成双半群结构
如果一个问题满足上述条件,我们称这样的问题为双半群模型

2.2. 具体体现

上一节给出来的模型是从纯数学的角度出发,你可以发现这玩意儿相当的抽象,不太好理解。
事实上它在算法竞赛中通常隐藏在分治维护区间信息后面出现,再具体一点它就是各种线段树,平衡树所扎根的大地(极特殊的除外)。
聪明的读者就要问了:这个模型怎么跟这堆树扯上关系的呢?
下文我们就以线段树为例,把它的各个细胞组织拆开来看看。

首先信息肯定是线段树不可或缺的一部分(没有信息我写它干嘛),它无疑可以被抽象成一个集合 D
我们定义一个二元运算符 + 表示两个信息的合并,可以发现这些性质:

  1. 显然两个信息合并后还是信息,并不会变成什么息信,即 D + D \rightarrow D
  2. 线段树上合并信息的先后顺序是无所谓的,即满足结合律 \forall a,b,c \in Da+(b+c)=(a+b)+c
  3. 有没有交换律 \forall a,b \in Da+b=b+a 呢?我们发现不一定,在维护区间和时显然有,但是在维护最大子段和时又没有了,所以信息不一定满足交换律。

聪明的读者肯定会发现,这不就是我们一开头提到的半群和交换半群吗?
所以我们可以得出一个结论:线段树上的信息所属的集合 D 加上合并操作 + 后,至少也是一个半群,在特殊情况下是一个交换半群。

我们在做题时还会在线段树上打上标记,如法炮制,将它也抽象成一个集合 T
我们定义一个二元运算符 \times 表示两个标记的复合,可以发现这些性质:

  1. 显然两个标记复合后还是标记,并不会变成什么记标,即 T \times T \rightarrow T,我们也可以称标记对复合封闭
  2. 线段树上标记复合的先后顺序是无所谓的,即满足结合律 \forall a,b,c \in Ta\times(b\times c)=(a\times b)\times c
  3. 如果我们什么都没做,那么肯定会出现空标记,我们把空标记记作 \epsilon,则满足 \forall a\in T\epsilon \times a = a \times \epsilon = a

聪明的读者肯定会发现,这不就是我们一开头提到的幺半群吗?
所以我们又可以得出一个结论:线段树上的标记所属的集合 T 加上复合操作 \times 后,是一个幺半群。

我们想想半群 D 和幺半群 T 之间有什么关系,可以发现这些性质:

  1. 我们打上的标记最终会作用到信息上,而信息被影响后还是信息,即 D \times T \rightarrow D
  2. 我们发现,一个信息先应用一个标记再应用另一个标记,结果和应用两个标记的复合一致,即结合律 \forall a\in Db,c\in T(a\times b)\times c=a\times(b\times c)
  3. 我们发现,标记下传后,当前结点的信息等于两个子树的信息的合并,即 \times+ 有分配律\forall a,b \in Dc\in T(a+b)\times c=a\times c+b\times c

聪明的读者肯定会发现,这不就是我们一开头提到的双半群结构吗?
所以我们还可以得出一个结论,线段树上的信息所属的半群 (D,+) 和标记所属的幺半群 (T,\times) 构成一个双半群结构。
而如果我们再把题目中要求的区间修改区间查询加入其中,它就变成了我们熟知的双半群模型。

2.3. 原理

说了这么多,也该进入正题——通过双半群模型构造我们想要的数据结构了,下面依然还是以线段树为例。

2.3.1. 暴力进行时

有一个问题,要求支持区间加、区间乘、区间和,如果我们偷懒,想到线段树可以维护矩阵,写出如下式子:

\begin{bmatrix} add & mul \\ 0 & 1 \end{bmatrix} \times \begin{bmatrix} sum \\ 1 \end{bmatrix} = \begin{bmatrix} mul\times sum+add \\ 1 \end{bmatrix}

可以发现这个写法很笨,明明只要两个变量维护的东西我们却用了四个变量。

但还有没有更笨的方法呢?我们考虑一个十分抽象的问题:

  1. 维护一个序列,序列里每个元素是个抽象的数据,其类型 M 的具体实现你是不知道的。
  2. 要求支持操作:给你一个黑盒函数 f(M) 使得 f(M) \rightarrow M,以及一个代表一段区间的数组,要求你作用这个函数到数组内的每个元素。
  3. 支持查询:给你一个黑盒函数 ans(M) 使得 ans(M)\rightarrow M,以及一个代表一段区间的数组,要求你作用这个函数到数组内的每个元素,然后得出结果。

可以发现,所有区间修改区间查询的线段树问题都可以抽象到这个问题、我们考虑一下这个问题怎么用线段树做。
因为我们对这些东西一无所知,所以我们无法压缩信息和标记,只有唯一的方法:

因为我们没有任何的简化,所以只能采取暴力得让人发指的方法,复杂度估计是 O(n^3 \log n) 以上,不过我们的确是用线段树解决的。(笑)

我们为什么要举这个比暴力还要暴力的构造出来呢?因为这个憨憨是线段树维护区间信息的某种基础,它具有某种“万有性”。
比如说,如果 A 和 B 设计了不同的以信息 + 标记为基础的数据结构,而如果 A 所维护的东西完全可以实现 B 所维护的东西(换而言之,即使不知道具体结构,我们也可以通过 A 的信息推算出 B 的所有信息,用 A 的标记实现 B 的所有标记),那我们就可以称 A 的数据结构更强(不考虑时空复杂度)。
如果这么说,上面这个憨憨构造就比所有的以信息 + 标记为基础的数据结构都强!

2.3.2. 返璞归真

事实上,我们虽然做题时不可能用这个憨憨构造,但是我们发扬拿来主义,借鉴个一下。
具体的说,设前面那个构造的信息为 D_0=代表总区间的数组,标记为 T_0={所有 f(M) \rightarrow M 的函数},则我们可以注意到以下事实:

这里非平凡的事情只有:标记对复合是封闭的和上面的两个等式。
我们可以吃惊地发现:只要选定了 T\subset T_0 以及 Dg:D_0\rightarrow D,满足复合的封闭性以及上面两条等式,它们一定满足双半群性质
结合律分配律什么的完全不用管,因为我们选取标记和信息的方式决定它们一定正确。
想想看,区间信息能正确合并怎么可能不结合?标记能复合怎么可能不结合?标记能正确影响区间怎么会不能分配?
而在实际实现中,我们一般不会把 T 真的实现成函数——例如在区间加、区间乘、区间和中,我们只要发现修改最后都可以被表示成形如 f(x)=ax+b 的函数,之后用一个二元组 (a,b) 来表示 f(x)=ax+b 这个函数即可。
这样一来,我们还可以得出两条等式:

  1. f_t 是标记 t 简化前代表的函数,则 f_t\times g(d)=g(d \times f_t)。也就是说简化后的标记作用在信息上等价于与原本的函数作用在区间上,相当于我们正确地用标记简化了原函数。

我们看到,我们最终返璞归真,回到了封闭律主宰一切的时期。

2.3.3. 实际方法

上一节已经提到构造需要满足的三个条件:

  1. 如果知道左右区间的信息,可以合并得到大区间的信息(D + D \rightarrow D)。
  2. 如果知道标记和之前的信息,可以计算得到新的信息(D \times T \rightarrow D)。
  3. 标记对复合封闭(T \times T \rightarrow T)。

据此,我们可以得出一个构造流程:

  1. 构造可行的信息 D,检验是否满足 D+D\rightarrow D,并得出转移形式。
  2. 通过计算各个操作对信息 D 的影响,总结出标记 T,检验是否满足 D\times T \rightarrow D,并得出转移形式。
  3. 通过计算验证标记 T 是否满足 T\times T \rightarrow T,并得出转移形式。
  4. 如果出现不满足的情况,从 1 重新开始。

上述依据是什么呢?
我们从题目中最先构造出来的肯定是信息,其次由操作可以归纳出我们要的函数简化形式(标记),最后得出来的就是标记的复合了。
实际上双半群模型的强悍之处,就在于只要我们知道我们要维护的信息 D,以及我们要支持的操作,就可以推出我们需要维护的标记 M 的形式,以及合并与复合的三个式子。

3. 构造矩阵

3.1. 引入

我们在前文提到过使用矩阵的笨做法,不过笨归笨,比起暴力和那个憨憨构造来说也是一个好方法,而且有些题目也基本只能构造出矩阵来解决。
不过虽然矩阵慢是慢了点,但它有一些特殊性质。
比方说,它天然就是一个幺半群。这意味着如果我们的信息和标记都是矩阵的形式,则其天然就是一个双半群模型。
这给我们的构造带来了极大的方便(读者可以参照下面几大节对比体会)。
比方说构造 D\times T \rightarrow D 时,我们发现我们只要对于题目中的每一个操作都构造出一个矩阵就可以了。
另外只要 T 构造出来,它就天然满足 T\times T\rightarrow T,我们就不需要另外再推导转移形式了。
下面我们将会给出两个例子来详细地讨论一下。

3.2 P7453 [THUSCH2017] 大魔法师

有三个序列 a,b,c,长度均为 n
又有 7 种操作,共 m 个,分别为:

  1. a_i=a_i+b_i
  2. b_i=b_i+c_i
  3. c_i=c_i+a_i
  4. a_i=a_i+k
  5. b_i=b_i\times k
  6. c_i=k
  7. 查询 \sum_{i=l}^{r} a_i,\sum_{i=l}^{r} b_i,\sum_{i=l}^{r} c_i

我们发现这里的操作实在太多太复杂了,几乎不可能直接推导,考虑构造矩阵简化思考。
首先考虑信息,我们肯定要有 a,b,c 在里头,而且还可以发现,我们还要有一个地方来支持 a_i+k。(自行构造一下 3\times 3 的矩阵标记就知道为什么了)
也就是说我们构造出来的信息应该是

\begin{bmatrix} a_i & b_i & c_i & 1\\0 & 0& 0& 0\\0 & 0& 0& 0\\0 & 0& 0& 0\end{bmatrix}

这里为了兼顾代码实现,所以使用了 4\times 4 的矩阵而不是 1\times 4 的向量。

我们再考虑各个操作所对应的矩阵,可以得到以下矩阵: $$1. \begin{bmatrix} 1 & 0 & 0 & 0\\ 1 & 1 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0& 0& 1\end{bmatrix}\quad 2. \begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\0 & 1 & 1 & 0\\0 & 0& 0& 1\end{bmatrix} \quad 3. \begin{bmatrix} 1 & 0 & 1 & 0\\ 0 & 1 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0& 0& 1\end{bmatrix}$$ $$4. \begin{bmatrix} 1 & 0 & 0 & 0\\ 1 & 1 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0& 0& 1\end{bmatrix}\quad 5. \begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\0 & 1 & 1 & 0\\0 & 0& 0& 1\end{bmatrix} \quad 6. \begin{bmatrix} 1 & 0 & 1 & 0\\ 0 & 1 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0& 0& 1\end{bmatrix}$$ 引入部分已经讲过了,只要我们对每个操作都构造出一个矩阵,则天然满足 $D \times T\rightarrow D$ 和 $T\times T \rightarrow T$。 于是我们的构造就结束了,这道题也就做完了。 ## 3.3. [CF718C Sasha and Array](https://www.luogu.com.cn/problem/CF718C) + 我们定义 $f_i$ 为第 $i$ 个斐波那契数。 + 并且给定一个 $n$ 个数的序列 $a$。有 $m$ 次操作,操作有两种: 1. 将 $a_l\sim a_r$ 加上 $k$。 2. 求 $\displaystyle\left(\sum_{i=l}^r f_{a_i}\right)\bmod (10^9+7)$。 + $1\le n,m\le 10^5$,$1\le a_i\le 10^9$。 这道题就更容易了。 我们在线段树上不维护 $a_i$,而是维护 $f_{a_i}$ 所代表的矩阵,我们记其为 $F_i$,只有这样我们才支持区间查询操作。 首先是 $D + D \rightarrow D$ 还是容易的,矩阵加法而已。 其次是 $D \times T \rightarrow D$,我们考虑操作对信息产生了什么影响,如果 $a_i=a_i+k$,则这个元素的答案就从 $f_{a_i}$ 变为 $f_{a_i+k}$,这个变化怎么用矩阵表示出来呢? 我们知道,在矩阵加速斐波那契数列时,我们的转移是 $F\times A = F'$,这里 $A$ 是我们的转移矩阵,为 $$\begin{bmatrix} 1 & 1\\1 & 0\end{bmatrix}$$ 也就是说,$f_i \times A^k =f_{i+k}$,所以如何转移就很显然了:如果我们将 $a_l\sim a_r$ 加上 $k$,也就对应着 $\forall i\in [l,r],F_i=F_i\times A^k$。 我们的标记 $T$ 就是 $A$ 的若干次方,操作转化成了区间矩阵乘法,至于 $T\times T \rightarrow T$ 则不言而喻,显然成立。 于是我们的构造就结束了,这道题也就做完了。 # 4. 构造 $k$ 元组 ## 4.1. 引入 矩阵虽然方便,但是它有一个很致命的缺点——常数太大。 还记得我们前文提到的那个使用矩阵的笨方法吗?那个问题明明构造标记只要用到一个二元组(两个变量),但我们使用了一个 $2\times 2$ 的矩阵(四个变量)。实际上我们可以发现,第二行的 $0$ 和 $1$ 其实是没多少意义的,只不过是转移的需要。 有没有一种可能,我们可以仿照它,挤出矩阵中的水分,把它缩成一个 $k$ 元组呢? 有的,在转移并不复杂的时候,我们就可以另辟新径,直接构造一个 $k$ 元组。 即使转移有些复杂,我们仍然可以通过矩阵减少思维量,同时据此得到我们要的转移形式。 ## 4.2. 区间加、乘、和 因为重头戏在下面,所以这里直接把结论丢出来: + 信息 $D$ 就形如 $(sum,len)$,为方便起见,我们也加上了区间长度。 + 标记 $T$ 就形如 $(add,mul)$,表示把区间内的每一个数 $x$ 变成 $mul\times x+add$。 + 信息的合并($D + D \rightarrow D$)就是 $(sum,len)+(sum',len')=(sum+sum',len+len')$。 + 标记对信息的作用($D\times T \rightarrow D$)就是 $(sum,len)\times (add,mul)=(mul\times sum+add,len)$。 + 标记的复合($T \times T \rightarrow T$)就是 $(add,mul)\times (add',mul')=(mul\times mul',mul'\times add+add')$。 ## 4.3. 区间加、区间历史和 ### 4.3.1. 一维([P3246 [HNOI2016] 序列](https://www.luogu.com.cn/problem/P3246)) 我们可以把题意转化为: > 给定序列 $a$ 和 $s$,有两个操作。 > > 1. 给出一段区间 $[l,r]$,令 $\forall i\in[l,r],a_i=a_i+k$。 > 2. $\forall i \in [1,n],s_i=s_i+a_i$。 我们首先考虑我们需要维护的信息 $D$,显然区间和与区间版本和是必需的,方便起见加入区间长度,这样信息 $D$ 就可以表示成 $(sa,sum,len)$ 的形式,其中 $sa$ 是区间和,$sum$ 是区间版本和。 $D + D \rightarrow D$ 还是很好想的,即 $(sa,sum,len)+(sa',sum',len')=(sa+sa',sum+sum',len+len')$,具体到代码就是 ```cpp node operator +(const node &x,const node &y){ node z={0,0,0}; z.a=x.a+y.a; z.sum=x.sum+y.sum; z.len=x.len+y.len; return z; } ``` 接下来我们考虑我们需要执行的操作。我们把更新历史版本和也看作是一个操作,那么总共就有两个操作,另一个是区间加。 再考虑这两个操作会对信息产生怎样的影响: 1. 令函数 $f$ 代表区间加操作,则有 $f((sa,sum,len))=(sa+len\times x,sum,len)$,其中 $x$ 是我们要加减的数。 2. 令函数 $g$ 代表区间更新操作,则有 $g((sa,sum,len))=(sa,sum+y \times sa,len)$,其中 $y$ 是更新次数。 再考虑这两个操作结合起来会产生怎样的影响,也就是求函数 $f$ 和 $g$ 的复合,可以得到 $g(f((sa,sum,len)))=(sa+len\times x,sum+y\times sa+len\times x \times y)$。 事实上可以发现,无论我们以何等顺序、何等次数执行这两个操作,最终的结果都可以表示成上面的那个形式。 也就是说,如果我们令 $F$ 为若干个 $f$ 和 $g$ 的复合,则 $F((sa,sum,len))=(sa+len\times taga,sum+upd\times sa+len\times cnta)$。 我们可以记一个三元组 $(taga,upd,cnta)$ 来表示 $F$,事实上我们可以惊讶的发现,这个三元组其实就是我们要维护的标记 $T$ 的形式。 于是我们可以开始考虑 $D \times T \rightarrow D$,这个也简单,上文已经得出来 $(sa,sum,len)\times (taga,upd,cnta)=(sa+len\times taga,sum+upd\times sa+len\times cnta)$,具体到代码就是 ```cpp node operator *(const node &x,const tag &y){ node z={0,0,0}; z.sum=x.sum+x.a*y.upd+x.len*y.cnta; z.a=x.a+x.len*y.taga; z.len=x.len; return z; } ``` 其次考虑 $T \times T \rightarrow T$,这个是最复杂的,不过我们有 $D\times (T\times T) = (D\times T)\times T$。也就是说我们可以通过 $(D\times T)\times T$ 所得到的系数,得到 $T\times T$ 的结果。 过程留给读者仿照上例自行推导,最终可以得到 $((sa,sum,len) \times (taga,upd,cnta))\times (taga',upd',cnta')=(sa+len\times (taga+taga'),sum+(upd+upd')\times sa+len\times (cnta+taga\times upd'+cnta'))$。 也就是说 $(taga,upd,cnta)\times(taga',upd',cnta')=(taga+taga',upd+upd',cnta+taga*upd'+cnta')$,具体到代码就是 ```cpp tag operator *(const tag &x,const tag &y){ tag z={0,0,0}; z.cnta=x.cnta+x.taga*y.upd+y.cnta; z.upd=x.upd+y.upd; z.taga=x.taga+y.taga; return z; } ``` 于是这棵线段树就这么被构造出来了。 ### 4.3.2. 二维([P8868 [NOIP2022] 比赛](https://www.luogu.com.cn/problem/P8868)) 我们可以把题意转化为: > 给定序列 $a$、$b$ 和 $s$,有三个操作。 > > 1. 给出一段区间 $[l,r]$,令 $\forall i\in[l,r],a_i=a_i+k$。 > 2. 给出一段区间 $[l,r]$,令 $\forall i\in[l,r],b_i=b_i+k$。 > 3. $\forall i \in [1,n],s_i=s_i+a_i\times b_i$。 现在多了一维,我们仍然考虑信息 $D$,发现我们要想维护 $s_i$ 就得维护 $a_i\times b_i$,于是可以得出一个五元组 $(a,b,sum,s,len)$,其中 $sum=\sum a_i\times b_i$。 $D+D\rightarrow D$ 仍然是很容易的,即 $(a,b,sum,s,len)+(a',b',sum',s',len')=(a+a',b+b',sum+sum',s+s',len+len')$。 我们继而考虑 $D\times T \rightarrow D$,求出这两个操作对信息的影响: 1. 令函数 $f$ 代表区间加序列 $a$ 操作,则有 $f((a,b,sum,s,len))=(a+len\times x,b,sum+b\times x,s,len)$,其中 $x$ 是我们要加减的数。 、 2. 令函数 $g$ 代表区间加序列 $b$ 操作,则有 $g((a,b,sum,s,len))=(a,b+len\times y,sum+a\times y,s,len)$,其中 $y$ 是我们要加减的数。 3. 令函数 $h$ 代表区间更新操作,则有 $h((a,b,sum,s,len))=(a,b,sum,s+sum\times z,len)$,其中 $z$ 是更新次数。 我们把它们复合起来,得到 $F(x) = h(g(f(x)))$,也就得到了 $F((a,b,sum,s,len))=(a+len\times x,b+len\times y,sum+a\times y+b\times x,s+zx\times b+zy\times a+len\times zxy,len)$。 我们发现这个式子已经变得很笨重了,而接下来还要处理 $T\times T\rightarrow T$。如果我们还想像一维那样代入合并同类项的话,计算量不可谓不大。但事实上,我们可以用矩阵来减少思维量。 让我们把 $D\times T\rightarrow D$ 改写成矩阵的形式看看吧! 易得: $$\begin{bmatrix}a & b & sum & s& len\\0&0&0&0&0\\0&0&0&0&0\\0&0&0&0&0\\0&0&0&0&0\end{bmatrix}\times\begin{bmatrix}1&0&y&zy&0\\0&1&x&zx&0\\0&0&1&z&0\\0&0&0&1&0\\x&y&xy&zxy&1\end{bmatrix}$$ 等号右边的就不写出来了,相信读者们都会计算矩阵乘法。 这样一来 $D$ 和 $T$ 都变成矩阵了,计算 $T\times T\rightarrow T$ 就只要乘以两个矩阵就好了! 让我们动手试一试吧!最终的出来的结果应该是: $$\begin{bmatrix} 1&0&y+y'&zy+z'y+(zy)'&0\\0&1&x+x'&zx+z'x+(zx)'&0\\0&0&1&z+z'&0\\0&0&0&1&0&\\ x+x'&y+y'&(x+x')(y+y')&zxy+x(zy)'+(zx)'y+z'xy+(zxy)'&1\end{bmatrix}$$ 虽然它看起来就很吓人,但是这相比起那个合并同类项的做法来说还是略显友好的。 我们想想看能从中得到哪些有用的信息: 1. 首先我们就可以发现,那些原本是 $0$ 或 $1$ 的位置,相乘后保持不变,显然这就是我们要挤出去的水分。 2. 其次我们还可以发现,在整个矩阵中,本质不相同的量其实也就六个。我们把它们提取出来,可以构成一个六元组 $(x,y,z,zx,zy,zxy)$,这就是我们之前得出来的标记 $T$。 注意到如果我们直接从 $F$ 中提取 $T$ 的话,会多得到一个 $xy$,但在我们的矩阵计算下,这个量并不是本质不同的。 3. 最后我们还可以发现,上面那个见鬼的矩阵里头,其实就包含了我们 $T\times T\rightarrow T$ 的转移形式,即 $(x,y,z,zx,zy,zxy)\times (x',y',z',(zx)',(zy)',(zxy)')=(x+x',y+y',z+z',zx+z'x+(zx)',zy+z'y+(zy)',zxy+x(zy)'+(zx)'y+z'xy+(zxy)')$。 于是我们成功通过矩阵来简化了思维量,同时也排除了冗余量。 我们需要的线段树也就构造出来了。 在具体的代码实现中,我的变量名为 $(sa,sb,sab,\_sab,len)$ 和 $(taga,tagb,upd,cnta,cntb,cntab)$,下面统一给出代码: ```cpp node operator +(const node &x,const node &y){ node z={0,0,0,0,0}; z.sa=x.sa+y.sa; z.sb=x.sb+y.sb; z.sab=x.sab+y.sab; z._sab=x._sab+y._sab; z.len=x.len+y.len; return z; } node operator *(const node &x,const tag &y){ node z={0,0,0,0,0}; z._sab=x._sab+x.sab*y.upd+x.sa*y.cntb+x.sb*y.cnta+x.len*y.cntab; z.sab=x.sab+x.sa*y.tagb+x.sb*y.taga+x.len*y.taga*y.tagb; z.sa=x.sa+x.len*y.taga; z.sb=x.sb+x.len*y.tagb; z.len=x.len; return z; } tag operator *(const tag &x,const tag &y){ tag z={0,0,0,0,0,0}; z.cntab=x.cntab+x.taga*x.tagb*y.upd+x.taga*y.cntb+x.tagb*y.cnta+y.cntab; z.cnta=x.cnta+x.taga*y.upd+y.cnta; z.cntb=x.cntb+x.tagb*y.upd+y.cntb; z.upd=x.upd+y.upd; z.taga=x.taga+y.taga; z.tagb=x.tagb+y.tagb; return z; } ``` ## 4.4. 区间(历史)最值问题 没想好怎么写,先咕咕了。 # 5. 参考资料 (**特别感谢**,本文部分出自此文)铃悬 & _rqy ——《[线段树维护分治信息略解](https://www.luogu.com/article/rqmfqvmu)》 Meatherm ——《[数据结构闲谈:范围分治的「双半群」模型](https://www.cnblogs.com/Meatherm/p/17925813.html)》 FutaRimeWoawaSete ——《[题解 P8868](https://www.luogu.com.cn/article/ywg78bto)》 yukimianyan——《[题解 LGP8868【[NOIP2022] 比赛】/【模板】“历史版本和”线段树](https://www.cnblogs.com/caijianhong/p/solution-p8868.html)》