暂时退役前部分数据结构 / 算法总结
arfa
·
2019-12-17 12:29:11
·
个人记录
2020.6.20 , 中考大劫。
本人即将进行暂时退役 , 把自己学过的数据结构或者有用的算法全部浅谈一遍 , 怕高一的时候又忘记了。
希望对后来人有帮助。
忘记说了 ,
大部分题目都可以在 Index 里面找到。
前面一些简单的我就不讲用处了 , 可以看别的博客。
持续更新。
\text{数组}
数组可以支持 O(1) 查询 , O(1) 修改 , O(n) 插入 , 这是很强大的功能。
它与链表差别较大。不过可以兼容大部分数据结构。
\text{栈}
栈可以做很多事情 , 其实就是模拟一个后进后出的数组。
我们把可以解决一类这种问题的数组叫栈。
一般来说 , 我们维护单调栈 , 题目有的 : 发射站。
\text{队列}
队列也可以做很多事情 , 它是栈意义上的扩张。
也就是前后两个栈 , 可以怎么理解。
不过具体怎么用还是按题目而定 , 如单调队列。
单调队列每次往前删数最差都是 O(n) 的。
不过别忘了这个数列的长度是 n , 所以插入删除均摊还是 O(1) 。
如果用了二分查找在一定程度上降低速度。
有些人会把“单调序列”说成“单调队列” , 如导弹拦截这道题 , 大家自己辨识清楚。
\text{链表}
一个点出度入度都是 1 , 就是链表。
链表的作用很大 , 比如图论中可以省空间。比如可以 O(n) 查询 O(n) 修改 , O(1) 插入。
链式前向星就是运用了链表 , 本人一般都是用链式前向星模拟链表。
如后缀排序一题。
\text{前缀和}
通过记录 1 \dots n 的信息来达到 O(1) 查询 , O(n) 修改的强大结构。
如果是单独拿来考 , 一般会考二维到三维 , 然后容斥一下 ?
为什么要单独来讲这样一个简单的结构 ?
不同于我讲数组 , 讲数组是提及 , 讲前缀和是重要奠基。
\text{差分}
很有趣的东西 , 很喜欢考树上差分。
它可以做到在保持位置的情况下区间修改 O(1) , 查询 O(n) 。
它对 val_i 的定义是 : num_i-num_{i-1} 。
对于区间修改 [l,r] , 就 val_l+k , val_{r+1}+k 即可。
查询每一个的值就是把 val 相加。
考虑树上差分。
其实很简单 , 你把树看成区间 , 关于 \text{lca}\{u,v\} 的区间。
让 val_u 和 val_v 都加上 k , \text{father\{lca}\{u,v\}\text{\}} 和 \text{lca}\{u,v\} 都减去 k 。
为什么要减掉两个 ?
因为 \text{lca}\{u,v\} 只用加一次 , 所以这样子能达到目的。
当然这只是对于点的差分 , 对于边的以及其它可以看 : Danny_boodman 的博客。
\text{并查集}
说一点树的知识先 :
一个点有 1 个入度 k 个出度的图就是 k 叉树。
树有一些有用的东西 , 比如两点之间只有一个最短路径 ,
两点之间的距离可以用 \text u \implies \text{root} 和 \text v \implies \text{root} 弄出来。
有很多关于树的文章 , 后面提及。
很多人喜欢把这个数据结构说成江湖传说。
它其实是一个效率极高的可以判断两点是否联通的一种好用的东西 ,
通常时候认为查询和修改都是 O(1) , 或者说是 O(\alpha(n)) 。
前提是路径压缩 , 也就是菊花操作。
不过可以被卡 , 时间复杂度会被卡到 Ω(m \log _{1+\frac n m}n) , 具体看 Atalod的博客。
关于按秩合并 , 我没用过。
大概是按照 \text{size} 来何必 , 并且不压缩。
时间复杂度变为 \log n , 有什么用?
网上随便看了一下说是在数据大的时候防止被卡。
个人认为是在还原操作中可以达到 \log 。
如果和其它的数据结构套用的话 , 个人建议还是不要用秩合并。
当然是一个知识浅薄的人的建议。
\text{线段树}
线段树可以说不是树 , 是一种分治。
分治顾名思义就是把一个东西拆成很多个东西合起来。
把“每个东西”逐一解决 , 然后合并。
但是为什么叫线段树呢?
因为线段树是分治的实体化 , 它只是通过不计算一些不用的信息来达到快速的效果。
除了不要那些不需要的 , 线段树还要讲这个查询的东西用已知的东西来加快查询速度。
好了 , 这里扯到分治了。
讲 [l,r] 分为 [l,\text{mid}] 和 [\text{mid+1},r] 处理。
但是我们有预处理 , 所以复杂度变成了 O(\log n) 。
当然除了区间和 , 一些带有一定性质的贡献也是可以维护的 , 例子 \text{max , min} 之类。
对于修改来说 , 分治怎么修改 ?
我们可以在某一个节点时候打下标记 , 这样子就不用全部枚举。
题目的话 , 挺多的 , 不胜枚举。
对于权值线段树 , 喜欢拿来套。
就是将值插入到线段树里面 , 可以做到查询第 k 小 , 查询最大最小,
等很多关于值的不独立贡献问题。
考虑到一个节点表示的 [l,r] , 它的值代表值域在 [l,r] 内的树的个数。
线段树的题目很灵活 , 定义可以自己弄。
只要你觉得它是一种分治的话 , 大部分都可以想到。
这里不得不提及一下树状数组了。
树状数组在处理多维问题上 , 空间和常数都比线段树小。
比如 , 二维中 , 树状数组是 n^2 , 线段树是 4 \times n^2 。
线段树的常数大概是树状数组的两三倍左右。
所以要套一些简单的东西最好还是用树状数组。
这里不讲 , 麻烦。
可以去看我挺喜欢的几个洛谷日报的 : Chanis 的博客 1 , Chanis 的博客 2 , 逆流之时 的博客。
对于二维的线段树 , 建议大家还是多多画图 , 然后理解。
首先要知道 , 其实二维的线段树空间就是 O(n^2) 的。
这样有助于理解。
那是因为查询只有 m 个 , 所以空间可以做到 O(m \log^2 n) 。
二维线段树我只做过 Mokia , 但是因为空间或者常树问题炸了。
当然用 \text{KD-Tree} 或树状数组会好一点。
线段树功能还有很多 , 比如 : OI wiki。
\text{主席树}
主席树 , 因为发明者 (黄 jia tai , 后面两个字忘了) 的拼音可以凑成***总理 , 所以因此确立。
英文是 \text{Chair tree} , 有很多博主写的是 \text{Chair-man tree} ?。
主席树的用处有很多 , 比如查询区间第 k 小等贡献不独立的问题。
其突出特点还在与可以维护可持久化数组或线段树。
何为主席树 ? 主席树是很多线段树连在一起。
你想想 , 如果给你 n 珂线段树 , 那么你是不是可以用这些线段树 ,
经过相加 , 求出 [l,r] 里面的一些值呢 ?
等等 , 时间复杂度不行 , 你不是要相加吗 ?
那我就前缀和维护 , [1,r] 代表第 1 珂到第 r 珂树的和。
等等 , 空间复杂度不行 , 哪来给你 n 珂线段树 ?
那我们考虑一个点只有一个数字 , 所以说如果我要维护一个 [1,r] 前缀和线段树 ,
从 [1,r-1] 到 [1,r] 在线段树就只会有一次插入 ,
而这次插入意味着我占用了 \log n 的空间。
那么我的空间其实就是 n \log n 的。
抽象 ? 看 arfa 的博客。建议前面一段不要看。
可持久化就是说我可以查询在以前的数组的问题 , 也就是现在的数组已经改变。
等等 , 刚刚你说主席树可以维护可持久化数组 ?
那是因为主席树第 r 珂树不一定要维护 [1,r] 的线段树和 ,
可以是 [1,l] 的 (l \leq r )。
这样子我就可以从 l 直接跳到 r 。
思考主席树的问题的时候就想想我有 n 珂线段树我会怎么做。
当让主席树也有缺点 , 它必须处理一些能前缀和的东西。
比如区间众数就是做不了的。
但是如果想最大值的问题 , 其实是可以做的。
因为权值线段树的作用就是把一些贡献独立的东西搞成不独立。
可以这样理解。
例题 ? HH 的项链 , KUR-Couriers , 高级打字机。
关于动态的主席树吗 , 因为主席树是维护一个前缀和的。
把前缀和变为其他数据结构来达到快速的有哪些 ?
上常数最小的树状数组即可。
例题 : 动态排名。
\text{树链剖分}
假如区间上的东西都变成了树上的 , 那么有三种情况。
动态树 , 树分块 , 树上莫队。
而如果是属于动态树操作且没有连接修改之类的东西 ,
那么树链剖分就是最佳选择。
假设我们从每一个叶子节点往上一直到根为一个区间 , 然后数据结构维护。
那么空间可能会炸掉。
树链剖分的作用就是将树分成很多个轻重链 , 大约 \log 个。
再用一些比较简单的 dfn 知识进行存储。
然后再用数据结构维护 , 比如线段树 ,
就能处理一些树上的简单问题 , 空间均摊应该是 O(n) , 而时间就是 O(n \log^2 n) 。
由于树链剖分将树分成了 \log n 个不相交的区间 ,
阉割掉后求 LCA 也是可以的 , 而且常数比较小。
但是加入了线段树以后 , 因为有些轻链很小 , 但是查询还是 \log ,
所以常数就大了。
\text{CDQ 分治}
发明者没有记错的话叫陈丹琪吧。
这种分治对于一些问题的降维非常有用处。
首先考虑分治的定义 , 在线段树中讲过。
cdq 分治可以理解为一个区间分成左右两边 , 也就是 [l,\text{mid}] 和 [\text{mid+1},r] 。
那么贡献可以分为三部分 , [l,\text{mid}] 的内部答案 , [\text{mid+1},r] 的内部答案 , [l,\text{mid}] 对 [\text{mid+1},r] 的贡献。
其实我们就考虑如何求 [l,\text{mid}] 对 [\text{mid+1},r] 的贡献即可。
因为我们可以递归这两个区间的答案。
假如 cdq 分治里面的询问贡献可以独立 , 那么是不是可以前缀和以下 ?
那么我们现在就有了 2 \times m 个左端点是 1 右端点是 r 的询问。
再想想 , 是不是只有修改 (修改默认为单点修改) 才会对询问有贡献 ?
也就是说我们考虑左边的修改如何对右边的询问产生贡献。
等等 , 这些询问好像左端点都是 1 , 那是不是可以排序呢 ?
等等 , 这些修改都是单点修改的话 , 是不是也可以修改的位置排序呢 ?
那么现在就变成了两个有序的序列 , 左边是位置有序修改 , 右边是位置有序询问。
其实这样子我们就可以 O(n) 统计贡献了。
因为右边的询问肯定只会被左边的且位置比自己小的修改贡献到 ,
so , 建立双指针跑就可以了~
考虑到只会递归 \log n 层 , 且每一层的总量是 O(n) ,
所以均摊就是 O(n \log n) , 加上迷一样的总定理 , 准确表示就是 O(f(n) \log n) 。常数还是不小的。
我们可以发现一个东西 , 不管是 cdq 还是点分治 ,
它们每一次处理的都可以算 O(n) , 不过这个处理实在一层里面的 ,
记起来还是 O(n) , 又因为有 O(\log n) 层 , 所以可行。
但是这样做有什么好处呢 ?
省空间啊 , 好打好理解。对于偏序问题就可以直接白嫖一个维度了。 打树套树常数又大 , 还要离散化对吧。
不过 cdq 里面套用的就尽量用一些树了。
详细参考 arfa 的博客。
\text{莫队}
重头戏 \times 1 。莫队的起名应该是因为队长叫莫涛吧。
它可以处理一些重要的贡献不独立的问题。且个人觉得有必要在分块之前讲解。
但是不要因为它是一个复杂度关于根号的算法就和分块比较。
贡献不独立 , 啥意思 ?
举一个简单的例子 , 区间内的 \sum\limits^{\text{数字种类个数}}_{c=1}(\sum\limits^r_{i=l} [num_i=c])^2 , 十万询问。
在说分块之前提一下 , 这个东西可以用 n^{\frac 2 3}m 做出来。
不过莫队不止于此。
如果你看到一个数据结构题目 , 它没有复杂的修改 , 并且答案的计算方式固定 ,
每一个询问没有一个对于答案的计算方式有影响的变量 , 只会有常数。
那么这道题你可以用莫队。
如果你是暴力的话 , 那么时间复杂度莫不是 O(nm) ?
但是如果你把所有询问按照左端点第一关键字右端点第二关键字来排个序再来跑 ,
复杂度就是 O(\text{玄学}) 了。(定义 \text{玄学} \leq nm )
但是很容易想到这样子是可以被卡到 nm 的。
莫队的意思是 , 你给 l 标一个块 , 按照 l 所在的块为第一关键字和 r 为第二关键字排序 ,
那么你的复杂度就变成了 O(nm^{\frac 1 2}) 。
我们来算一下 , 假设块里面有 k 个询问 , 那么移动的的总量其实是 \frac {n^2} k+mk 。
要求两者平均 , 列个方程 , 解出来 k=m^{\frac 1 2} 。 所以总的复杂度就是 O(nm^{\frac1 2}) 。
所以说一般情况下 n 越小 m 越大莫队其实越快。
我们算复杂度的时候要把莫队的关键字弄进去。
莫队可以看成是排序后用将关键字加一或者减一的方式来达到你的询问的一种状态 , 再每一次移动某一个关键字的时候顺便记录一下这个询问到答案 , 并且使移动次数相对少的算法。这是我玩了怎么多年的总结。
所以说不要进入两个误区 : 一是询问的时候如果有对答案有影响的参数 (那么这道题就不是莫队)。二是你可能认为用多个桶来存答案来提高复杂度 , 这个是不行的 ! 莫队只能保存现有的一个状态 !
对于第二个误区 , 举个例子 , 考虑二维的莫队 , 矩阵 n \times n ,
如果我把询问拆成一维 , 行吗 ? 可以啊!
每一个二维的询问我拆成很多个一维的询问 , 跑完以后每个询问开数组进行合并。
空间不够 ? 那么我就把询问分成几组 , 一组一组搞。
好像是正确的 , 复杂度算出来是 n^2m^{\frac 2 3} , 看起来很快。
但是我们可以清晰的发现这样子是不对的 ,
我们无法同时转移多个数组 !
说起二维莫队 , 我就来讲讲莫队复杂度的规律。
通常情况下 , 你的莫队有 k 个关键字的话 (就像带修莫队有 3 个关键字) :
你的复杂度就是 O(\text{可移动位置个数} \times \text{询问}^{\frac{k-1} k}) , 简单写就是 O(nm^{1-k^{-1}}) 。
这时候你的块就是 \frac{n}{m^{k^{-1}}} 。
对于通常情况下的 k 维莫队 , 可以认为它的复杂度是 O(n^km^{\frac {2k-1} {2k}}) ,
所以说二维莫队的复杂度就是 O(n^2m^{\frac 3 4}) 。是不是很慢。
其实还好 , 当然我们可以认为 k 维分块的复杂度是 O(n^{\frac {k-1}{k}}m) 。
莫队的复杂度这样定义以后不会再因为其他数据结构的加入而更改 ,
所以一般情况下我们不会用莫队套用 \log 数据结构 , 复杂度太高。
有时候如果我们不支持加入或删除操作 , 比如区间众数 ,
我们可以用回滚莫队 , 比较简单 , 自己可以网上查阅。
当然我们可以套一个分块进去 , 因为分块的修改可以达到 O(1) , 而查询通常可以达到 n^{\frac 1 2} , 这样子复杂度就比较小 O(n^{\frac 1 2}m+nm^{\frac 1 2}) 。
莫队还有一个骚操作 , 就是二次离线莫队了。
当然我还不是全部都会 , 比如我不知道如何将空间变为 O(m) 。留坑。
对于一些数对的题目 , 可以用这个东西 , 比如查询区间逆序对。
你会发现其实每一次的移动的贡献就是查询某个区间比我小或者比我大的东西 ,
而这个贡献其实是可以差分的 , 但是直接做的话又变成 nm^{\frac 1 2} \log n 了……
所以我们可以把这些询问存下来 , 再次离线处理这 nm^{\frac 1 2} 询问。 我们只需要用分块或者其它东西 ,
满足这些询问在一起处理的时候达到 O(1) 的复杂度即可。
时空复杂度都是 O(nm^{\frac 1 2}) 。
\text{upd : 2019-12-19}
先顺带一提 , 我第一次看懂二次离线莫队的博客是 ZZQ 的博客。
巨佬 alpha1022 给我讲了 n+m 空间的做法。
下方的箭头有很多意义 , 有 到 和 对 的意思。
首先我们考虑从 [l,r] \implies [l,r+k] , 这些询问是不是有一些特点。
假如一个 i \in (l,r] , 那么每一个的贡献其实就是 num_i \implies [l,i)
套路差分一下就是 num_i \implies [1,i)-num_i \implies [1,l) 了。
左端点看起来不能处理 , 但是会发现每一个询问的转移 $l$ 是不会变的。
所以说我们的空间复杂度就变成了 $O(n+m)$。
但是这样子好像不那么严谨 , 我继续写出来所有的转移供打码。
为了方便我还是写只有 $\pm1$ 的情况。
$[l,r] \implies [l,r+1]$ : $+num_{r+1} \implies [1,r]-[1,l-1]
[l,r] \implies [l,r-1]$ : $-num_r \implies [1,r-1]-[1,l-1]
[l,r] \implies [l+1,r]$ : $-num_l \implies -[1,l]+[1,r]
[l,r] \implies [l-1,r]$ : $+num_{l-1} \implies -[1,l-1]+[1,r]
注意一下我们把第一种情况都写在前面 , 第二种都是后面。
\text{upd : 2019-12-27}
我自己出了一道题目 , 给教堂拿去比赛了。不过没人切。
题目描述 : \sum\limits_{1 \leq i,j \leq n}[num_i \mod num_j=k] ,
我们其实要考虑到 , 如果 num_i=k 且 num_j > k 的话也是有贡献的,
所以我们不妨把这个东西拆成两种贡献的数对 ,
第一种贡献叫做被膜为 k , 第二种贡献为膜你为 k 即可。
Link
\text{分块}
重头戏 \times 2 。分块是一种数据结构 , 也是一种思想。
具体的可以看国家集训队的论文 , 那个还是挺有用的。
分块的作用有三个 ,
一 . 将某种操作变成极快 , 把相对的操作变为较快。如查询 O(1) , 修改 O(n^{\frac 1 2}) 。
二 . 将某些东西简单化快速化 , 比如分段打表 , 我写的平衡树优化。
三 . 将某些经典数据结构做不了的东西变为可行。
分块还可以套各种东西 ,
只有你觉得可以 , 分块就可以。
用处一 ,
比如在上面的莫队那道题中 , 我们用到了修改为 O(1) 的分块。
权值分块等可以在 O(1) 插入的限制内做很多查询为 O(n^{\frac 1 2}) 的东西。
具体的话可以看一道叫作业的题目。
那什么时候查询是 O(1) 呢?
具体来说就是建立一颗树 , 每个节点代表一个区间 , 像线段树一样。
不同的是对于区间长度为 $k$ 的节点 , 它有 $k^{\frac 1 2}$ 个儿子 , 每个儿子对应区间长度 $k^{\frac 1 2}$。
这样子的话查询的复杂度就是 $O(\log \log n)$ 的了。
对于修改 , 区间修改和单点修改都跟线段树一样的 , 打标记即可。
修改的复杂度为 $\sum\limits^{\log \log n}_{i=1} n^{\frac 1{2^i}}$ , 加起来不会超过 $2n^{\frac 1 2}$。
$\text{upd : 2019-12-27}
我自己出了一道题目 , 给教堂拿去比赛了。不过也没人切。Link。
题目描述 : \max\limits_{l \leq i,j \leq r\ ,\ num_i \not = num_j ,\ \text A(num_i)-\text A(num_j) \leq K} \text A(num_i) 。\text A(T)=\sum\limits^n_{i=1} [T=num_i] 。
这道题题面看起来挺吓人 , 其实很简单。
考虑到只有其它的数字的出现次数跟自己的相减小于 K 的时候 , 有贡献
所以我们就可以维护一个出现次数的数组 ,
再维护一个出现次数再我和我 -K 的数的个数的数组 , 这个东西维护 O(1) 。
这样子的话用分块进行维护 O(1) 就可以了啊 , 查询 O(n^{\frac 1 2})
具体方法是将上面两个数组相乘 , 如果不是 0 就是有贡献 ,
这个时候用分块去求最大值即可 , 回滚莫队也行 , 不过麻烦。
那我们还是要说说为什么有些时候我们的分块查询和修改都不是 O(1) ,
简单的说 , 就是要求查询和修改的复杂度平均了 , 也就是某一个操作太复杂 ,
比如说 , 我查询这个东西其实是要前缀和的,
那么我们的复杂度就上去了。
用处二 ,
那么我就举上面说的两个例子 :
第一个例子 , 虽然这种方法很逊而且不实用 , 但是我还是讲讲。
众所周知 \text{FHQ Treap} 是一个常数及其大的算法。
但是我们可以进行分块 , 数据大的时候 , 我们可以做到期望复杂度 O(\log \sqrt{n}) 。
其实我们就是开一个线段树 , 对每一段权值进行分块 , 每一段权值的总信息交给线段树维护 , 每一段权值的分信息交给 \text{Treap} 维护即可。
这样子的话操作的复杂度就变为了 O(\log k+\log \left\lfloor\frac{n}{k}\right\rfloor) (k 为块的数量)
当然代码很长 , 而且容易被卡到 O(\log n) , 所以我说是对“常数”的优化。
这个东西的广义版就更强了 , 应该就是 \text{Sqrt tree} , 然而我不会。
奇技淫巧 ? 看下一个 :
第二个例子 , 题目是求 T=10^{11} 内的质数个数 , 正解洲阁筛暴毙 ,
本人没有实际做过这道题 , 所以这里是不标准的浅谈。
所以我们分段打表即可 , 我们考虑对预处理一个 [1,\left\lfloor\frac{T}{k}\right\rfloor \times k] 的表 (k 为块的数量)
那么我们存储的空间为 O(k) , 查询的时间复杂度为 O(\left\lfloor\frac{T}{k}\right\rfloor) 。
那么显然 k 可以设置成 k=n^{\frac 1 2} , 10^{5.5} 。
由于我们每次查询一个值其实是可以用 \text{Miller} 来求的 , 假如这种算法的复杂度为 30 \log n ,
那么这个算法的复杂度就将是 3 \times 10^{6.5} \log 10^{11} 。
会不会卡不过去啊 , 其实就是卡不过去 , 这样子只能骗分。
我们不妨比较精确的列一个方程 , 如 3 \times \left\lfloor\frac{T}{k}\right\rfloor \log 10^{11}=k , 当然不用准确求出。
好奇的算一下 \left\lfloor\frac{T}{k}\right\rfloor=\left\lfloor\frac{k}{30}\right\rfloor , k=(30T)^{\frac 1 2} 。
这样子我们的空间加了 \sqrt{30} 倍 , 时间复杂度降了 \sqrt{30} 倍。
上述办法可能还是会被卡 , 其实可以将 k 直接调成 10^7 ,
这样子复杂度就只有 3 \times 10^5 \log 10^{11} 了!
于是这种复杂度就变成了 O(\frac T k \log T) , 常数 30 。空间 O(k) 。
洲阁筛复杂度 O(\frac{T^{\frac{3}{4}}}{\ln T}) , 空间 O(n^{\frac 1 2}) 。
\text{upd : 2020-1-26}
有一道题 , 可以打表 , 分块打表更快。
求 \sum\limits^n_{i=1} f(i) , f(a) 为 a 的最小质因子。
这道题可以打表 , 其实也可以 \text{Min25} 。 然后讲讲如何打表。
由于此题贡献独立 , 所以可以分块。
首先我们肯定需要本地预处理 , 我们将 n 分成 \frac n k 个块 , 每个块有 k 个数。
考虑到正整数 a 的最小质因子不大于 a^{\frac 1 2} , 所以我们预处理 n^{\frac 1 2} 里的质数个数。
接着我们打表即可。
考虑到要一个一个搞 , 所以我们单独的 (可理解为边角暴力) 复杂度为 O(k \ln n^{\frac 1 2}) , 空间为 O(\frac n k) 。
要求两者平衡 , \frac n k=k \ln n^{\frac 1 2} , 显然 k={(\frac n {\ln n^{\frac 1 2}})}^{\frac 1 2}=\frac{n^{\frac 1 2}}{\ln^{\frac 1 2} n^{\frac 1 2}} , 时空复杂度就都是 O(\sqrt{n \ln n^{\frac 1 2}}) 。
可以承受大约 10^{12} ~10^{13} 的范围。
用处三 ,
分块的形式千变万化 , 这里先举一个例子 :
每次修改关于区间 [l,r] , 将 num_l 放在 num_r 的位置上 , (l,r] 向左平移。
每次查询是一个区间某个数字的出现次数 , n,m=10^5 。
考虑分块 , 如果每一个块都开一个队列的话 , 并且实时记录一下这个块的出现次数的话,
就可以做到修改查询 O(n^{\frac 1 2}) 。
当然分块还可以做一些问题 , 比如查询区间第 k 小。
正解的话最快的是划分树 , 其次是主席树 , 时间复杂度都是 O(m \log n) 的。
分块的话我们可以考虑权值分块+区间分块 ,
前面不是说过主席树维护前缀和吗? 那我分块也效仿 ,
设 \text{Recf}[l,r] 为 [1,l] 区间中数值在第 r 块的个数 ,
再来一个 \text{Times}[l,r] 为 [1,l] 区间中数值为 r 的个数。
查询的时候直接差分一下跑分块即可。时间复杂度 O(mn^{\frac 1 2}) , 空间 O(n^{\frac 3 2 }) 。
但是不要以为分块只是为了把经典数据结构思路简单化了。
能不能考虑如果区间第 k 小带单点修改呢 ? 难道我们要用 n^{\frac 1 2} \log n 的二分 ?
考虑我们的查询复杂度是 O(k \log n) , 修改是 O(\left\lfloor\frac{n}{k}\right\rfloor) , 我们列一个方程。
最后可以解出 , k=\left\lfloor\frac{n}{\log n}\right\rfloor^{\frac 1 2} , 那么复杂度就变为 n^{\frac 1 2} \log^{\frac 1 2} n , 人们喜欢写成 \sqrt{n \log n} 。
有一些经典的问题 , 比如区间众数。
我们把这个数列分成 $\frac n k$ 块 , 每个块有 $k$ 个数。
然后我们预处理一个 $\text{Ans}[l,r]$ 代表 $l$ 块到 $r$ 块的答案 , $\text{Times}[l,x]$ 代表 $1$ 到 $r$ 块数字为 $x$ 的个数。
这样子的话 , 我们每一次查询 , 就已经有了一个整块的答案 ,
然后在散块那里去更新看有没有比这个众数的出现次数更多的。
时间复杂度 $O(mk)$ , 空间复杂度 $O(\frac{n^2}{k})$。两者可达最优 , 为 $O(nm^{\frac 1 2})$。
最最最经典的问题 , 莫过于树套树了。主要操作可以看二逼平衡树。
我们设一个 $\text{Times}[i,j]$ 为在第 $i$ 块,权值为 $j$ 的个数。设 $\text{Fock[i,j]}$ 代表 $1$ 到第 $i$ 块权值为 $j$ 的个数。
设 $\text{Block}[i,j]$ 代表 $1$ 到第 $i$ 块,权值在第 $j$ 块的个数。查询的时候我们直接搞一搞,修改我们也搞一搞就好了。
查询 $k$ 的排名 , 只需要对边角暴力一下 , 然后用 $\text{Block}$ 统计答案 (要对本块再次进行暴力)。
查询排名为 $k$ 的数字,我们再次把边角加起来 , 然后同 $\text{Block}$ 一起统计答案。
单点修改 , 我们把 $\text{Times}$ 改一下 , 然后对后面的 $\text{Fork}$ 和 $\text{Block}$ 进行实时维护。
查 $k$ 的前继,我们把边角加起来 , 然后看 $k$ 这个块有没有前继 , 没有用 $\text{Block}$ 往前跳 , 后继同理。
这题 , 未来日记。比较好玩。
如果有人将此题的数据卡 $\text{std}$ 的复杂度到 $O(mn^{\frac 1 2} \log _{1+\frac n m}n)$ , 那么他巨。
首先我们设一个 $\text{Times}[i,j]$ 为第 $i$ 块,权值为 $j$ 的个数。
设 $\text{Fock}[i,j]$ 代表 $1$ 到第 $i$ 块权值为 $j$ 的个数。设 $\text{Block}[i,j]$ 代表 $1$~$i$ 块 , 权值在第 $j$ 块的个数。
满足 $\text{Times}[i,j]$ 实时更新, 且一次我们只触及 $x,y$ 两个数,等于单点修改 , 所以只需要 $n^{\frac 1 2}$ 的时间就可以更新 $\text{Fock}[i,j]$ , $\text{Block}[i,j]$ 同理。
其次 , 我们对大块进行修改的时候,直接一个并查集即可。
不过我们会发现,对于残余的块修改的时候 , 我们可能会违背这个块的并查集。所以对残余块修改时重构一下这个块的并查集。
其它题解上面说只重构 $x,y$,常数减。
然后就是一些关于位置的问题。
比如 , 两个操作 , 第一个操作 , 查询一个区间某个数的出现次数。
第二个操作 , 对于区间 $[l,r]$ , 将 $num[r]$ 转移到 $num[l]$ , 然后将 $num[i]$ 转移到 $num[i+1]$ , $i \in (l,r]$。 就是平移。
这个东西只需要亿点细节 , 用分块就可以了。
想想 , 在分块里面套队列的话 , 每一次平移 , 前后搞一下即可。
还有一道题 , 天降之物。
因为这道题我并没有 $\text{A}$ 且我的方法会被卡 (应该)。 所以我就随便说说。
由于答案只能由左右两边的点与自己构成,意思就是一个 $x$ 和一个 $y$,
如果它们之间存在着 $x_1$,那么对答案产生贡献的就只有 $(x_1,y)$ 而不可能是 $(x,y)$ , $\text{Dis}(x,y)$ 一定大于 $\text{Dis}(x_1,y)$。
考虑分块,记录一个 $\text{Ans}[i,x,y]$ 代表第 $i$ 块 $x$ 和 $y$ 的答案,动态空间可以做到 $n^{\frac 3 2}$。
然后记录一个 $\text{Dis}[i,x,0/1]$ 代表第 $i$ 块最左边 (或者右边) 离块的边缘的距离是多少。
前者先记录单独每一个块的答案,后者用来记录跨块的答案贡献。当然,如果你觉得自己的空间写得十分的小的话,你可以尝试搞两个分块,那就不用后者了。
但是考虑到 $\text{Ans}[i,x,y]$ 的空间需要动态,所以需要一个 $\text{Num}[i,x]$ 记录第 $i$ 块 $x$ 的编号,然后还要开一个队列来维护这些编号 , 空间常数巨大。
$\text{upd : 2020-1-28}
\text{upd : 2020-2-20}
\text{upd : 2020-2-23}
三天说的是同一个东西我就直接全部一起 \text {upd} 了。
本 \text{idea} 是康了 \text{lxl} 的建表法进行的少许改动 , 所以还是称之为 \text{lxl} 建表法。
本算法可以有效的提高求 \text{rmq} 的使用效率 , 当然 , 是比笛卡尔树会简单很多。
但是 , 本算法也有缺点 ,不过不是特意卡的话 , 就不会爆。不过 , 如果是特意卡的话 , 时间复杂度也不会比普通的 \text{st} 表慢。
以下可以默认 n=m=10^7 , 且求最大值。
我们把序列分成 \frac n k 块 , 每块有 k 个数。
那么我们对于块的最大值进行建表 , 时间复杂度就是 \frac n k \log \frac n k 。
对于查询来说, 我们记录一下每一个块的前缀和后缀最大值 , 就可以做到查询一个区间 O(1) 了。
但是会发现 , 如果两个端点都在同一个块里面 , 我们就必须进行暴力求解。
查询时间复杂度要卡的话 , O(mk) , 建表 \frac n k \log \frac n k 。
列出等式 , mk=\frac n k \log \frac n k , 可以得出 k\approx 4.58862 , 也就是说整个程序的运算量大概在 4.58862 \times 10^7 左右 , 加常数很大。
然后我们有两种解决方式 , 一种是直接调块 , 一种是套线段树。
第一种 : 我们可以发现 , 两个端点都在同一块的几率是 (\frac n k)^{-2} , 那么如果我们的询问均摊 , 就会有 (\frac n k)^{-2}m 个询问是在同一块的。
我们乘上这一段的复杂度 (\frac n k)^{-2}mk , 等于 \frac {mk^3} n 。
我们可以设 , k=\log n , 建表的时间复杂度就是 O(n \log(n \log^{-1} n) \log^{-1} n) , 会发现 {n^{-2}m\log^3 n} 还不到 1 。
第二种 : 我们对于每一个块建立线段树 , 建表的理论复杂度是不变的 , 但常数会加。
这样子的话列出方程 : \frac n k \log \frac n k=m \log k , k\approx 7.177707 ,
完结。
$\text{upd : 2020.2.1}
一个新的 \text{idea} , 但是并没有程序验证过。
就是对于那些要查询历史的分块 , 存起来即可。
比如 , 区间赋值修改为 x , 历史查询区间和。
那么对于一个整块来说 , 记录下 x 即可。那么查询到这个地方的和就是这个块的大小乘上 x 。
对于一个散块来说 , 我们要求暴力重构整个块 , 然后记录区间和。可以直接将整个块照搬下来。
会发现 , 假如有 \frac n k 块 , 每块的大小为 k 。那么整块的记录空间就是 O(\frac {mn} k ) 的。因为每次查询只会有两个散块 , 所以记录空间就是 O(mk) 的。
对于没有影响的块就直接把上一次的标记打上来即可。常数巨大。
查询的时候可以直接跳到某个历史中的标记群 , 然后进行查询。
撤回到某个历史时刻 , 其实直接把那个历史时刻后的历史时刻的标记群都看作垃圾即可。
对于刚才的例子 , 时间复杂度为 O(\frac {mn} k+mk) 。