猫树学习笔记

本文参考自算法发明者 immortalCO(猫锟) 的博客 一种高效处理无修改区间或树上询问的数据结构(附代码)

感谢 猫锟 提供了对于一类题比较通用的解决办法,以及思路启发。

问题描述

给出一个某种元素的序列 a1,a2,,an,要求进行 m 次询问,每一次是询问一段区间 [l,r] 的某种支持结合律和快速合并的信息,要求在线。

这类问题比较通用,比如在 DP 的优化中就常常见到。

算法实现

算法介绍

对于常规问题,比如区间最值,区间最大子段和。我们常常能用线段树等数据结构达到,构造 O(n) ,询问 O(logn) 的时间复杂度。

对于这些做法,只有一点不好,询问复杂度 不够优秀,且对于一些特定问题,线段树的 push_up 合并也不好写。

但对于区间最值这类的问题, 我们可以类似 RMQ 那样,在一般的问题上,以预处理的时间和空间,换取快速的询问

我们首先考虑询问一个区间 [ql,qr] 。如果 ql=qr ,就可以直接得到答案。否则会不断在线段树上定位,而且会在几个区间的中点 mid 处被分开。

我习惯于线段树每个区间维护的一个闭区间 [l,r] ,其中中点 mid=l+r2

考虑第一次被分开的位置,假设为 p 。那么原来的区间 [ql,qr] 就被分为 [ql,mid] ,与 [mid+1,qr]

我们考虑对于每一个 mid 预处理他向前的后缀 [i,mid] 的答案(其中 limid) ,以及他向后的前缀 [mid+1,j] (其中 mid+1jr)。

如果我们知道了 p 点所在的位置,我们可以直接利用之前预处理的 [ql,mid] 以及 [mid+1,qr] 的答案直接合并即可。

不难发现预处理的复杂度是 O(nlogn) 的(对于每一层每个数刚好被考虑一次 O(n)×O(logn)=O(nlogn)


然后怎么快速知道这个位置呢?

不难发现这个 p 的位置,就是线段树上对应 [l,l][r,r] 节点的 lca (最近公共祖先)所处的位置,我们可以考虑用 st 表预处理,然后可以直接查询 p 的位置,但这样显然太麻烦了。

如果整棵线段树满足堆式存储(也就是对于点 i ,它的两个儿子分别为 2i2i+1 ),就有一个很好的性质。

对于任意两个同深度的点,他们的 lca 是他们二进制下 lcp (最长公共前缀)。

这个是十分显然的,因为对于两个深度相同的点,他们第一次分开的位置,必然导致当前最后一位的二进制不同,而前面都是相同的。

我们把整棵树建成一个满二叉树 [1,2k],那么对于任意一个区间 [i,i] 都是满足他们的深度是最深且在同一层的。

注意对于不同深度的点不一定满足这个情况!!这就是为什么我们为什么要建满的原因。

然后对于两个数 x,y 的二进制下的 lcpx >> Log2[x ^ y] 。(这个很显然,丢掉第一个不相同的位后面的所有位就行了)

这样我们就可以实现询问 O(1) 啦。

我们称这个数据结构为 猫树

算法本质

看了一下 UOJ 评论区。。。

其实就是将分治进行离线,我们用一个东西来存储这个分治结构,以及前面按位置分治的答案。

所以这个算法最重要的还是,寻找特定问题的分治方案。

例题讲解

区间最大子段和

题意

给你一个序列 ai ,有 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; }

__EOF__

本文作者zjp_shadow
本文链接https://www.cnblogs.com/zjp-shadow/p/9377742.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zjp_shadow  阅读(3026)  评论(3编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示