猫树
基本信息
这是一个比较良心的数据结构,代码属于比较易懂也好打。只是有可能目前的应用范围有点小。
猫树呢,大概是一个能 \(O(n\log n)\) 建树, \(O(1)\) 查询,不太能修改(你非要的话就 \(O(n)\) ),维护信息需要满足结合律且能在可接受复杂度合并。
算法实现
建树
以查询出发,我们假设我们答案要的区间是 \([{\rm l},\ {\rm r}]\) ,那么假设我们已经能知道 \([{\rm l},\ {\rm mid}]\) 和 \([{\rm mid} + 1,\ {\rm r}]\) 的答案,那是不是直接合并就行了。
我们想象线段树建树是 \([{\rm L},\ {\rm R}]\) 递归到 \([{\rm L},\ {\rm Mid}]\) 和 \([{\rm Mid} + 1,\ {\rm R}]\) ,以左边这个小区间为例,如果我们直接处理 \([{\rm L}-{\rm Mid},\ {\rm Mid}]\) 的答案,因为最多递归 \(\log n\) 次,整个时间复杂度就是 \(O(n \log n)\) ,还是比较能够接受的。
然后我们发现假如一个答案区间 \([{\rm l}, {\rm r}]\) 的 \({\rm l}\) 和 \({\rm r}\) 在递归的同一小区间,就并不是我们处理时能取到的答案,直到最后递归到了一个区间 \({\rm l}\) 和 \({\rm r}\) 被分开了,我们就能找到一个 \({\rm mid}\) 能合并两边的答案,从而我们就达到了目的。
然后我们思考怎么存答案,对于每一层,不同的区间所在范围互不交叉,所以我们以层数为第一维,具体的哪一个位置为第二维。
查询
但是没完,我们要求的是 \(O(1)\) 查询,但是我们好像不知道怎么一下子定位在哪个区间能合并。
能发现两个点作为两个叶子节点,他们的 \({\rm lca}\) 就是答案所在的区间,因为此时两个点所在的区间被合并成一个大区间了,正好就是有一个 \({\rm mid}\) 大于 \({\rm l}\) 且小于 \({\rm r}\) 。
那是不是什么 ST 表就可以了??其实还可以更简单!!
好像一些性质没用上,我们来想假如两个值在一个区间被分开了,那他们会有什么联系。
好像没有什么性质吧,因为边界是 \([1,\ n]\) ,那就改成 \([1,\ 2 ^ m]\) ,其中是 \(2 ^ m\) 大于 \(n\) 中最小的数。本质上时间复杂度不会变,但是加入两个数被分开,假如目前是第 \(k\) 层,那就是第 \(k\) 高位不一样。
这就相当于两个异或之后前 \(k - 1\) 位全部消失,那我们只要预处理二进制位数,我们就能很快得到 \(k\) 的值,因为答案存的方式正好需要 \(k\) ,正好就可以马上算出答案了,也就是真正意义上的 \(O(1)\) 了。
修改
修改你大爷,以单点修改为例,影响的地方从第一层一直到自己所在的叶子,所在区间大小和为:
\[\sum_{i = 1} ^ {m} \frac{n}{2 ^ i} = n \]搞屁,修改都是 \(O(n)\) 的,那就不修改(当然什么修改次数很小倒是可以考虑考虑)
猫树分治
前景提要:某些卡空间但是没有要求强制在线的题
其实处理方法很简单,就有点归并排序的意思,每次递归两个小区间,就扫一遍询问,把答案能算的算了,不能算了分成两拨继续递归就好啦。
这样的话时间复杂度不变,空间复杂度从原先 \(O(n \log n)\) 变成 \(O(n)\) 了。
代码
以例题中的 GSS1 为例。
/*
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 4e5 + 10;
int n, a[N], len = 1, lg[N], m;
inline int read() {
char ch = getchar();
int s = 0, w = 1;
while (!isdigit(ch)) {if (ch == '-') w = -1; ch = getchar();}
while (isdigit(ch)) {s = (s << 3) + (s << 1) + (ch ^ 48); ch = getchar();}
return s * w;
}
struct immortalCO {
#define lson (now << 1), l, mid
#define rson (now << 1 | 1), mid + 1, r
int pos[N], p[21][N], s[21][N];
inline void build(int now, int l, int r, int d) {
if (l == r) return pos[l] = now, void();
int mid = (l + r) >> 1, all, mx;
p[d][mid] = s[d][mid] = all = mx = a[mid];
mx = max(mx, 0);
for (int i = mid - 1; i >= l; --i) {
all += a[i]; mx += a[i];
p[d][i] = max(p[d][i + 1], mx);
s[d][i] = max(s[d][i + 1], all);
mx = max(mx, 0);
}
p[d][mid + 1] = s[d][mid + 1] = all = mx = a[mid + 1];
mx = max(mx, 0);
for (int i = mid + 2; i <= r; ++i) {
all += a[i]; mx += a[i];
p[d][i] = max(p[d][i - 1], mx);
s[d][i] = max(s[d][i - 1], all);
mx = max(mx, 0);
}
build(lson, d + 1); build(rson, d + 1);
}
inline int query(int l, int r) {
if (l == r) return a[l];
int d = lg[pos[l]] - lg[pos[l] ^ pos[r]];
return max(max(p[d][l], p[d][r]), s[d][l] + s[d][r]);
}
#undef lson
#undef rson
} ct;
int main() {
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
for (; len < n; len <<= 1);
for (int i = 2; i <= (len << 2); ++i) lg[i] = lg[i >> 1] + 1;
ct.build(1, 1, len, 1);
m = read();
while (m--) printf("%d\n", ct.query(read(), read()));
return 0;
}
例题
GSS1(最大子段和)
GSS5(区间端点带限制最大子段和)
好吃的题目(猫树分治)
[CmdOI2019]口头禅(猫树分治 + SAM )
鸣谢: