数据结构闲谈:范围分治的「双半群」模型

从「双半群」出发看范围分治问题#

对于区间修改 + 区间查询的线段树(此处以其为例)问题,使用懒标记维护时,记信息 (Data) 集合为 D,标记 (Tag) 集合为 T,需要满足如下性质:

  • 支持通过合并得出区间信息,即存在运算 +:D×DD,其中 × 为笛卡尔积。
  • 标记可以作用于信息,作用之后信息仍然是信息,即存在运算 apply:T×DD。对于 apply(t,d),下文简记为 td
  • 标记可以复合,复合之后标记仍然是标记,即存在运算 :T×TT。对于 t2t1,下文简记为 t2t1
  • 当节点上没有标记时,事实上存储了一个代表「什么都不做」的空标记,即要求 (T,) 存在单位元。

同时,我们注意到:

  • 我们要支持以任意顺序合并得出区间信息,即要求 D+ 满足结合律,且信息合并之后仍然是信息,因此 (D,+) 需要构成半群。对于两个条件,后者和前者是等价的。但是,区间信息的合并不仅仅是一个冷冰冰的运算,它是有实际意义的,只要我们确信我们的运算确实将两个区间的信息合并到了一起,+ 就具有结合律。

  • 要求标记可以复合、且标记对复合封闭,同时 (T,) 存在单位元。因为复合一定是满足结合律的,因此这些条件使得 (T,) 需要构成幺半群。后者比前者更弱(映射的复合一定满足结合律),因此我们设计标记时,直接考虑如何使标记能够复合即可。

  • 先合并区间信息,再总体作用标记(区间修改),与先作用标记,再合并区间信息(标记下传时),得到的信息应该等价,因此 t(d1+d2)=td1+td2,因此要求 +分配律

信息和合并 (D,+) 构成半群,标记和复合 (T,) 构成幺半群,+ 有分配律,且标记可以作用于信息上,此即为可以使用线段树维护的信息要求,称之为「双半群」模型。

经过上面的观察,为了构成「双半群」模型,有两点十分重要:

  • 标记对复合封闭,且存在单位元。
  • 信息可以合并。

设计标记和信息时,大多只需要考虑这两点,其它条件便自然满足。更具体地,我们设计时,遵从以下步骤进行考虑:

  1. 如何设计信息才能使其构成半群,并能够计算出答案?
  2. 为了使标记能够作用在信息上,标记需要包含些什么?
  3. 如何使标记能够复合,并对复合封闭?如果需要添加信息,需要添加哪些信息才能够使原来的信息被正确维护?
  4. 不断迭代 2,3,直到不需要添加任何信息为止。

举一个非常极端的例子:区间加法,区间颜色数。我们只想知道区间颜色数,但如果我们的信息仅仅是区间颜色数,它首先是不能合并的,并且标记也无法作用在信息上,于是我们必须添加更多信息(事实上只能维护一个 bitset 存储每个颜色是否出现),然后继续考虑标记如何作用在添加的信息上。很幸运,这一次不需要添加任何信息了,于是迭代结束。

实际问题中的标记与信息设计#

大部分情况下,只要找到了一种描述标记的好方式使得它可以复合,那么它大概率也是可以作用在信息上的。而要找到描述标记的方式,又要先从信息入手,即为了维护出这些信息,需要如何描述标记。

例 1

维护数列 a1,a2,,an,支持:

  • 区间加法
  • 区间乘法
  • 询问区间和

我们手算了一些任意标记的复合,发现它们都可以写作:区间中的每一个数 x 都变为 ax+b。为了正确维护区间和,我们还需要添加区间长度的信息,而这次我们发现区间长度不受标记影响,于是迭代结束了。

  • 描述标记:使用二元组 (a,b) 表示,区间中的每一个数 x 都变为 ax+b
  • 信息:记录区间长度,区间和

例 2 (历史版本和线段树)

维护数列 a1,a2,,an,支持:

  • 区间加法,随后生成一个新的历史版本
  • 询问区间和
  • 询问区间历史版本和

维护区间和的部分是简单的,此处不再赘述。

