所谓维护区间,也就是对一个区间进行一些操作和查询,往往需要利用一些数据结构完成,本文将分析各种维护区间的方法的优劣。
朴素/暴力
一般来说,这种方法比较好些,不易出 BUG 。但是时间复杂度奇高,部分分应该能拿到,如果数据水甚至可以拿到 (70%) 的分数(如洛谷的树状数组模板1)。
前缀和/差分
前缀和就是可以 (mathcal{O}(1)) 得出区间 ([l,r]) 的某个信息,差分可以 (mathcal{O}(1)) 修改一段区间 ([l,r]) 的值 。
前缀和和差分最显著的优势就是写起来很直观,好理解,比暴力还好写(这是真的,暴力往往需要循环修改/查询,而前缀和/差分就避免了这些问题,只需要维护一个前缀和/差分数组就可以了),并且单种操作比暴力要快很多。
然而前缀和与差分最让后人诟病的地方就是它们只能进行一种区间操作。假如您希望同时进行区间修改和区间查询那么无论是用前缀和还是差分都只能 (mathcal{O}(1)) 进行一种操作,另一项操作无论如何都是需要花费区间长度的时间代价,那么最后时间复杂度还是降不下来。
但是如果您懒得写 线段树/树状数组/ST表 且题目中不涉及两种操作,那么前缀和/差分就已经绰绰有余了。
ST表
ST表广泛应用于著名的 RMQ(区间最值问题)中。
没学,天坑待填(滑稽保命)
树状数组
如果需要同时进行区间修改和查询,那么树状数组是一个较好的选择。树状数组的核心思想就是利用 lowbit 运算修改和查询。
树状数组最大的好处就是弥补了前缀和与差分的不足之处,并且在时间和空间上都吊打线段树。
但是树状数组真的不好理解。
我很不喜欢树状数组,很大一个原因就是这个数据结构过于不直观,我自己也没什么兴趣研究二进制这种奇怪的技术233,其实只要记住两段代码即可。其中给 (a_x) 加上一个值就是让当前位置 (i) 不断加 lowbit(x) ,然后查询 (x) 的前缀和就是不断减去 lowbit(x) 。
void add(int x,int y)
{
for(register int i=x;i<=n;i+=l(i))t[i]+=y;
}
//等价于 a[x]+=y
int sum(int x)
{
int ans=0;
for(register int i=x;i;i-=l(i))ans+=t[i];
return ans;
}
//得到第x个数的前缀和
反正这种用二进制的奇技淫巧我大都不太喜欢。。。
而且树状数组极其不易拓展,所以如果题目不去刻意卡常那就尽量写线段树(当然,如果您是像yjx那样的树状数组神仙当我没说。。)
线段树
大爱线段树!(lxl不要打我,我不会做Ynoi。。。)
线段树清晰易懂,时间飞快,卡常能过,代码简介,乃区间维护之霸者也。 By lxl
然后线段树的可拓展性非常强,区间修改、单点修改、区间查询、单点查询都能干,并且拓展到二维后功能更加强大。
但是线段树常数真的很大,得开 4 倍空间,基本稍微毒瘤一点的出题人出关于维护区间之类的题目都会去卡一卡线段树。
但是,,,但是,,,但是还是有解决方法的。比如加个快读什么的。。。或者关闭 io 流同步什么的。。。我也不是很会,但是据我所知快读+O2一般都能卡过去。
我有一篇文章就是专门放线段树模板的,这些题卡一卡用线段树总能过。。。→这里
所以一般维护区间的题目建议写线段树,一般写线段树最好加快读。
分块
基本思想就是将一个序列分成几块,查询/更改的时候秉持“大局维护,局部朴素”的原则,是一种优雅的暴力。
我不会。天坑待填。
大概也就这么多罢。