一类经典问题的若干解法

标题指的是这类问题:

我们经常会看见求 \(\sum\limits_{x=l}^r\sum\limits_{y=x}^r f(x,y)\) 这类问题。我们常常能够通过智慧将 \(f(x,y)\) 转化为二维平面上的点,然后发现所有 \(f\) 可以用一些矩形加来表示。通常这里面矩形加的次数是 \(\mathcal O(n)\) 或者 \(\mathcal O(n\log n)\) 等低复杂度的。那么得到这些矩形后应该怎么做呢?

方法 \(\mathit1\) - 线段树历史版本和

将其中一维扫描线,将修改和查询都拆成两部分,然后就是线段树历史版本和了。

时间复杂度 \(\mathcal O((m+q)\log n)\),其中 \(n\) 是原序列大小,\(m\) 是修改次数,\(q\) 是查询次数。

方法 \(\mathit2\) - 树状数组

可以将修改拆成 \(4\) 个后缀矩形加,将查询拆成 \(4\) 个前缀矩形查,那么发现这样一来有贡献的修改操作就是端点再前缀矩形内的。那么还是一维扫描线,另一维用树状数组维护。

具体地,一个修改操作 \((x,y,v)\) 对一个查询操作 \((a,b)\) 的贡献是:

\[v(a-x+1)(b-y+1)={\color{red}v}(a+1)(b+1)-{\color{red}vx}(b+1)-{\color{red}vy}(a+1)+{\color{red}vxy} \]

将四个红色部分分别维护就行了。时间复杂度仍是 \(\mathcal O((m+q)\log n)\) 的。


以上两种方法复杂度相同,但是哪个的常数更优秀呢?实践证明,树状数组虽然要拆成 \(4\) 个操作,每个操作又要维护 \(4\) 个部分,保底就有 \(16\) 的常数了,但是树状数组常数本身较小,而线段树涉及标记下传等,还有递归操作,所以实际上树状数组的常数是要略胜一筹的。


方法 \(\mathit3\) - 二维线段树

时间复杂度是 \(\mathcal O((m+q)\log^2n)\),因为是在线的(可以一边矩形加,一边查询),所以在这里是大炮打蚊子,关键效率极其低。


以上的方法中还是更推荐树状数组,因为其运行效率更高,并且好写,并且不用更高级的知识,初学者就能写。要说唯一的不足可能就是代码比较抽象,难调试(虽然基本上不怎么会写错)。

posted @ 2024-02-17 10:53  TulipeNoire  阅读(11)  评论(1编辑  收藏  举报