标记作用于信息时,考虑:一个位置被取历史版本和时,贡献为 ai+tagi,因此该标记的贡献被分为两个部分:一是原来的区间和对历史版本和的贡献,二是标记对历史版本和的贡献(即标记的历史版本和),前者即为原来的区间和与标记中取历史版本和次数的乘积,我们只需要重点考虑后者。

我们把任意标记的复合看作一个标记队列 q,其中的标记要么是一次区间加法,要么是一次取历史版本和。考虑两个标记队列 q1,q2,其中各有一些区间加法操作和一些取历史和操作,现在我们要把 q2 接在 q1 之后形成一个新的标记队列 q3=q1+q2

我们发现复合后,q2 中每有一个取历史版本和标记,q1 中的每个加法标记都会对历史版本和贡献标记本身的值。于是,我们还要额外维护加法标记的和,以及取历史版本和的次数。这些标记可以直接复合,不需要添加额外信息,于是迭代结束了。

  • 描述标记:记录 tagh,表示区间中每个位置的历史版本和加上 tagh;记录 tag,表示加法标记和;记录 cnt,表示取历史版本次数
  • 信息:记录区间长度,区间历史版本和

例 3 (改编自 [NOIP2022] 比赛)

维护两个数列 a1,a2,,anb1,b2,,bn,支持:

  • a 区间加法
  • b 区间加法
  • 标记一个新的历史版本
  • 区间 [l,r]aibi 的历史版本和

不太需要直觉就可以想到先维护 a,b,ab 的区间和,这三者是简单的。

  • 描述标记:使用二元组 (x,y) 表示,表示区间中每个 ai 都加上 x,区间中每个 bi 都加上 y
  • 信息:记录区间长度,区间 a,b,ab 区间和

标记作用于信息的时候,需要考虑四个部分。注意到一个位置 i 被取历史版本和时,它的贡献是 (ai+tagai)(bi+tagbi),其中 tagai,tagbi 表示位置 i 当前的 a,b 标记。 拆开之后,我们需要维护的有:取历史版本和的次数 cnttagai 的历史和 htagatagbi 的历史和 htagb,以及 tagaitagbi 的历史和 htagab

接下来考虑标记队列,其中的标记仅有对 a 的加法,对 b 的加法,以及取历史版本和。我们发现复合后,q1 中的标记有如下的额外贡献:

  • 每个对 a 的加法标记对于 htaga 有额外贡献,贡献次数为 cnt(q2)
  • 每个对 b 的加法标记同理
  • q1 中的 tagatagb 可以对 htagab 产生额外贡献,贡献次数为 cnt(q2)
  • q1 中对 a 的加法标记和 q2 中对 b 的加法标记可以对 htagab 产生额外贡献,总贡献为二者的在各自队列中的历史和;q1 中对 b 的加法标记同理

详见代码:

inline void applytag(int k,int l,int r,ull tagA,ull tagB,ull htagA,ull htagB,ull htagAB,ull cnt){
	tr[k].hsumAB+=cnt*tr[k].sumAB+tr[k].sumA*htagB+tr[k].sumB*htagA+htagAB*(ull)(r-l+1); // 标记作用于信息
	tr[k].htagA+=tr[k].tagA*cnt+htagA;
	tr[k].htagB+=tr[k].tagB*cnt+htagB;
	tr[k].htagAB+=tr[k].tagA*tr[k].tagB*cnt+tr[k].tagB*htagA+tr[k].tagA*htagB+htagAB;
    // 标记的复合
	tr[k].sumAB+=tr[k].sumB*tagA,tr[k].sumA+=tagA*(ull)(r-l+1),
	tr[k].sumAB+=tr[k].sumA*tagB,tr[k].sumB+=tagB*(ull)(r-l+1);
	tr[k].tagA+=tagA,tr[k].tagB+=tagB;
	tr[k].cnt+=cnt;
    // sumA,sumB,sumAB 的维护,这是简单的
	return; 
}

例 4

给定数列 a1,a2,,an,支持:

  • 区间加法
  • 区间赋值
  • 询问区间最大值
  • 询问区间在所有历史版本中的最大值

标记作用于信息时,需要考虑:最值为之前产生 / 最值为标记作用后产生。如果最值为标记作用之后产生,则需要记录标记的历史最值。

