猫树学习笔记
本文参考自算法发明者 immortalCO(猫锟) 的博客 一种高效处理无修改区间或树上询问的数据结构(附代码) 。
感谢 猫锟 提供了对于一类题比较通用的解决办法,以及思路启发。
问题描述
给出一个某种元素的序列 \(a_1,a_2,\dots ,a_n\),要求进行 \(m\) 次询问,每一次是询问一段区间 \([l,r]\) 的某种支持结合律和快速合并的信息,要求在线。
这类问题比较通用,比如在 DP 的优化中就常常见到。
算法实现
算法介绍
对于常规问题,比如区间最值,区间最大子段和。我们常常能用线段树等数据结构达到,构造 \(O(n)\) ,询问 \(O(\log n)\) 的时间复杂度。
对于这些做法,只有一点不好,询问复杂度 不够优秀,且对于一些特定问题,线段树的 push_up
合并也不好写。
但对于区间最值这类的问题, 我们可以类似 \(RMQ\) 那样,在一般的问题上,以预处理的时间和空间,换取快速的询问。
我们首先考虑询问一个区间 \([q_l, q_r]\) 。如果 \(q_l = q_r\) ,就可以直接得到答案。否则会不断在线段树上定位,而且会在几个区间的中点 \(mid\) 处被分开。
我习惯于线段树每个区间维护的一个闭区间 \([l, r]\) ,其中中点 \(\displaystyle mid = \lfloor \frac{l + r}{2} \rfloor\) 。
考虑第一次被分开的位置,假设为 \(p\) 。那么原来的区间 \([q_l, q_r]\) 就被分为 \([q_l, mid]\) ,与 \([mid + 1, q_r]\) 。
我们考虑对于每一个 \(mid\) 预处理他向前的后缀 \([i, mid]\) 的答案(其中 \(l \le i \le mid\)) ,以及他向后的前缀 \([mid + 1, j]\) (其中 \(mid + 1 \le j \le r\))。
如果我们知道了 \(p\) 点所在的位置,我们可以直接利用之前预处理的 \([q_l, mid]\) 以及 \([mid + 1, q_r]\) 的答案直接合并即可。
不难发现预处理的复杂度是 \(O(n \log n)\) 的(对于每一层每个数刚好被考虑一次 \(O(n) \times O(\log n) = O(n \log n)\))
然后怎么快速知道这个位置呢?
不难发现这个 \(p\) 的位置,就是线段树上对应 \([l, l]\) 和 \([r, r]\) 节点的 \(lca\) (最近公共祖先)所处的位置,我们可以考虑用 \(st\) 表预处理,然后可以直接查询 \(p\) 的位置,但这样显然太麻烦了。
如果整棵线段树满足堆式存储(也就是对于点 \(i\) ,它的两个儿子分别为 \(2 i\) 和 \(2i + 1\) ),就有一个很好的性质。
对于任意两个同深度的点,他们的 \(lca\) 是他们二进制下 \(lcp\) (最长公共前缀)。
这个是十分显然的,因为对于两个深度相同的点,他们第一次分开的位置,必然导致当前最后一位的二进制不同,而前面都是相同的。
我们把整棵树建成一个满二叉树 \([1, 2^k]\),那么对于任意一个区间 \([i, i]\) 都是满足他们的深度是最深且在同一层的。
注意对于不同深度的点不一定满足这个情况!!这就是为什么我们为什么要建满的原因。
然后对于两个数 \(x, y\) 的二进制下的 \(lcp\) 为 x >> Log2[x ^ y]
。(这个很显然,丢掉第一个不相同的位后面的所有位就行了)
这样我们就可以实现询问 \(O(1)\) 啦。
我们称这个数据结构为 猫树 。
算法本质
看了一下 UOJ 评论区。。。
其实就是将分治进行离线,我们用一个东西来存储这个分治结构,以及前面按位置分治的答案。
所以这个算法最重要的还是,寻找特定问题的分治方案。
例题讲解
区间最大子段和
题意
给你一个序列 \(a_i\) ,有 \(m\) 次询问,每次询问一个区间 \([l, r]\) ,表示询问这段区间的最大子段和。
题解
如果没有区间,那么这个是个经典的分治问题。
最大子段和,要么完全在左区间,要么完全在右区间,要么跨越中点。
所以我们只需要预处理 \([i, mid]\) 的最大前缀和与 \([mid + 1, j]\) 的最大前缀和,这个一边遍历一边取 \(\max\) 。
以及这两个区间的最大子段和。至于那个最大子段和,可以利用前缀和相减,保存一个前缀和最小值就行了。
这个算法比标准线段树上合并信息,要好写并且更快。
代码
对于第一道题,还是建议看看代码怎么写的。。(瓶颈在输入输出上也是没谁啦)
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
using namespace std;
inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
inline int read() {
int x = 0, fh = 1; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * fh;
}
inline void Out(int x) {
static char sta[18], top, flag = false;
if (!x) { puts("0"); return ; }
sta[top = 1] = '\n';
if (x < 0) flag = true, x = -x;
for (; x; x /= 10) sta[++ top] = (x % 10) + 48;
if (flag) putchar ('-'), flag = false;
while (top) putchar (sta[top --]);
}
void File() {
#ifdef zjp_shadow
freopen ("1043.in", "r", stdin);
freopen ("1043.out", "w", stdout);
#endif
}
const int N = 50100, Maxn = N * 4, MaxLog = 20, inf = 0x7f7f7f7f;
int pos[Maxn], val[Maxn], Log2[Maxn], maxlen;
namespace CatTree {
inline int Max(int a, int b) { return a > b ? a : b; }
int Pre[MaxLog][Maxn], Sum[MaxLog][Maxn];
void Build(int o, int l, int r, int dep) {
if (l == r) { pos[l] = o; return ; }
int mid = (l + r) >> 1, sum, minv;
Sum[dep][mid] = Pre[dep][mid] = sum = minv = val[mid]; chkmin(minv, 0);
Fordown(i, mid - 1, l) {
Pre[dep][i] = Max(Pre[dep][i + 1], sum += val[i]);
Sum[dep][i] = Max(Sum[dep][i + 1], sum - minv);
chkmin(minv, sum);
}
Sum[dep][mid + 1] = Pre[dep][mid + 1] = sum = minv = val[mid + 1]; chkmin(minv, 0);
For (i, mid + 2, r) {
Pre[dep][i] = Max(Pre[dep][i - 1], sum += val[i]);
Sum[dep][i] = Max(Sum[dep][i - 1], sum - minv);
chkmin(minv, sum);
}
Build(o << 1, l, mid, dep + 1);
Build(o << 1 | 1, mid + 1, r, dep + 1);
}
inline int Query(int l, int r) {
if (l == r) return val[l];
register int dep = Log2[pos[l]] - Log2[pos[l] ^ pos[r]];
return Max(Max(Sum[dep][l], Sum[dep][r]), Pre[dep][l] + Pre[dep][r]);
}
}
int n;
int main() {
File();
n = read();
For (i, 1, n) val[i] = read();
for (maxlen = 1; maxlen < n; maxlen <<= 1);
CatTree :: Build(1, 1, maxlen, 1);
For (i, 2, maxlen << 1) Log2[i] = Log2[i >> 1] + 1;
for (register int m = read(), l, r; m; -- m) {
l = read(), r = read();
Out(CatTree :: Query(l, r));
}
#ifdef zjp_shadow
cerr << (double) clock() / CLOCKS_PER_SEC << endl;
#endif
return 0;
}