我们发现,一次区间赋值之后,后面的任何操作都可以看作区间赋值,于是任何标记队列都可以被看作若干次区间加法 + 若干次区间赋值。

两个标记的复合亦是容易的。分类讨论 q1 中是否存在区间赋值操作即可。

例 5

Luogu P6242 【模板】线段树 3

题面略。

历史操作也可以结合区间最值操作,即:我们分别考虑对于区间最值的标记,以及非区间最值的标记。只要我们维护好这两套标记,就和普通历史操作线段树无异,复杂度和 Segment Tree Beats 的复杂度相同,为 O((n+m)logn)O((n+m)log2n)

// mxtag: 对区间最值的加法标记
// mxhtag: 对区间最值的历史最大加法标记
// tag / htag: 同理,对非区间最值的标记

inline void applytag(int k,int l,int r,int mxtag,int mxhtag,int tag,int htag){
	tr[k].sum+=1ll*tr[k].cnt*mxtag+1ll*(r-l+1-tr[k].cnt)*tag;
	tr[k].hmax=std::max(tr[k].hmax,tr[k].mx+mxhtag);
	tr[k].mx+=mxtag;
	if(tr[k].cmx!=-INF) tr[k].cmx+=tag; // 特判区间全部为 1 种数的情况
	tr[k].mxhtag=std::max(tr[k].mxhtag,tr[k].mxtag+mxhtag),tr[k].mxtag+=mxtag;
	tr[k].htag=std::max(tr[k].htag,tr[k].tag+htag),tr[k].tag+=tag;
	return;
}

inline void psdown(int k,int l,int r){
	int mid=(l+r)>>1,maxval=std::max(tr[lc(k)].mx,tr[rc(k)].mx);
	
	if(tr[lc(k)].mx==maxval) // 若左区间最值不为区间最值,则需要按照非区间最值的标记处理
        applytag(lc(k),l,mid,tr[k].mxtag,tr[k].mxhtag,tr[k].tag,tr[k].htag);
	else applytag(lc(k),l,mid,tr[k].tag,tr[k].htag,tr[k].tag,tr[k].htag);
	
	if(tr[rc(k)].mx==maxval) // 若右区间最值不为区间最值,同理
        applytag(rc(k),mid+1,r,tr[k].mxtag,tr[k].mxhtag,tr[k].tag,tr[k].htag);
	else applytag(rc(k),mid+1,r,tr[k].tag,tr[k].htag,tr[k].tag,tr[k].htag);
	
	tr[k].tag=tr[k].htag=tr[k].mxtag=tr[k].mxhtag=0;
	return;
}

特殊的标记 / 信息形式:矩阵#

在某些题目中,考虑标记如何作用于信息实在是过于复杂,于是我们找到了一种偷懒的方法:使用矩阵来描述信息。我们知道,矩阵的本质是映射。对于列向量 x,我们可以利用映射 Ax=y,通过左乘矩阵 A 的方式来将列向量 x 变为 y

如果所有信息都可以被表示为行数相同的列向量 x,合并信息可以表示为向量加法,一个标记作用在信息上可以用矩阵 A 来描述,那么这种标记 / 信息形式就是一个双半群模型:行数相同的列向量对加法构成交换群,矩阵乘法满足结合律且存在单位矩阵 I,并且矩阵乘法对矩阵加法也是天然满足分配律的。

常见的矩阵有两种。如果涉及区间加法,区间求和,那么最常见的 (+,×) 矩阵绝对是你的不二之选,因为给区间打上加法标记 v 后,区间和的变化量是 len×v,这里出现了乘号。

如果涉及最值,那就应该试试 (min,+) 矩阵,即,我们把原来的一切 + 都改成 min× 改为 +,这样还是会满足我们需要的所有性质。

例题:

  • Luogu P4314 CPU 监控 (广义矩阵乘法)
  • [NOIP2022] 比赛 (普通矩阵乘法)
  • Luogu P7453 [THUSCH2017] 大魔法师 (普通矩阵乘法;同时这也是一个直接考虑标记会过于复杂的例子)

作者:Meatherm

出处:https://www.cnblogs.com/Meatherm/p/17925813.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Meatherm  阅读(634)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示